Android使用ReplacementSpan创建标签样式

对于富文本展示,比如字体变化、背景色、前景色等,一般都需要使用SpannableString配合各种系统自带的Span(比如AbsoluteSizeSpan、BackgroundColorSpan等等)来实现。

创建ReplacementSpan的子类,可以自定义实现更丰富的样式,本文通过一个常见小需求来介绍ReplacementSpan的使用方法

** 文章需要读者对Canvas使用有基础了解即可

参考文档:

Span | Android 开发者 | Android Developers ReplacementSpan | Android Developers

利用SpannableString富文本方式设置圆角标签背景

Android Textview获取文本高度及drawable 居中

自定义 View - 扔物线

需求

下面是视觉稿,在文本前面展示一个标签,标签字体比文本小,颜色为白色,背景为带有圆角的矩形。

(可以直接跳过中间部分,使用最后一页中的代码实现)

1.修改标签大小

如果只是前两个字为小字体会很容易实现:

直接使用AbsoluteSizeSpan,修改字体大小

ini 复制代码
val spannable1 = SpannableString("散件" + goodsName)
        spannable1.setSpan(
            AbsoluteSizeSpan(20), 0, 2,
            Spanned.SPAN_INCLUSIVE_EXCLUSIVE
        )
binding.tvGoodsName1.text = spannable1        

2.绘制背景色和文本

ReplacementSpan是一个抽象类,有两个抽象方法:getSize 返回span的宽度,draw绘制span。

draw方法的参数比较多,可以看下,其中x、top、bottom、y是我们绘制过程中最重要的坐标参数:

less 复制代码
/**
     * Draws the span into the canvas.
     *
     * @param canvas Canvas into which the span should be rendered.
     * @param text Current text.
     * @param start Start character index for span.  span文本其实下标
     * @param end End character index for span.      span文本结束下标
     * @param x Edge of the replacement closest to the leading margin.  span边缘接近leading margin处,可以认为是span最左边坐标。
     * @param top Top of the line.                                      span顶部
     * @param y Baseline.                                               span绘制文本的基线
     * @param bottom Bottom of the line.                                span底部
     * @param paint Paint instance.
     */
public abstract void draw(@NonNull Canvas canvas, CharSequence text,
                          @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x,
                          int top, int y, int bottom, @NonNull Paint paint);

先实现一个自定义ReplacementSpan,绘制圆角矩形和文本(先不管他们的位置是否居中):

kotlin 复制代码
class RoundBackgroundColorSpan(
    private val bgColor: Int,
    private val textColor: Int,
    private val padding: Int,
    private val radio: Int,
) :
ReplacementSpan() {
    override fun getSize(
        paint: Paint,
        text: CharSequence,
        start: Int,
        end: Int,
        fm: Paint.FontMetricsInt?
            ): Int {
        // 设置宽度 为文字左右添加 padding
        return paint.measureText(text, start, end).toInt() + (padding * 2)
    }

    override fun draw(
        canvas: Canvas,
        text: CharSequence,
        start: Int,
        end: Int,
        x: Float,
        top: Int,     // span顶部
        y: Int,
        bottom: Int,  // span底部
        paint: Paint
    ) {
        // 保存原始颜色
        val originalColor = paint.color

        // 修改背景色
        paint.color = bgColor

        // 绘制圆角矩形背景
        canvas.drawRoundRect(
            RectF(
                x,
                top.toFloat(),
                x + (paint.measureText(text, start, end).toInt() + (padding * 2)),
                bottom.toFloat()
            ),
            radio.toFloat(),
            radio.toFloat(),
            paint
        )
        paint.color = textColor
        // 画文字,增加 padding
        canvas.drawText(text, start, end, x + padding, y.toFloat(), paint)
        // 将paint颜色复原
        paint.color = originalColor
    }
}

解释一下代码的内容:

RoundBackgroundColorSpan构造方法中有四个参数:

bgColor - 背景色

textColor - 文本颜色

padding - 文本的边距(使标签文本左右两边留一定留白间距)

radio - 背景矩圆角矩形的圆角大小

paint.measureText 方法可以测量文本宽度,加上两个padding就是文字整体占用的宽度

canvas.drawRoundRect 方法用来绘制圆角矩形

canvas.drawText 绘制文本

将我们创建的span设置应用:

