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 主要是用于改变文本中某一行或者某几行的背景。它在绘制背景时,考虑的是行的位置信息,如行的左右边界(left和right)、顶部和底部位置(top和bottom)。简单说就是 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)
        }
    }
}

效果如下图所示:

参考

相关推荐
五彩小白4 小时前
自回归和上下文
android
事圆则缓6 小时前
Android 多渠道打包实战:Product Flavors、签名、AAB 与发包校验
android
树下困觉眯8 小时前
View绘制流程学习--获取WindowManager的三种方式
android·系统架构·view design
vilya9 小时前
我怎么给手机 GUI Agent 做双通道感知:无障碍树为主,投屏像素兜底
android·人工智能
李子红了时10 小时前
阿狸舞台APP(安卓端手机EncFS解密+压缩包解压+视频播放)
android·音视频
事圆则缓10 小时前
Android 使用 Jenkins 实现 CI/CD,并用 SonarQube 建立质量门禁
android·ci/cd·jenkins
其实防守也摸鱼10 小时前
DeepSeek Harness 开源贡献手记:从 Issue 到 Merge 的完整旅程
android·数据库·学习·ai·oracle·自动化
恋猫de小郭10 小时前
Android CLI 支持 AI Agent 通过 Device Streaming 调试云真机
android·前端·flutter
素师良码1 天前
第5篇:显示驱动必备调试工具浅析
android
维克兜率天1 天前
【维克】配对交易的季节性:哪些品种适合长拿?
android·开发语言·笔记·python·算法·kotlin·量化