如何在多行TextView的指定文本段下方绘制自定义Drawable?
解决多行TextView中指定文本段的自定义下划线问题
针对你遇到的问题,这里提供几个实用的解决方案,均基于Android原生Span系统实现,无需额外依赖:
方案1:改进ReplacementSpan(推荐,无需自定义TextView)
之前的ReplacementSpan失效大概率是因为没有正确保留原文本的绘制逻辑。下面的自定义Span会先绘制原文本,再在目标短语下方绘制自定义下划线,支持多行场景(只要目标短语在同一行):
自定义Span代码
public class CustomUnderlineSpan extends ReplacementSpan { private final Drawable customUnderline; private final int underlineHeight; public CustomUnderlineSpan(Context context) { // 初始化下划线高度(2dp转px) underlineHeight = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, context.getResources().getDisplayMetrics() ); // 创建自定义下划线Drawable(可替换为自己的Drawable) customUnderline = new ShapeDrawable(new RectShape()); ((ShapeDrawable) customUnderline).getPaint().setColor(Color.RED); ((ShapeDrawable) customUnderline).getPaint().setStrokeWidth(underlineHeight); } @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { // 返回原文本宽度,不改变原有布局 return (int) paint.measureText(text, start, end); } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { // 先绘制原文本,保证内容正常显示 canvas.drawText(text, start, end, x, y, paint); // 计算下划线垂直位置:基于文本基线+字体descent,让下划线紧贴文本下方 float underlineY = y + paint.getFontMetricsInt().descent - underlineHeight / 2f; // 设置Drawable绘制边界 customUnderline.setBounds( (int) x, (int) (underlineY - underlineHeight), (int) (x + paint.measureText(text, start, end)), (int) underlineY ); customUnderline.draw(canvas); } }
使用方式
SpannableString spannable = new SpannableString("这里是多行TextView的测试文本,目标短语会被加上自定义下划线,后续内容继续换行显示..."); // 定位目标短语的起始和结束索引 int targetStart = spannable.toString().indexOf("目标短语"); int targetEnd = targetStart + "目标短语".length(); // 给目标文本设置自定义Span spannable.setSpan( new CustomUnderlineSpan(this), targetStart, targetEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); yourTextView.setText(spannable);
方案2:自定义TextView结合标记Span
如果需要更灵活的控制(比如同时处理多个不同样式的下划线),可以通过自定义TextView,在onDraw阶段获取目标文本坐标后绘制:
1. 定义标记用的空Span
public class UnderlineMarkerSpan extends CharacterStyle { // 仅用于标记目标文本位置,无需实现逻辑 }
2. 自定义TextView代码
public class UnderlineTextView extends androidx.appcompat.widget.AppCompatTextView { private Drawable customUnderline; private int underlineHeight; public UnderlineTextView(Context context) { super(context); init(); } public UnderlineTextView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { underlineHeight = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics() ); customUnderline = new ShapeDrawable(new RectShape()); ((ShapeDrawable) customUnderline).getPaint().setColor(Color.BLUE); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Layout layout = getLayout(); if (layout == null || !(getText() instanceof Spannable)) return; Spannable spannableText = (Spannable) getText(); UnderlineMarkerSpan[] markerSpans = spannableText.getSpans(0, spannableText.length(), UnderlineMarkerSpan.class); for (UnderlineMarkerSpan span : markerSpans) { int spanStart = spannableText.getSpanStart(span); int spanEnd = spannableText.getSpanEnd(span); // 确认目标短语在同一行(题目已保证,此处做容错) int lineIndex = layout.getLineForOffset(spanStart); if (layout.getLineForOffset(spanEnd) != lineIndex) continue; // 获取目标文本的左右x坐标和底部y坐标 float leftX = layout.getPrimaryHorizontal(spanStart); float rightX = layout.getPrimaryHorizontal(spanEnd); float bottomY = layout.getLineBottom(lineIndex) - underlineHeight / 2f; // 绘制下划线 customUnderline.setBounds( (int) leftX, (int) (bottomY - underlineHeight), (int) rightX, (int) bottomY ); customUnderline.draw(canvas); } } }
使用方式
SpannableString spannable = new SpannableString("多行文本测试,标记的目标短语会显示自定义下划线..."); int targetStart = spannable.toString().indexOf("目标短语"); int targetEnd = targetStart + "目标短语".length(); // 添加标记Span spannable.setSpan(new UnderlineMarkerSpan(), targetStart, targetEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // 给自定义TextView设置文本 yourUnderlineTextView.setText(spannable);
方案3:LineBackgroundSpan结合标记Span
如果需要和行背景效果结合,可以用LineBackgroundSpan,在绘制行背景时定位目标短语:
自定义LineBackgroundSpan代码
public class TargetLineBackgroundSpan implements LineBackgroundSpan { private final Drawable customUnderline; private final int underlineHeight; public TargetLineBackgroundSpan(Context context) { underlineHeight = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, context.getResources().getDisplayMetrics() ); customUnderline = new ShapeDrawable(new RectShape()); ((ShapeDrawable) customUnderline).getPaint().setColor(Color.GREEN); } @Override public void drawBackground(Canvas canvas, Paint paint, int left, int right, int top, int baseline, int bottom, CharSequence text, int start, int end, int lineNumber) { if (!(text instanceof Spannable)) return; Spannable spannable = (Spannable) text; UnderlineMarkerSpan[] markerSpans = spannable.getSpans(start, end, UnderlineMarkerSpan.class); for (UnderlineMarkerSpan span : markerSpans) { int spanStart = spannable.getSpanStart(span); int spanEnd = spannable.getSpanEnd(span); // 计算目标短语在当前行的左右偏移 float spanLeft = left + paint.measureText(text, start, spanStart); float spanRight = left + paint.measureText(text, start, spanEnd); // 绘制下划线 float underlineY = bottom - underlineHeight / 2f; customUnderline.setBounds( (int) spanLeft, (int) (underlineY - underlineHeight), (int) spanRight, (int) underlineY ); customUnderline.draw(canvas); } } }
使用方式
SpannableString spannable = new SpannableString("多行文本,目标短语在这一行,测试LineBackgroundSpan的效果..."); int targetStart = spannable.toString().indexOf("目标短语"); int targetEnd = targetStart + "目标短语".length(); // 添加标记Span spannable.setSpan(new UnderlineMarkerSpan(), targetStart, targetEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // 添加LineBackgroundSpan到整个文本 spannable.setSpan(new TargetLineBackgroundSpan(this), 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); yourTextView.setText(spannable);
内容的提问来源于stack exchange,提问作者RusArtM
相关产品推荐
相关产品推荐

