Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alternative to ReplacementSpan in Android

I have an app that pages large text and sets multiple spans to each word or sentence. I am using ReplacementSpan to draw the background for each word. I cannot use BackgroundSpan because it is too simple and doesn't give me control over the canvas. Because of ReplacementSpan extending MetricAffectingSpan which affects the layout of the text, breaking my paging completely. I am using StaticLayout to calculate the text for each page, and StaticLayout doesn't allow spanning so it can calculate a priori the spanning size influences.

Is there a replacement to ReplacementSpan? How can I draw the background I want without affecting the size and layout of the text itself?

This is the code for my replacementspan:

public class BackgroundColorWithoutLineHeightSpan extends ReplacementSpan {

  private static final float DP_ACTIVE = ViewsUtils.dpToPx(4);
  private static final int DP_OUTSIDE_PADDING = (int) ViewsUtils.dpToPx(6);
  private static final float DP_PHRASE = ViewsUtils.dpToPx(4);
  private static final float DP_ROUNDED = ViewsUtils.dpToPx(3);

  private final int mColor;
  private final int mTextHeight;
  private int mBorderColor;
  private boolean mIsSelected;
  private boolean mIsPhrase;

  public BackgroundColorWithoutLineHeightSpan(int color, int textHeight, boolean isPhrase) {
    mColor = color;
    mTextHeight = textHeight;
    mIsPhrase = isPhrase;
  }

  public BackgroundColorWithoutLineHeightSpan(int color, int textHeight, boolean isSelected, int borderColor, boolean isPhrase) {
    mColor = color;
    mTextHeight = textHeight;
    mIsSelected = isSelected;
    mBorderColor = borderColor;
    mIsPhrase = isPhrase;
  }

  @Override
  public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
    return Math.round(measureText(paint, text, start, end));
  }

  @Override
  public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {

    canvas.save();

    Rect newRect = canvas.getClipBounds();
    newRect.inset(-DP_OUTSIDE_PADDING, -DP_OUTSIDE_PADDING);

    canvas.clipRect(newRect, Region.Op.REPLACE);

    float measuredText = measureText(paint, text, start, end);

    int paintColor = paint.getColor();

    if (!mIsSelected) {
      RectF rect;
      rect = new RectF(x, top, x + measuredText, top + mTextHeight);

      paint.setStrokeWidth(0.0f);
      paint.setColor(mColor);
      paint.setStyle(Paint.Style.FILL);

      canvas.drawRoundRect(rect, DP_ROUNDED, DP_ROUNDED, paint);

    } else {

      RectF rect;
      if (mIsPhrase) {
        rect = new RectF(x - DP_PHRASE, top - DP_PHRASE, x + measuredText + DP_PHRASE, top + mTextHeight + DP_PHRASE);
      } else {
        rect = new RectF(x - DP_ACTIVE, top - DP_ACTIVE, x + measuredText + DP_ACTIVE, top + mTextHeight + DP_ACTIVE);
      }
      paint.setStrokeWidth(0.0f);
      paint.setColor(mColor);
      paint.setStyle(Paint.Style.FILL);

      canvas.drawRoundRect(rect, DP_ROUNDED, DP_ROUNDED, paint);

      RectF border;
      if (mIsPhrase) {
        border = new RectF(x - DP_PHRASE, top - DP_PHRASE, x + measuredText + DP_PHRASE, top + mTextHeight + DP_PHRASE);
      } else {
        border = new RectF(x - DP_ACTIVE, top - DP_ACTIVE, x + measuredText + DP_ACTIVE, top + mTextHeight + DP_ACTIVE);
      }

      paint.setColor(mBorderColor);
      paint.setStrokeWidth(4.0f);
      paint.setStyle(Paint.Style.STROKE);

      canvas.drawRoundRect(border, DP_ROUNDED, DP_ROUNDED, paint);
    }

    paint.setStyle(Paint.Style.FILL);
    paint.setColor(paintColor);
    canvas.drawText(text, start, end, x, y, paint);

    canvas.restore();
  }

  private float measureText(Paint paint, CharSequence text, int start, int end) {
    return paint.measureText(text, start, end);
  }
}
like image 421
mthandr Avatar asked Aug 21 '16 00:08

