最近遇到过一个实现类似微信读书的划线效果的需求。如下图所示,可以看到,微信读书划线支持涂抹、直线以及波浪线三种效果。
对于涂抹效果可以使用 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)
}
}
}
效果如下图所示: