目标
通过 ColorTrackTextView 来展示一种文字进度条效果,其中一部分文本以原始颜色显示,另一部分文本以目标颜色显示,随着进度的变化,逐步改变文字颜色的展示。
最终效果如下:

1. 结构分析
- 文本显示:该控件将展示文本内容,文本的颜色随着进度的变化而变化。
- 进度条效果:文本的颜色变化会基于一个进度值(0f 到 1f),从一个颜色(原始颜色)逐渐过渡到另一个颜色(目标颜色)。
- 支持方向:文本颜色的过渡可以是从左到右,也可以是从右到左。
2. 实现思路
- 初始化画笔:我们需要两个画笔,分别绘制未变化的颜色和变化后的颜色。
- 计算绘制位置:根据进度计算文本的变化区间和显示位置。
- 剪切区域:使用 canvas.clipRect() 来控制不同进度下文本显示的区域。
- 动态更新进度:通过设置进度值并调用 invalidate() 来刷新视图,使文本颜色过渡。
3. 关键技术点解析
我们用到了 Canvas 类的几个重要方法,特别是 clipRect()、save() 和 restore()。这些方法对于自定义绘制文本的效果至关重要。
自定义 TextView:通过继承 AppCompatTextView 来创建自定义控件。利用系统原有的属性,可以省去一些步骤
canvas.clipRect()
功能
canvas.clipRect() 方法用于限制 Canvas 上绘制内容的区域,即设置一个矩形区域作为画布的可绘制区域,只有在这个区域内的绘制操作才会被执行,超出该区域的部分会被裁剪掉。
因为我们要达到的目标是一个文字两种颜色,两种颜色需要分别绘制,因为不可能只绘制部分字体,这里使用 canvas.clipRect() 的目的就是把裁剪区域外的字体扔掉。也可以理解为 clipRect 就是一个剪刀。
方法
public void clipRect(int left, int top, int right, int bottom);
public void clipRect(Rect rect);
public void clipRect(float left, float top, float right, float bottom);
接受一个 Rect 对象 或四个坐标,直接定义裁剪矩形的区域。
canvas.save()
canvas.save() 会将当前的画布状态(包括变换矩阵、剪裁区域、画笔、绘制属性等)保存到栈中。这样,之后对画布的操作就不会影响到原本的画布状态,保证每次的绘制操作都是局部的。相当于记录个快照。
方法
public int save();
public int save(int saveFlags);
保存当前画布的状态。带有标志位参数,可以指定保存哪些部分的状态,例如是否保存剪切区域、变换矩阵等。常用的标志位是 Canvas.ALL_SAVE_FLAG(保存所有状态)。
canvas.restore()
canvas.restore() 方法用于恢复上次保存的画布状态。你可以在绘制过程中做一些修改(如变换矩阵、裁剪区域等),然后恢复到之前的状态,继续进行绘制操作。
方法
public void restore();
通过 save() 方法保存了画布的状态之后,可以在需要时通过 restore() 恢复画布的状态,以便不影响后续绘制。
在 onDraw() 方法中,我们使用 save() 和 restore() 来保护每次对 Canvas 状态的修改不相互干扰。例如,每次使用 clipRect() 时,都先保存当前的画布状态,然后修改裁剪区域,完成绘制后,再调用 restore() 恢复原始状态,确保其他绘制操作不受影响。
save() 和 restore() 两个方法的作用可能在理解起来有点困难,虽然 Canvas 本身是一个单一的对象,但通过 save() 和 restore(),可以在 同一个 Canvas 上创建多个“画布状态”。只可意会不可言传,MD,好玄幻。
这两个方法可以理解成是 “记住”和“恢复” 画布状态的工具。
4. 定义自定义属性
定义两种颜色:文本的原始颜色,和改变的颜色。这些属性可以通过 res/values/attrs.xml 文件来定义:
<declare-styleable name="ColorTrackTextView">
<attr name="originColor" format="color"/>
<attr name="changeColor" format="color"/>
</declare-styleable>
5. 初始化视图元素
初始化画笔和两种颜色。
private void initPaint(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ColorTrackTextView);
// 获取用户设置的颜色,若未设置则使用默认颜色
int initialColor = typedArray.getColor(R.styleable.ColorTrackTextView_originColor, getTextColors().getDefaultColor());
int changedColor = typedArray.getColor(R.styleable.ColorTrackTextView_changeColor, getTextColors().getDefaultColor());
// 创建两个画笔,一个用于初始颜色,一个用于变化后的颜色
mInitialPaint = createPaint(initialColor);
mChangedPaint = createPaint(changedColor);
typedArray.recycle();
}
6. 绘制视图内容(onDraw)
文字变色关键一点就是确定分割线的位置,如下图所示的 middle 坐标。

