Android中替代ReplacementSpan的方法

6
我有一个应用程序,可以分页显示大量文本,并为每个单词或句子设置多个span。我正在使用ReplacementSpan来绘制每个单词的背景。我不能使用BackgroundSpan,因为它太简单了,无法控制画布。由于ReplacementSpan扩展了MetricAffectingSpan,这会影响文本的布局,从而完全破坏我的分页。我正在使用StaticLayout来计算每个页面的文本,但StaticLayout不允许跨度,因此它可以预先计算跨度大小的影响。
是否有替代ReplacementSpan的方法?如何在不影响文本本身的大小和布局的情况下绘制所需的背景?
以下是我的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);
  }
}
1个回答

5

试试这个简单的span,它会在所有的span上绘制实心红色背景(即使它是多行span),但你可以绘制任何你喜欢的东西:

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);
        }
    }
}

测试代码(将0ssb.length()作为startend设置不太高效,因此您可以进行优化):

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) + "]");

编辑

如果您有多个单词需要标记/突出显示,您可以使用以下修改版本:

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);
            }
        }
    }
}

测试代码:

    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);

非常感谢您的帮助!有了这个解决方案,我可以为每个单词设置背景,而文本仍然可以自动换行而不影响其布局。 - mthandr
刚刚找到一件事情:Layout#getSelectionPath,检查一下,也许有帮助。 - pskink
布局非常强大。我还有几个与文本相关的功能,一定会查看在布局中还能做什么。谢谢。 - mthandr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接