Android 实现微信读书划线的效果

最近遇到过一个实现类似微信读书的划线效果的需求。如下图所示,可以看到,微信读书划线支持涂抹、直线以及波浪线三种效果。

对于涂抹效果可以使用 BackgroundColorSpan实现,代码示例如下:

kotlin 复制代码
val content = SpannableStringBuilder(textView.text)  
content.setSpan(BackgroundColorSpan(Color.RED), 0, content.length / 2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)  
textView.text = content

效果如下图所示:

对于直线划线的效果则可以通过 UnderlineSpan 来实现,代码如下所示:

kotlin 复制代码
val content = SpannableStringBuilder(textView.text)  
content.setSpan(UnderlineSpan(), 0, content.length / 2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)  
textView.text = content

效果如下图所示:

如果你需要设置下划线的颜色和粗细,则需要自定义 UnderlineSpan,代码示例如下:

kotlin 复制代码
class CustomUnderLine(val color: Int, val underlineThickness: Float): UnderlineSpan() {

    @RequiresApi(Build.VERSION_CODES.Q)
    override fun updateDrawState(ds: TextPaint) {
        ds.underlineColor = color // 下划线的颜色
        ds.underlineThickness = underlineThickness // 下划线的粗细
        super.updateDrawState(ds)
    }

}

效果如下所示:

但是对于绘制波浪线,Android 没有没有提供直接的接口来实现。这时我们可以通过 LineBackgroundSpan 来间接实现波浪线的效果。

kotlin 复制代码
class Standard implements LineBackgroundSpan, ParcelableSpan {
    // 存储背景颜色的变量
    private final int mColor;

    // 构造方法,接受一个颜色整数值作为参数,用于定义背景颜色
    public Standard(@ColorInt int color) {
        mColor = color;
    }

    // 从包裹中创建 LineBackgroundSpan.Standard 对象的构造方法
    public Standard(@NonNull Parcel src) {
        mColor = src.readInt();
    }

    @Override
    public int getSpanTypeId() {
        return getSpanTypeIdInternal();
    }

    /** @hide */
    @Override
    public int getSpanTypeIdInternal() {
        return TextUtils.LINE_BACKGROUND_SPAN;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        writeToParcelInternal(dest, flags);
    }

    /** @hide */
    @Override
    public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
        dest.writeInt(mColor);
    }

    /**
     * 获取该 span 的颜色
     * @return 颜色整数值
     */
    @ColorInt
    public final int getColor() {
        return mColor;
    }

    // 绘制背景的方法,在画布上绘制指定颜色的矩形作为行背景
    // left:该行相对于输入画布的左边界位置,以像素为单位。
    // right:该行相对于输入画布的右边界位置,以像素为单位。
    // top:该行相对于输入画布的上边界位置,以像素为单位。
    // baseline:该行文本的基线相对于输入画布的位置,以像素为单位。
    // bottom:该行相对于输入画布的下边界位置,以像素为单位。
    // text:当前的文本内容。
    // start:该行文本在整个文本中的起始字符索引。
    // end:该行文本在整个文本中的结束字符索引。
    // lineNumber:在当前文本布局中的行号。
    @Override
    public void drawBackground(@NonNull Canvas canvas, @NonNull Paint paint,
                               @Px int left, @Px int right,
                               @Px int top, @Px int baseline, @Px int bottom,
                               @NonNull CharSequence text, 
                               int start, 
                               int end,
                               int lineNumber) {
       
        final int originColor = paint.getColor();
        paint.setColor(mColor);
        canvas.drawRect(left, top, right, bottom, paint);
        paint.setColor(originColor);
    }
}

如上的源码所示,LineBackgroundSpan 主要用于改变文本中的行的背景。LineBackgroundSpan 有一个实现LineBackgroundSpan.Standard,作用和 BackgroundColorSpan 都是改变文本的背景颜色,区别是LineBackgroundSpan 主要是用于改变文本中某一行或者某几行的背景。它在绘制背景时,考虑的是行的位置信息,如行的左右边界(leftright)、顶部和底部位置(topbottom)。简单说就是 LineBackgroundSpan 提供了更多行的信息,方便我们做更细致的处理。

代码示例如下:

kotlin 复制代码
class WaveLineBackgroundSpan(val waveColor: Int) : LineBackgroundSpan {

    // 创建画笔用于绘制波浪线,初始化时设置颜色、样式和线宽
    val wavePaint = Paint().apply {
        color = waveColor
        style = Paint.Style.STROKE
        strokeWidth = 6f
    }

    override fun drawBackground(
        canvas: Canvas, paint: Paint,
        @Px left: Int, @Px right: Int,
        @Px top: Int, @Px baseline: Int, @Px bottom: Int,
        text: CharSequence, start: Int, end: Int,
        lineNumber: Int
    ) {
        // 定义波浪线的振幅和波长,振幅决定波浪的高度,波长决定波浪的周期
        val amplitude = 5
        val wavelength = 15

        // 获取要绘制波浪线的文本宽度
        val width = paint.measureText(text.subSequence(start, end).toString()).toInt()

        // 遍历文本宽度范围内的每个点,计算并绘制波浪线上的点
        for (x in left until (left + width)) {
            // 根据正弦函数计算每个点的 y 坐标,实现波浪效果
            val y = (amplitude * Math.sin((x.toFloat() / wavelength).toDouble())).toInt()
            // 在画布上绘制波浪线上的点,确保 x 坐标不超过右边界
            canvas.drawPoint(x.toFloat().coerceAtMost(right.toFloat()), (bottom + y).toFloat(), wavePaint)
        }
    }
}

效果如下图所示:

参考

相关推荐
Winston Wood5 分钟前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-3 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen5 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年13 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿15 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神16 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛16 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法17 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter18 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快19 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android