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

效果如下图所示:

参考

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95272 天前
Andorid Google 登录接入文档
android
黄林晴2 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android