mthandr


1 Answers

try this simple span, it draws solid red background over all the span (even if it is multi-line span) but you can draw whatever you like:

class LBS implements LineBackgroundSpan {
    private final TextView tv;
    private int start;
    private int end;

    public LBS(TextView tv, int start, int end) {
        this.tv = tv;
        this.start = start;
        this.end = end;
    }

    @Override
    public void drawBackground(Canvas c, Paint p, int left, int right, int top, int baseline, int bottom, CharSequence text, int start, int end, int lnum) {
        Layout layout = tv.getLayout();
        int startLine = layout.getLineForOffset(this.start);
        int endLine = layout.getLineForOffset(this.end);
        if (startLine <= lnum && lnum <= endLine) {
            if (startLine == lnum) {
                left = (int) layout.getPrimaryHorizontal(this.start);
            }
            if (endLine == lnum) {
                right = (int) layout.getPrimaryHorizontal(this.end);
            }
            int origColor = p.getColor();
            p.setColor(Color.RED);
            c.drawRect(left, top, right, bottom, p);
            p.setColor(origColor);
        }
    }
}

testing code (setting 0 and ssb.length() as a start and end is not very efficient so you can optimize it):

TextView tv = new TextView(this);
setContentView(tv);
tv.setTextSize(32);
SpannableStringBuilder ssb = new SpannableStringBuilder("Chop a handfull spinach, pork shoulder, and dill in a large cooker over medium heat, cook for six minutes and varnish with some bok choy.");
LBS span = new LBS(tv, 30, 100);
ssb.setSpan(span, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(ssb);

Log.d(TAG, "onCreate text [" + ssb.subSequence(30, 100) + "]");

EDIT

if you have multiple words to mark / highlight you could use such a modified version of it:

class LBS implements LineBackgroundSpan {
    TextView tv;
    List<Pair<Integer, Integer>> ranges;

    public LBS(TextView tv) {
        this.tv = tv;
        ranges = new ArrayList<>();
    }

    public void add(int start, int end) {
        ranges.add(new Pair<>(start, end));
    }

    @Override
    public void drawBackground(Canvas c, Paint p, int left, int right, int top, int baseline, int bottom, CharSequence text, int start, int end, int lnum) {
        Layout layout = tv.getLayout();
        for (Pair<Integer, Integer> range : ranges) {
            int startLine = layout.getLineForOffset(range.first);
            int endLine = layout.getLineForOffset(range.second);
            if (startLine <= lnum && lnum <= endLine) {
                if (startLine == lnum) {
                    left = (int) layout.getPrimaryHorizontal(range.first);
                }
                if (endLine == lnum) {
                    right = (int) layout.getPrimaryHorizontal(range.second);
                }
                int origColor = p.getColor();
                p.setColor(Color.RED);
                c.drawRect(left, top, right, bottom, p);
                p.setColor(origColor);
            }
        }
    }
}

test code:

    TextView tv = new TextView(this);
    setContentView(tv);
    tv.setTextSize(32);
    String text = "Chop a handfull spinach, pork shoulder, and dill in a large cooker over medium heat, cook for six minutes and varnish with some bok choy.";
    SpannableStringBuilder ssb = new SpannableStringBuilder(text);
    LBS span = new LBS(tv);

    String[] words = {
            "spinach, pork shoulder", "cooker", "with some bok choy",
    };
    for (String word : words) {
        int idx = text.indexOf(word);
        span.add(idx, idx + word.length());
    }

    ssb.setSpan(span, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ssb);
like image 168
pskink Avatar answered Nov 01 '22 16:11

pskink