实现效果:点击文字控件,TextView的颜色会从左边或者右边开始滚动改变,从绿色逐步变为红色,
一、在开始之前希望你可以做一下以下工作,体验一下实际运行的效果:
1、打开AndroidStudio,新建一个工程
2、在main/res/values下新建一个attrs.xml,然后复制以下内容
<?xml version="1.0" encoding="utf-8"?>
<!--该文件是定义属性名和格式的地方-->
<resources>
<declare-styleable name="MyTextView">
<!--format是该属性的取值类型-->
<attr name="cmChangeTextColor" format="color"/>
<attr name="cmOriginTextColor" format="color"/>
</declare-styleable>
</resources>
3、在MainActivity.java同级目录下,新建一个MyTextView.java,然后复制以下内容
package com.你自己的包名,跟MainActivity一样即可;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
public class MyTextView extends android.support.v7.widget.AppCompatTextView {
private static final String TAG = "MyTextView";
Paint OriginPaint,ChangePaint;
//当前进度
private float cmCurrentProgress;
//默认朝向
private Directory cmCurrentDirectory = Directory.LEFT_TO_RIGHT;
public enum Directory{
LEFT_TO_RIGHT,RIGHT_TO_LEFT
}
public MyTextView(Context context) {
super(context);
}
//xml中构建的attrs使用该构造函数
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context,attrs);
}
//该构造函数用于代码中自定义控件时指定style
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}
private void init(Context context,AttributeSet attributeSet){
Log.d(TAG, "init: 获取自定义属性");//获取自定义属性
//获取颜色
TypedArray typedArray=context.obtainStyledAttributes(attributeSet,R.styleable.MyTextView);
int defValue=getTextColors().getDefaultColor();
int OriginColor=typedArray.getColor(R.styleable.MyTextView_cmOriginTextColor,defValue);
int ChangeColor=typedArray.getColor(R.styleable.MyTextView_cmChangeTextColor,defValue);
typedArray.recycle();
//创建画笔
OriginPaint=getTextPaintByColor(OriginColor);
ChangePaint=getTextPaintByColor(ChangeColor);
}
Paint getTextPaintByColor(int textColor){
Log.d(TAG, "getTextPaintByColor: 获取画笔");
//创建画笔
Paint paint = new Paint();
//设置画笔颜色
paint.setColor(textColor);
//设置抗锯齿
paint.setAntiAlias(true);
//设置防抖动
paint.setDither(true);
//设置字体大小
paint.setTextSize(getTextSize());
return paint;
}
//在这个方法中执行绘制操作,传入画笔和画布
@Override
protected void onDraw(Canvas canvas) {
//根据当前进度,获取当前中间值
int middle = (int) (cmCurrentProgress * getWidth());
//根据朝向,绘制TextView
if(Directory.LEFT_TO_RIGHT == cmCurrentDirectory){
//当前朝向为 从左到右
drawMyText(canvas,ChangePaint,0,middle);
drawMyText(canvas,OriginPaint,middle,getWidth());
}else{
//当前朝向 从右到左
drawMyText(canvas,ChangePaint,getWidth() - middle,getWidth());
drawMyText(canvas,OriginPaint,0,getWidth()-middle);
}
}
/*
* synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),
* 运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),
* 有的话要等正在使用synchronized方法的线程B(或者C 、D)
* 运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。*/
public synchronized void setDirectory(Directory directory){
this.cmCurrentDirectory = directory;
}
public synchronized void setCurrentProgress(float currentProgress){
this.cmCurrentProgress = currentProgress;
//刷新界面
invalidate();
}
private void drawMyText(Canvas canvas, Paint textPaint, int start, int end) {
Log.d(TAG, "drawMyText: 绘制文字");
//保存画布状态
canvas.save();
//设置绘制区域
Rect rect = new Rect(start,0,end,getHeight());
//裁剪
canvas.clipRect(rect);
//获取控件此时的文字
String text = getText().toString();
Rect bounds = new Rect();
//测量text的宽和高
textPaint.getTextBounds(text,0,text.length(),bounds);
//获取字体的宽度
int x = getWidth()/2 - bounds.width()/2;
//获取基线
Paint.FontMetricsInt fontMetricsInt = textPaint.getFontMetricsInt();
int dy = (fontMetricsInt.bottom - fontMetricsInt.top)/2 - fontMetricsInt.bottom;
int baseLine = getHeight()/2 + dy;
//https://blog.csdn.net/qqqq245425070/article/details/79027979
//这一步执行绘制,给出文字的内容和位置,就会按照Paint的要求去绘制
canvas.drawText(text,x,baseLine,textPaint);
//释放画布状态,既恢复Canvas旋转,缩放等之后的状态。
canvas.restore();
}
}
4、打开activity_main.xml,复制以下内容
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<你自己的包名.MyTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:clickable="true"
android:focusable="true"
android:textSize="24sp"
android:id="@+id/id_textView"
app:cmChangeTextColor="@color/colorAccent"
app:cmOriginTextColor="@color/colorPrimary"
/>
</LinearLayout>
5、打开MainActivity.java,复制以下内容
package 你自己的包名,不需要修改;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
public class MainActivity extends AppCompatActivity {
int i=0;//用于点击事件的状态切换,设置不同的方向
MyTextView myTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myTextView=findViewById(R.id.id_textView);
myTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
/*
在Android动画中,总共有两种类型的动画View Animation(视图动画)和Property Animator(属性动画);
View Animation包括Tween Animation(补间动画)和Frame Animation(逐帧动画);
Property Animator包括ValueAnimator和ObjectAnimation;
*/
// 创建ValueAnimator实例,对象区间在浮点0~1之间
ValueAnimator valueAnimator= ObjectAnimator.ofFloat(0,1);
//设置动画时长
valueAnimator.setDuration(5000);
//设置插值器,可以设定动画的移动效果,速度,是否回弹等
//参考https://blog.csdn.net/qq_30889373/article/details/78881140
valueAnimator.setInterpolator(new DecelerateInterpolator());
//valueAnimator.setInterpolator(new OvershootInterpolator());
if (i==0){
myTextView.setDirectory(MyTextView.Directory.LEFT_TO_RIGHT);
i=1;
}else {
myTextView.setDirectory(MyTextView.Directory.RIGHT_TO_LEFT);
i=0;
}
//设置监听,回传的结果为当前的状态数值,0~1,然后设置当前进度
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue= (float) animation.getAnimatedValue();
myTextView.setCurrentProgress(animatedValue);
}
});
valueAnimator.start();
}
});
}
}
6、运行
如果没有成功运行,检查activity_main.xml中包名是否修改为你的包名,各个新建的文件中有没有标红的地方,有的话,着手修改。
在Android中,自定义控件算是UI中比较复杂的知识(对新手来说),一般学习Android UI部分,遇到的第一个坎可能是ListView,因为这个控件需要比较多的代码去实现,而且涉及到Adapter,涉及到子项布局的设计,每行数据的存取值,还有一些优化写法等等;然后可能会遇到一些需要自定义的控件,实现个性化的展示,这里需要涉及attrs、Paint、Canvas等等,为了好理解,我觉得有必要从后面往前讲起。
二、一直跟踪代码的流转,会停留在这里,
canvas.drawText(text,x,baseLine,textPaint);
在我们自己写的代码中,是这一步执行了绘制的操作,你可以用Ctrl+F查看这个函数所在的位置。
这个方法所做的操作是,传入String字符串text,并告知它的位置(x,baseLine),然后按照textPaint的要求去绘制。
text的数据来源是用getText().toString()方法,直接获取这个控件的文字“MyTextView”。
x和baseLine,是与我们要绘制的文字的位置,这两个值都是int,如果你尝试修改这个值,将会发生有趣的变化,我们尝试在baseLine后面+30看会怎么样
canvas.drawText(text,x,baseLine+30,textPaint);
再次运行:
这里可以看到,我们改了位置,所以绘制的MyTextView字符整体下降了30,但是为什么显示不全呢?因为这个控件的高度是wrap_content,所以控件的高度只有一个TextView的高度,而它绘制的区域却在这个高度以外(baseLine是基线位置,关于这个可以参考https://blog.csdn.net/qqqq245425070/article/details/79027979,说的通俗易懂),当你在activity_main.xml中修改TextView的高度时,它就能全部显示出来了。
现在我们聚焦到最后一个变量textPaint,就是它导致我们的TextView变得与众不同,我们称它为画笔,而Canvas我们称为画布,刚刚我们说的那个函数,相当于笔和布都准备好了,然后你来画。
现在我们来观察准备画笔的操作。
第一步把画笔拿出来,也就是新建一个paint对象,
Paint paint = new Paint();
请按Ctrl+F查看这一句所在的位置。
然后准备颜料或笔头的一些材质什么的,重点关注一下颜色是怎么传进来的,我们使用
paint.setColor(textColor);
这个方法设置画笔颜色,而textColor是一个int类型,传入了OriginColor和ChangeColor
两个值,我们知道ChangeColor这个值是红色,而这个值是怎么得到的呢?通过
int ChangeColor=typedArray.getColor(R.styleable.MyTextView_cmChangeTextColor,defValue);
方法获取,注意MyTextView_cmChangeTextColor这个变量是来自attrs.xml中名为MyTextView的declare-styleable标签内的cmChangeTextColor,而这个cmChangeTextColor又被activity_main.xml中引用
可以看到上面的cmChangeTextColor取值为@color/colorAccent,而这个colorAccent正是红色,它在color中已经定义好了
其实我们也可以自己在activity_main.xml中随意更改一个颜色,看一下效果如何
运行
可以看到这时候颜色已经变为蓝色(注意activity_main.xml中的layout_height已经被我改100dp,所以可以全部显示)。
OK现在我们也明白了颜色的来源,那么我们来讲详细的画画过程。
三、一开始我们说绘制的方法是
canvas.drawText(text,x,baseLine,textPaint);
实际上,点击一下控件,就会触发很多次这个方法,实际上监听的执行函数在onDraw这里,你可以在这里加一句日志看一下到底点一下执行了多少次这个方法
Log.d(TAG, "onDraw: middle=“+middle+" cmCurrentProgress="+cmCurrentProgress+" getWidth="+getWidth());
那么打印出来的结果是:
。
。
。
可以看出来,middle这个值是从1到1080的变化的,不管是从左往右还是从右往左。
每执行一次,红色的内容就涂一点点,一直涂到满为止。
注意从右向左的drawMyText方法,执行的是getWidth() - middle,也就是当middle为0的时候,从1080的位置开始绘制,然后逐渐减下来。
在MainActivity中,只要设置ValueAnimator动画,做一些简单的初始化即可。