You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在多行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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 06:37:07