less 复制代码
val spannable1 = SpannableString("散件" + goodsName)
spannable1.setSpan(
    AbsoluteSizeSpan(20), 0, 2,
    Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
spannable1.setSpan(
    RoundBackgroundColorSpan(
        Color.parseColor("#108EE9"),
        Color.WHITE,
        padding,
        radio
    ),
    0,
    2,
    Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
binding.tvGoodsName1.text = spannable1

跑代码看看效果:

效果有了,但是还差点意思:背景矩形太高,标签文字也没有居中。

【如果设置标签文字和后面的文字大小一样(不使用AbsoluteSizeSpan),效果如下图:

3.使标签文字居中

绘制文本的方法是canvas.drawText,这是这部分的重点

下面是drawText的源码

less 复制代码
/**
     * Draw the specified range of text, specified by start/end, with its origin at (x,y), in the
     * specified Paint. The origin is interpreted based on the Align setting in the Paint.
     *
     * @param text The text to be drawn
     * @param start The index of the first character in text to draw
     * @param end (end - 1) is the index of the last character in text to draw
     * @param x The x-coordinate of origin for where to draw the text
     * @param y The y-coordinate of origin for where to draw the text
     * @param paint The paint used for the text (e.g. color, size, style)
     */
public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
                     @NonNull Paint paint) {
    super.drawText(text, start, end, x, y, paint);
}

绘制文本时,传入坐标x,y。这个坐标并不是文本的左上角或者右下角,而是文本的基线起始位置。下图中左侧标注了基线(baseline)的位置。

(图取自文章Android Textview获取文本高度及drawable 居中

Paint有个公共方法叫做getFontMetrics,这个方法可以获得字体相关的一个对象FontMetrics,这个对象中有上图字体的关键数据:

arduino 复制代码
/**
     * Class that describes the various metrics for a font at a given text size.
     * Remember, Y values increase going down, so those values will be positive,
     * and values that measure distances going up will be negative. This class
     * is returned by getFontMetrics().
     */
public static class FontMetrics {
    /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
    public float   top;
    /**
         * The recommended distance above the baseline for singled spaced text.
         */
    public float   ascent;
    /**
         * The recommended distance below the baseline for singled spaced text.
         */
    public float   descent;
    /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
    public float   bottom;
    /**
         * The recommended additional space to add between lines of text.
         */
    public float   leading;
}

FontMetrics 类描述了给定文本大小的字体的各种度量标准。该类由 getFontMetrics() 方法返回。Y 值随着向下而增加,这些描述的值以基线baseline为基点,即baseline的值为0来进行描述,top 和 ascent 的值为负值,descent 和 bottom 的值为正值。比如,baseline到字体顶部的距离就是 -top(top为负值,那么-top就是正值)。

通过fontMetrics获取文本高度(参考Android Textview获取文本高度及drawable 居中

ini 复制代码
val fm = paint.fontMetrics
val fontHeight = fm.top - fm.bottom

有了以上数据,我们就可以来计算,baseline是是多少时,可以让文字居中展示。

参考下图开始计算baseline:

下图标注了要计算的baseline的位置,外边框矩形为原字体高度区域。

1.计算center坐标:top和bottom为draw方法的参数,那么center = (top + bottom) / 2。

2.计算标签的高度:tagFontHeight = fm.bottom - fm.top(fm为paint获取到的fontMetrics)

3.计算tag top 坐标:tagTop = center - tagFontHeight/2 (Android Canvas坐标系是Y轴向下)

4.计算baseline坐标:baseline = tagTop + (-fm.top) = tagTop - fm.top (fm.top为负值)

继续计算标签背后有颜色的圆角矩形的顶部和底部坐标

5.计算标签背后圆角矩形的top = tag top

6.计算标签背后圆角矩形的bottom = tag top + tagFontHeight

如上计算代码如下:

kotlin 复制代码
class RoundBackgroundColorSpan(
    private val bgColor: Int,
    private val textColor: Int,
    private val padding: Int,
    private val radio: Int,
) :
ReplacementSpan() {
    override fun getSize(
        paint: Paint,
        text: CharSequence,
        start: Int,
        end: Int,
        fm: Paint.FontMetricsInt?
            ): Int {
        //设置宽度为文字宽度加 padding
        return paint.measureText(text, start, end).toInt() + (padding * 2)
    }

    override fun draw(
        canvas: Canvas,
        text: CharSequence,
        start: Int,
        end: Int,
        x: Float,
        top: Int,
        y: Int,
        bottom: Int,
        paint: Paint
    ) {
        val originalColor = paint.color
        val fm = paint.fontMetrics


        paint.color = bgColor

        val center = (top + bottom) / 2
        val tagFontHeight = fm.bottom - fm.top
        val tagTop = center - tagFontHeight / 2
        val baseline = tagTop - fm.top

        //画圆角矩形背景
        canvas.drawRoundRect(
            RectF(
                x,
                tagTop,
                x + (paint.measureText(text, start, end).toInt() + (padding * 2)),
                tagTop + tagFontHeight
            ),
            radio.toFloat(),
            radio.toFloat(),
            paint
        )
        paint.color = textColor
        //画文字,两边各增加8dp
        canvas.drawText(text, start, end, x + padding, baseline, paint)
        //将paint复原
        paint.color = originalColor
    }
}

最后效果就出来啦:

总结

不论是ReplacementSpan还是自定义View,Android已经封装好我们重写draw方法就可以进行绘制实现。

我们在绘制前,需要对canvas和paint的功能有个大概了解。

绘制步骤

1.思路捋清楚(要画哪些东西,怎么画)

2.查canvas的绘制的api及用到的参数(drawText、drawXXX等等)

3.用已有数据计算坐标

相关推荐
GEEKVIP2 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20054 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6894 小时前
Android广播
android·java·开发语言
与衫5 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了11 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵12 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru17 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng18 小时前
android 原生加载pdf
android·pdf
hhzz18 小时前
Linux Shell编程快速入门以及案例(Linux一键批量启动、停止、重启Jar包Shell脚本)
android·linux·jar
火红的小辣椒19 小时前
XSS基础
android·web安全