middle 最小为0,最大为getWidth(),由此我们可以动态计算middle的坐标(mProgress 为 当前进度值,范围为 0.0 到 1.0):
int middle = (int) (mProgress * width); // 当前进度对应的分割位置
这时我们就可以动态获取分割线左右两个矩形的坐标了,假设 middle值为0.5,此时画笔1,将画出完整的文字变色四个字,通过 canvas.clipRect() 剪切下 分割线左边矩形,此时右边内容将不会显示!此时的画布状态为大矩形的一半,接着我们 调用canvas.restore() 恢复画布状态,又可以操作整个大矩形画布了,然后再用画笔2,再画出完整的文字变色四个字,同理,剪切掉左边矩形,只显示右边矩形内容,最后就是显示一个文本两种颜色了。
核心代码如下:
if (mDirection == Direction.LEFT_TO_RIGHT) {
// 从左到右:左边是改变颜色,右边是初始颜色
drawText(canvas, mChangedPaint, x, baseline, 0, middle); // 绘制变色部分
drawText(canvas, mInitialPaint, x, baseline, middle, width); // 绘制不变色部分
} else if (mDirection == Direction.RIGHT_TO_LEFT) {
// 从右到左:右边是改变颜色,左边是初始颜色
drawText(canvas, mChangedPaint, x, baseline, width - middle, width); // 绘制变色部分
drawText(canvas, mInitialPaint, x, baseline, 0, width - middle); // 绘制不变色部分
}
//..
private void drawText(Canvas canvas, Paint paint, int x, int y, int left, int right) {
canvas.save(); // 保存画布状态
canvas.clipRect(left, 0, right, getHeight()); // 裁剪画布区域
canvas.drawText(getText().toString(), x, y, paint); // 绘制文本
canvas.restore(); // 恢复画布状态
}
这里有一点比较绕的地方,从左到右是从分割线是从左边开始(0.0 到 1.0),从右至左是分割线从右边开始(1.0 到 0.0),红色逐渐占领黑色领土!这一点能理解,就能看懂代码了。
7. 完整代码
public class ColorTrackTextView extends androidx.appcompat.widget.AppCompatTextView {
// 初始颜色的画笔
private Paint mInitialPaint;
// 改变颜色的画笔
private Paint mChangedPaint;
// 当前进度值,范围为 0.0 到 1.0
private float mProgress = 0f;
// 动画方向,默认为从左到右
private Direction mDirection = Direction.LEFT_TO_RIGHT;
public enum Direction {
LEFT_TO_RIGHT, RIGHT_TO_LEFT
}
public ColorTrackTextView(Context context) {
this(context, null);
}
public ColorTrackTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ColorTrackTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint(context, attrs);
}
// 初始化画笔对象
private void initPaint(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ColorTrackTextView);
// 获取用户设置的颜色,若未设置则使用默认颜色
int initialColor = typedArray.getColor(R.styleable.ColorTrackTextView_originColor, getTextColors().getDefaultColor());
int changedColor = typedArray.getColor(R.styleable.ColorTrackTextView_changeColor, getTextColors().getDefaultColor());
// 创建两个画笔,一个用于初始颜色,一个用于变化后的颜色
mInitialPaint = createPaint(initialColor);
mChangedPaint = createPaint(changedColor);
typedArray.recycle();
}
// 根据颜色创建画笔对象
private Paint createPaint(int color) {
Paint paint = new Paint();
paint.setColor(color);
paint.setAntiAlias(true); // 开启抗锯齿
paint.setDither(true); // 开启抖动效果
paint.setTextSize(getTextSize()); // 设置文本大小
return paint;
}
@Override
protected void onDraw(Canvas canvas) {
// 获取文本内容和画布的宽高
String text = getText().toString();
int width = getWidth();
int height = getHeight();
// 获取文本的边界和基线位置
Rect textBounds = new Rect();
mInitialPaint.getTextBounds(text, 0, text.length(), textBounds);
int textWidth = textBounds.width();
int x = width / 2 - textWidth / 2; // 计算文本绘制的起始 X 坐标
Paint.FontMetrics fontMetrics = mInitialPaint.getFontMetrics();
int baseline = height / 2 + (int) ((fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom); // 计算基线 Y 坐标
// 根据当前进度和方向绘制文本
int middle = (int) (mProgress * width); // 当前进度对应的分割位置
if (mDirection == Direction.LEFT_TO_RIGHT) {
// 从左到右:左边是改变颜色,右边是初始颜色
drawText(canvas, mChangedPaint, x, baseline, 0, middle); // 绘制不变色部分
drawText(canvas, mInitialPaint, x, baseline, middle, width); // 绘制变色部分
} else if (mDirection == Direction.RIGHT_TO_LEFT) {
// 从右到左:右边是改变颜色,左边是初始颜色
drawText(canvas, mChangedPaint, x, baseline, width - middle, width); // 绘制不变色部分
drawText(canvas, mInitialPaint, x, baseline, 0, width - middle); // 绘制变色部分
}
}
// 绘制指定区域的文本
private void drawText(Canvas canvas, Paint paint, int x, int y, int left, int right) {
canvas.save(); // 保存画布状态
canvas.clipRect(left, 0, right, getHeight()); // 裁剪画布区域
canvas.drawText(getText().toString(), x, y, paint); // 绘制文本
canvas.restore(); // 恢复画布状态
}
// 设置文字的变化方向
public void setDirection(Direction direction) {
mDirection = direction;
}
// 设置当前进度并刷新界面
public void setProgress(float progress) {
mProgress = progress;
invalidate(); // 重绘视图
}
}
8. 使用
xml:
<com.xaye.example.ColorTrackTextView
android:id="@+id/tv_track"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:originColor="@color/black"
app:changeColor="@color/red"
android:text="文字变色"
android:textSize="30dp"
android:layout_centerHorizontal="true"/>
activity:
private fun leftToRight(){
val trackTextView = mBind.tvTrack
trackTextView.setDirection(ColorTrackTextView.Direction.LEFT_TO_RIGHT)
val valueAnimator = ValueAnimator.ofFloat(0f, 1f);
valueAnimator.setDuration(2000)
valueAnimator.addUpdateListener { animation ->
val value = animation.animatedValue as Float
trackTextView.setProgress(value)
}
valueAnimator.start()
}
private fun rightToLeft(){
val trackTextView = mBind.tvTrack
trackTextView.setDirection(ColorTrackTextView.Direction.RIGHT_TO_LEFT)
val valueAnimator = ValueAnimator.ofFloat(0f, 1f);
valueAnimator.setDuration(2000)
valueAnimator.addUpdateListener { animation ->
val value = animation.animatedValue as Float
trackTextView.setProgress(value)
}
valueAnimator.start()
}
9. 最后
本篇文章只是讲解文字变色的原理,它的应用场景有很多,一般和滑动组件配合使用(0.0 - 1.0的变化),下篇文章介绍具体的应用场景。
另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai
2137

被折叠的 条评论
为什么被折叠?



