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

效果如下图所示:

参考

相关推荐
江上清风山间明月2 小时前
Flutter DragTarget拖拽控件详解
android·flutter·ios·拖拽·dragtarget
debug_cat5 小时前
AndroidStudio Ladybug中编译完成apk之后定制名字kts复制到指定目录
android·android studio
编程洪同学9 小时前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端
氤氲息11 小时前
Android 底部tab,使用recycleview实现
android
Clockwiseee12 小时前
PHP之伪协议
android·开发语言·php
小林爱12 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio
小何开发13 小时前
Android Studio 安装教程
android·ide·android studio
开发者阿伟13 小时前
Android Jetpack LiveData源码解析
android·android jetpack
weixin_4381509913 小时前
广州大彩串口屏安卓/linux触摸屏四路CVBS输入实现同时显示!
android·单片机
CheungChunChiu14 小时前
Android10 rk3399 以太网接入流程分析
android·framework·以太网·eth·net·netd