自定义View_05(小试牛刀)字体变色
照旧, 先上图
文字变色
这是一个字体变色的 Demo, 主要还是练习 onDraw
方法.
实现思路
- 两个画笔,一个原色画笔, 一个变色画笔
- 一个和文本宽度相关的进度值
- 先用原色画笔绘制出文本
- 不断改变进度值, 原色画笔不断向右进行裁剪,把左边的裁剪掉,
- 原色画笔不断裁剪的同时, 变色画笔不断根据同一个进度值进行绘制
例如当前进度值是50%, 就是文本的一半, 就是在 "大牛之路," 这个后面.
原色画笔向右裁剪50%, 显示的就是 "从小牛开始"这几个字, 前面几个字被裁剪掉了.
然后变色画笔开始绘制, 从左向右绘制 50%, 就是 "大牛之路," 这几个字. 后面几个字被裁剪掉了.
组合起来,这样 "大牛之路," 与 "从小牛开始" 字体的颜色就会不相同了. 不断改变进度值,就可以实现上图效果.
运用到的知识点
- Canvas 的裁剪 (学习目标)
- 绘制文本基线的计算
正文:
1. 创建自定义属性文件 attrs.xml
自定义属性说明:
(因为我们的自定义控件是继承自 TextView, TextView 自身的属性已经够我们使用的了, 所以这里我们就定义两个属性就够了.)
MyTrackTextView: 我们自定义 View 的名字
originColor: 表示原色
changeColor: 表示改变的颜色
<resources>
<declare-styleable name="MyTrackTextView">
<attr name="originColor" format="color" />
<attr name="changeColor" format="color" />
</declare-styleable>
</resources>
2. 新建 MyTrackTextView.java 文件
建好文件后, 需要做以下几件事情
- 继承 TextView,
- 重写三个构造函数
- 重写 onDraw 方法.
- 获取我们上面定义的两个自定义属性.
- 初始化两个画笔(原色的和需要改变的颜色的)
@SuppressLint("AppCompatCustomView")
public class MyTrackTextView extends TextView {
private Paint mOriginPaint, mChangePaint;
public MyTrackTextView(Context context) {
this(context, null);
}
public MyTrackTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTrackTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 获取自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTrackTextView);
int originColor = typedArray.getColor(R.styleable.MyTrackTextView_originColor, getTextColors().getDefaultColor());
int changeColor = typedArray.getColor(R.styleable.MyTrackTextView_changeColor, getTextColors().getDefaultColor());
mOriginPaint = getPaintByColor(originColor);
mChangePaint = getPaintByColor(changeColor);
// 回收
typedArray.recycle();
}
private Paint getPaintByColor(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) {
super.onDraw(canvas);
}
}
OK, 准备工作完成.
3. 先绘制一个原色的文本
- 先在布局文件中引入我们的自定义控件, 并设置两个按钮, 添加点击事件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<org.zyq.MyTrackTextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:text="大牛之路,从小牛开始"
android:textSize="20sp"
app:changeColor="@color/colorPrimary"
app:originColor="@color/colorAccent" />
<Button
android:onClick="leftToRight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="从左向右移动" />
<Button
android:onClick="rightToRight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="从右向左移动" />
</LinearLayout>
- 开始在 onDraw 方法中绘制原色文本
@Override
protected void onDraw(Canvas canvas) {
//不要继承父类的.super.onDraw
//传入画布与画笔
drawText(canvas, mOriginPaint);
}
private void drawText(Canvas canvas, Paint paint) {
//获取字体的宽度,宽度的一半减去文字的一半,得到开始位置
String text = getText().toString();
Rect bounds = new Rect();
mOriginPaint.getTextBounds(text, 0, text.length(), bounds);
int x = getWidth() / 2 - bounds.width() / 2;
//基线
Paint.FontMetricsInt metricsInt = paint.getFontMetricsInt();
int dy = (metricsInt.bottom - metricsInt.top) / 2 - metricsInt.bottom;
int baseLine = getHeight() / 2 + dy;
//开始画
canvas.drawText(text, x, baseLine, paint);
}
运行效果如下
原色运行效果
4. 开始使用裁剪方法 canvas.clipRect()
canvas.clipRect(int left, int top, int right, int bottom)
这个方法大概意思就是说,使用矩形进行裁剪. 矩形以本地坐标表示
left: 区域的起始坐标
top: 区域的顶部坐标
right: 区域的右侧坐标
bottom: 区域的底部坐标
这4个坐标设置完后, 刚好构成了一个正方形/长方形, 保留这个区域内的内容, 裁剪区域外的内容, ......理解起来有点费劲. 其实就是裁剪的是这个区域外的内容. 直接理解为,这个区域内是要保留的,剩下的都裁剪掉
网上找的图
由left和top生成一个点,right和bottom生成一个点,然后取这2个点的交集就生成了蓝色区域(裁剪之后的图片),
我们先随便设置一个矩形, 看一下效果.
@Override
protected void onDraw(Canvas canvas) {
//不要继承父类的.super.onDraw
canvas.clipRect(getWidth() / 2, 0, getWidth(), getHeight());
//传入画布与画笔
drawText(canvas, mOriginPaint);
}
canvas.clipRect(getWidth() / 2, 0, getWidth(), getHeight());
保留的区域为:
起始位置: 文本宽度的一半
顶部位置: 0
截止位置: 文本的宽度
底部位置: 文本的高度距离
意思是保留的文本从一半到最后, 其余的裁剪掉.
效果如下:
文本中间到最后的裁剪
我这里是为了测试, 所以写了固定的值, 实际上起始位置的值和截止位置的值都是动态改变的. 我们需要设置一个变量来保存当前进度
private float mCurrentProgress = 0.5f;
那么我们改造一下 onDraw
和 drawText
方法,使其变得更通用,增加两个参数,起始位置和结束的位置
@Override
protected void onDraw(Canvas canvas) {
//不要继承父类的.super.onDraw
//根据进度把要移动的值算出来
int moveValue = (int) (mCurrentProgress * getWidth());
//传入画布, 画笔, 起始位置, 结束位置
drawText(canvas, mOriginPaint, moveValue, getWidth());
}
private void drawText(Canvas canvas, Paint paint, int start, int end) {
//裁剪区域为文本start位置到end位置之外的区域.
canvas.clipRect(start, 0, end, getHeight());
//绘制文本
.....
}
5. 开始绘制变色文本
@Override
protected void onDraw(Canvas canvas) {
//不要继承父类的.super.onDraw
//根据进度把要移动的值算出来
int moveValue = (int) (mCurrentProgress * getWidth());
//传入画布, 画笔, 起始位置, 结束位置, 绘制原色文本
drawText(canvas, mOriginPaint, moveValue, getWidth());
//传入画布, 画笔, 起始位置, 结束位置, 绘制变色文本
drawText(canvas, mChangePaint, 0, moveValue);
}
原色文本的裁剪区域是文本左边的一半,那么变色文本的裁剪区域就是文本右边的一半,
运行后,我们发现,并没有变色,是为什么呢.因为画了原色后,画布并没有释放, 所以变色的才会没有效果.
在 drawText
方法的开始和结尾需要加上
private void drawText(Canvas canvas, Paint paint, int start, int end) {
//保存画布
canvas.save();
//裁剪区域为文本start位置到end位置之外的区域.
...
//绘制文本
.....
//释放画布
canvas.restore();
}
save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。
restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
运行效果如下:
变色文本
那么剩下的就简单了, 我们只需要在外部不断的改变 mCurrentProgress
这个值, 就可以了. 先让控件动起来, 再考虑方向的问题.
6. 让控件动起来(从左至右)
- 在自定义控件内对
mCurrentProgress
属性添加set
方法, 可以让外部调用, 并且不断绘制. - 把
mCurrentProgress
值设置为 0.0f
private float mCurrentProgress = 0.0f;
public void setCurrentProgress(float currentProgress) {
this.mCurrentProgress = currentProgress;
invalidate();
}
- 在 mainActivity 中 leftToRight 点击事件中使用属性动画, 让控件动起来.
private MyTrackTextView myTrackTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myTrackTextView = findViewById(R.id.textview);
}
public void leftToRight(View view) {
ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0, 1);
valueAnimator.setDuration(2000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
myTrackTextView.setCurrentProgress(value);
}
});
valueAnimator.start();
}
怎么样, 到这一步,是不是已经动起来了? 下一步就是改变一下方向, 从右至左.
7.从右至左
我们需要在自定义控件中设置一个方向的属性, 来表示是从左至右,还是从右至左.,并设置它的set
方法
//不同方向,左,右, 默认从左至右
private Orientation mOrientation = Orientation.LEFT_TO_RIGHT;
public enum Orientation {
LEFT_TO_RIGHT,
RIGHT_TO_LEFT
}
public void setOrientation(Orientation orientation) {
this.mOrientation = orientation;
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
//不要继承父类的.super.onDraw
// 根据进度把要移动的值算出来
int moveValue = (int) (mCurrentProgress * getWidth());
//如果是从左向右
if (mOrientation == Orientation.LEFT_TO_RIGHT) {
//1.绘制变色的
drawText(canvas, mChangePaint, 0, moveValue);
//2.绘制原色的
drawText(canvas, mOriginPaint, moveValue, getWidth());
} else {
//1.绘制原色的
drawText(canvas, mOriginPaint, getWidth() - moveValue, getWidth());
//2.绘制变色的
drawText(canvas, mChangePaint, 0, getWidth() - moveValue);
}
}
从左向右,我们已经明白了.
从右向左,
原色: drawText(canvas, mOriginPaint, getWidth() - moveValue, getWidth());
变色: drawText(canvas, mChangePaint, 0, getWidth() - moveValue);
下面一张图应该很直观. (略丑)
8. 让从右向左变色也动起来.
public void leftToRight(View view) {
myTrackTextView.setOrientation(MyTrackTextView.Orientation.LEFT_TO_RIGHT);
ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0, 1);
valueAnimator.setDuration(2000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
myTrackTextView.setCurrentProgress(value);
}
});
valueAnimator.start();
}
public void rightToRight(View view) {
myTrackTextView.setOrientation(MyTrackTextView.Orientation.RIGHT_TO_LEFT);
ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0, 1);
valueAnimator.setDuration(2000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
myTrackTextView.setCurrentProgress(value);
}
});
valueAnimator.start();
}
收工, github地址稍后放出.