Skip to content

使用 ReplacementSpan

ReplacementSpanandroid.text.style里的一个抽象类。

我们主要在draw方法中对文本进行操作。

实现下划线效果

准备工作

新建一个drawable_tv_underline.xml。用来充当下划线。 可以修改下划线的颜色和粗细。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="line">
    <stroke
        android:width="2dp"
        android:color="#3792e5" />
</shape>

以此类推,再建立一个drawable_tv_underline2.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="line">
    <stroke
        android:width="1dp"
        android:color="#FF7700" />
</shape>

实现Span

新建DrawableSpan.kt文件,DrawableSpan继承ReplacementSpan

需要传入一个Drawable对象来当下划线。

draw方法中,我们先计算出文字的显示范围,以此来确定下划线的尺寸和位置。

import android.graphics.*
import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan
import kotlin.math.roundToInt

class DrawableSpan(private val drawable: Drawable) : ReplacementSpan() {
    private val padding: Rect = Rect()
    var textColor = Color.parseColor("#3792e5")

    init {
        drawable.getPadding(padding)
    }

    override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
        val rect = RectF(x, top.toFloat(), x + measureText(paint, text, start, end), bottom.toFloat())
        drawable.setBounds(rect.left.toInt() - padding.left,
                rect.top.toInt() - padding.top,
                rect.right.toInt() + padding.right,
                (rect.bottom.toInt() + rect.height() / 1.2f).toInt())
        paint.color = textColor
        canvas.drawText(text, start, end, x, y.toFloat(), paint)
        drawable.draw(canvas)
    }

    override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int = paint.measureText(text, start, end).roundToInt()

    private fun measureText(paint: Paint, text: CharSequence, start: Int, end: Int): Float = paint.measureText(text, start, end)
}

使用

使用这个DrawableSpan。 模拟了一个使用场景。

final String text1 = "RustFisher:\nHow to underline text in TextView with some different color than that of text?";
SpannableStringBuilder ssb = new SpannableStringBuilder(text1);
for (int i = 0; i < text1.length(); i += 6) {
    ssb.setSpan(new DrawableSpan(getResources().getDrawable(R.drawable.drawable_tv_underline)), i, Math.min(i + 4, text1.length()), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mBinding.tv1.setText(ssb);

SpannableStringBuilder ssb2 = new SpannableStringBuilder(text1);
for (int i = 0; i < text1.length(); i += 6) {
    ssb2.setSpan(new DrawableSpan(getResources().getDrawable(R.drawable.drawable_tv_underline2)), i, Math.min(i + 4, text1.length()), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mBinding.tv2.setText(ssb2);

运行效果

span1

可以看出,有下划线的部分,文字颜色也被修改了。

多个文字颜色与下划线

前面我们实现了下划线效果。从上面的效果图我们看到下划线和文字颜色都可以修改。 那么我们可以根据这个特点来增加文字变色效果。

如果只是修改文字颜色,我们也可以使用ForegroundColorSpan来实现。 这里我们实现的是从头开始的变色效果。

实现span类

draw方法里,我们需要处理一下变色终点下标foregroundEndIndex与文本结尾end之间的关系。

import android.graphics.*
import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan
import kotlin.math.roundToInt

class UnderlineAndForegroundSpan(private val drawable: Drawable) : ReplacementSpan() {
    private val padding: Rect = Rect()
    var textColor = Color.parseColor("#202020")
    var foregroundColor = Color.parseColor("#F8872E")
    var foregroundEndIndex = 0

    init {
        drawable.getPadding(padding)
    }

    override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
        val rect = RectF(x, top.toFloat(), x + measureText(paint, text, start, end), bottom.toFloat())
        drawable.setBounds(rect.left.toInt() - padding.left,
                rect.top.toInt() - padding.top,
                rect.right.toInt() + padding.right,
                (rect.bottom.toInt() + rect.height() / 1.2f).toInt())
        if (foregroundEndIndex <= 0) {
            paint.color = textColor
            canvas.drawText(text, start, end, x, y.toFloat(), paint)
        } else {
            when {
                end <= foregroundEndIndex -> {
                    paint.color = foregroundColor
                    canvas.drawText(text, start, end, x, y.toFloat(), paint)
                }
                start < foregroundEndIndex -> {
                    // 把文字分开2段画
                    paint.color = foregroundColor
                    canvas.drawText(text, start, foregroundEndIndex, x, y.toFloat(), paint)

                    paint.color = textColor
                    val drawInPart = text.subSequence(start, foregroundEndIndex)
                    canvas.drawText(text, foregroundEndIndex, end, x + measureText(paint, drawInPart, 0, drawInPart.length), y.toFloat(), paint)
                }
                else -> {
                    paint.color = textColor
                    canvas.drawText(text, start, end, x, y.toFloat(), paint)
                }
            }
        }

        drawable.draw(canvas)
    }

    override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int = paint.measureText(text, start, end).roundToInt()

    private fun measureText(paint: Paint, text: CharSequence, start: Int, end: Int): Float = paint.measureText(text, start, end)
}

使用与运行效果

与前面类似,模拟一个使用场景。

单独使用

final String text = "RustFisher:\nHow to underline text in TextView with some different color than that of text?";
SpannableStringBuilder ssb = new SpannableStringBuilder(text);
for (int i = 0; i < text.length(); i += 6) {
    UnderlineAndForegroundSpan span = new UnderlineAndForegroundSpan(getResources().getDrawable(R.drawable.drawable_tv_underline));
    span.setForegroundEndIndex(text.length() / 2);
    ssb.setSpan(span, i, Math.min(i + 4, text.length()), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mBinding.tv3.setText(ssb);

underline-foreground

结合ForegroundColorSpan

先使用ForegroundColorSpan来设定颜色,再把下划线效果加进去。

SpannableStringBuilder ssb2 = new SpannableStringBuilder(text);
final int foregroundColor = Color.RED;
ssb2.setSpan(new ForegroundColorSpan(foregroundColor), 0, text.length() / 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
for (int i = 0; i < text.length(); i += 6) {
    UnderlineAndForegroundSpan span = new UnderlineAndForegroundSpan(getResources().getDrawable(R.drawable.drawable_tv_underline));
    span.setForegroundEndIndex(text.length() / 2);
    span.setForegroundColor(foregroundColor);
    ssb2.setSpan(span, i, Math.min(i + 4, text.length()), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mBinding.tv4.setText(ssb2);
会覆盖掉前面设置的ForegroundColorSpan

underline-foreground

参考

  • https://stackoverflow.com/questions/19046614/how-to-underline-text-in-textview-with-some-different-color-than-that-of-text

作者: RustFisher
联系: rf.cs@foxmail.com
博客: rustfisher.com | RustFisher cnblog
示例: AndroidTutorial Gitee, Tutorial Github
链接: https://www.an.rustfisher.com/android/text/style/use-replacementspan/
一家之言,仅当抛砖引玉。如有错漏,还请指出。