重学 Android 自定义 View 系列(四):文字变色

目标

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

最终效果如下:
在这里插入图片描述

1. 结构分析


  • 文本显示:该控件将展示文本内容,文本的颜色随着进度的变化而变化。
  • 进度条效果:文本的颜色变化会基于一个进度值(0f 到 1f),从一个颜色(原始颜色)逐渐过渡到另一个颜色(目标颜色)。
  • 支持方向:文本颜色的过渡可以是从左到右,也可以是从右到左。

2. 实现思路


  1. 初始化画笔:我们需要两个画笔,分别绘制未变化的颜色和变化后的颜色。
  2. 计算绘制位置:根据进度计算文本的变化区间和显示位置。
  3. 剪切区域:使用 canvas.clipRect() 来控制不同进度下文本显示的区域。
  4. 动态更新进度:通过设置进度值并调用 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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值