我有一个可以分页显示大文本并为每个单词或句子设置多个跨度的应用程序。我正在使用ReplacementSpan为每个单词绘制背景。我不能使用BackgroundSpan,因为它太简单了,也无法控制 Canvas 。由于ReplacementSpan扩展了MetricAffectingSpan,这会影响文本的布局,从而完全破坏了我的分页。我正在使用StaticLayout来计算每个页面的文本,并且StaticLayout不允许跨度,因此它可以先验地计算跨度大小的影响。

替换范围有替代品吗?如何在不影响文本本身的大小和布局的情况下绘制所需的背景?

这是我的替换范围的代码:

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

最佳答案

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

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

10-08 15:34