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)
        }
    }
}

效果如下图所示:

参考

相关推荐
风浅月明6 小时前
[Android]如何判断当前APP是Debug还是Release环境?
android
freflying11196 小时前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
私人珍藏库8 小时前
[Android] APK提取器(1.3.7)版本
android
m0_748232648 小时前
mysql的主从配置
android·mysql·adb
秋长愁8 小时前
Android监听应用前台的实现方案解析
android
胖虎19 小时前
2025 新版Android Studio创建Java语言项目
android·java·android studio·创建java项目
JabamiLight10 小时前
Lineageos 22.1(Android 15)Launcer简单调整初始化配置
android·android 15·lineageos 22.1·launcer
敲代码的鱼哇12 小时前
设备唯一ID获取,支持安卓/iOS/鸿蒙Next(uni-device-id)UTS插件
android·ios·uniapp·harmonyos
太空漫步1113 小时前
android滑动看新闻
android
KdanMin13 小时前
“让App玩捉迷藏:Android教育平板的‘隐身术’开发实录”
android