序言:最近项目中有一个这样的需求,当用户填写完数据之后,传后台去计算,然后需要跑一个这样的动画,由于这个动画效果同事用的是后台切的图组成的帧动画,然后一向强迫症如我就非常不喜欢那么多图片就是为了成就这么一个动画,于是乎我决定自己用最近学习的属性动画来写一个,如有错误或者更好的解决办法,请及时指正,话不多说,先看图:
我们可以来分析一下,首先呢我们可以把这个图分成四部分,三串数字一个圆,每一串数字一个接一个的从圆的上侧滚动到下侧,但是注意并不是同时的,我们可以用三个动画为三串数字来实现这个效果(这是我的想法),先从中间开始,因为中间部分坐标什么的都比较简单:
在这里把自定义view的几个步骤讲的稍微详细一点,为了自己能得到复习的同时也为了能帮助需要了解自定义view的同学,顺便说一下,如果你还没有看我的另外一篇关于自定义view之属性动画的,请移步:Android自定义view之属性动画初见
1、先自定义属性,以便以后可以自己定制,圆的颜色,半径,字体颜色及大小,我我们暂时就需要这么几个属性:
<declare-styleable name="CustomNumAnimView">
<attr name="round_radius" format="dimension" />
<attr name="round_color" format="color" />
<attr name="text_color" format="color" />
<attr name="text_size" format="dimension" />
</declare-styleable>
2、好了,然后我们得在构造方法中获得属性所对应的值:
private int roundColor; //圆的颜色
private int textColor; //数字的颜色
private float textSize; //数字字体大小
private float roundRadius; //圆的半径
private Paint mPaint; //画笔
private Rect textRect; //包裹数字的矩形
public CustomNumAnimView(Context context, AttributeSet attrs) {
super(context, attrs);
//获取自定义属性
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomNumAnimView, defStyleAttr, 0);
roundColor = array.getColor(R.styleable.CustomNumAnimView_round_color, ContextCompat.getColor(context, R.color.colorPrimary));
roundRadius = array.getDimension(R.styleable.CustomNumAnimView_round_radius, 50);
textColor = array.getColor(R.styleable.CustomNumAnimView_text_color, Color.WHITE);
textSize = array.getDimension(R.styleable.CustomNumAnimView_text_size, 30);
array.recycle();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(textSize);
textRect = new Rect();
//得到数字矩形的宽高,以用来画数字的时候纠正数字的位置
mPaint.getTextBounds(middleNum, 0, middleNum.length(), textRect);
}
3、获取完属性之后,我们得要画一个圆,画在哪里呢?当然是屏幕的中心了,在onDraw方法中做如下操作:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setAntiAlias(true); //设置抗锯齿
mPaint.setStyle(Paint.Style.FILL_AND_STROKE); //设置画笔填充,画实心圆
mPaint.setColor(roundColor); //设置圆的颜色
canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, roundRadius, mPaint); //画圆
}
4、我们可以画数字了,在画数字之前我想让大家知道Android中手机屏幕的坐标系的结构,如下图所示:
好了,对坐标系有一定了解之后,我们开始画数字,先把中间数字的效果做出来,我记得我在上一篇关于动画的博客中有讲到
TypeEvaluator
,这真是个好东西,有不清楚的同学请移步我之前的博客,在这里也给大家推荐一个学习属性动画的博客:Android自定义控件三部曲文章索引
public class CustomPointEvaluator implements TypeEvaluator {
/**
*
* @param fraction 系数
* @param startValue 起始值
* @param endValue 终点值
* @return
*/
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
CustomPoint startPoint = (CustomPoint) startValue;
CustomPoint endPoint = (CustomPoint) endValue;
float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
CustomPoint point = new CustomPoint(x, y);
return point;
}
}
这个类帮助我们告诉系统如何在设置的时间内从初始值过渡到结束值,并获取中间的状态
5、上面的类中还有一个东西CustomPoint
,这个表示每一个数字行进过程中的坐标,我们把它当作一个点来处理,这样更加方便:
public class CustomPoint {
private float x; //点的x坐标
private float y; //点的y坐标
public CustomPoint(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
}
6、然后我们就开始动画的过程:
private boolean isFirstInit = false; //是否是第一次初始化
private CustomPoint middlePoint; //中间的数字的实时点
private ValueAnimator middleAnim; //中间数字动画
private String middleNum = "9";
private boolean isMiddleNumInvalidate = false; //中间数字是否重绘界面
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!isFirstInit) {
middlePoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
drawText(canvas);
startAnimation(); //开始动画
isFirstInit = true;
} else {
drawText(canvas);
}
}
/**
* 画数字
* @param canvas
*/
private void drawText(Canvas canvas) {
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setColor(roundColor);
canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, roundRadius, mPaint);
mPaint.setColor(textColor);
mPaint.setTextSize(textSize);
if (isMiddleNumInvalidate) {
canvas.drawText(middleNum, middlePoint.getX(), middlePoint.getY(), mPaint);
isMiddleNumInvalidate = false;
}
}
7、这个过程还是比较好理解的,首先所有的点我们只初始化一遍,然后开始动画也只在第一次初始化中执行,因为我们设置的动画是无限循环的,然后就是我们的startAnimation()
方法了:
private void startAnimation() {
//初始化中间数字的开始点的位置
final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
//初始化中间数字的结束点的位置
final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 + roundRadius + textRect.height() / 2);
middleAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
//监听从起始点到终点过程中点的变化,并获取点然后重新绘制界面
middleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
middlePoint = (CustomPoint) animation.getAnimatedValue();
isMiddleNumInvalidate = true;
invalidate();
}
});
middleAnim.setDuration(300);
middleAnim.setRepeatCount(ValueAnimator.INFINITE);
}
8、这个过程也还是比较好理解的,首先我们创建了两个点,也就是数字的初始位置以及结束位置,然后再利用ValueAnimator
的ofObject
方法来对数字行进路线进行分析,然后每一次监听的时候都让界面进行重绘,这样就能感觉数字一直在移动,让我们来看一下效果:
9、OMG,图片录制的不太友好,实际效果可不是这样子的,但是我们不难发现,数字好像没有变,只是单一的数字,下面我们要做的就是在一次动画结束之后,取随机数,怎么样才能知道动画一次运行完成了呢?万能的
google
肯定会有方法的,我们只需要再加一个监听就好了:
middleAnim.addListener(new CustomAnimListener() {
@Override
public void onAnimationRepeat(Animator animation) {
middleNum = getRandom();
}
});
/**
* 获取0-9之间的随机数
*
* @return
*/
private String getRandom() {
int random = (int) (Math.random() * 9);
return String.valueOf(random);
}
10、这个监听就是当动画重复之后会执行这个方法,我们也可以认为每当动画执行完一遍之后都会执行这个方法。好了,让我们再来看看效果吧:(可能录屏软件都有点问题,看起来并不和谐>_<)
11、完成一个之后,剩下的两个就简单了,我们只需要找到旁边两个点的坐标就行了,我是这么分析的,我们来看一张图:
首先呢,左右两边肯定是关于
Y
轴对称的,而且我的想法是,这三个数字所在的点将X
轴平分成了四段(只包括整个圆),然后根据这个可以算出左边点的横纵坐标,纵左边是横坐标的根号三倍,算出坐标之后就好办了,依照中间点的动画原则,我们来看一下完整的代码:
public class CustomNumAnimView extends View {
private int roundColor; //圆的颜色
private int textColor; //数字的颜色
private float textSize; //数字字体大小
private float roundRadius; //圆的半径
private Paint mPaint; //画笔
private Rect textRect; //包裹数字的矩形
private boolean isFirstInit = false; //是否是第一次初始化
private CustomPoint leftPoint; //左边的数字的实时点
private ValueAnimator leftAnim; //左边数字动画
private String leftNum = "9";
private boolean isLeftNumInvalidate = false; //左边数字是否重绘界面
private CustomPoint middlePoint; //中间的数字的实时点
private ValueAnimator middleAnim; //中间数字动画
private String middleNum = "9";
private boolean isMiddleNumInvalidate = false; //中间数字是否重绘界面
private CustomPoint rightPoint; //右边的数字的实时点
private ValueAnimator rightAnim; //右边数字动画
private String rightNum = "9";
private boolean isRightNumInvalidate = false; //右边数字是否重绘界面
public CustomNumAnimView(Context context) {
this(context, null);
}
public CustomNumAnimView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomNumAnimView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomNumAnimView, defStyleAttr, 0);
roundColor = array.getColor(R.styleable.CustomNumAnimView_round_color, ContextCompat.getColor(context, R.color.colorPrimary));
roundRadius = array.getDimension(R.styleable.CustomNumAnimView_round_radius, 50);
textColor = array.getColor(R.styleable.CustomNumAnimView_text_color, Color.WHITE);
textSize = array.getDimension(R.styleable.CustomNumAnimView_text_size, 30);
array.recycle();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(textSize);
textRect = new Rect();
//得到数字矩形的宽高,以用来画数字的时候纠正数字的位置
mPaint.getTextBounds(middleNum, 0, middleNum.length(), textRect);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!isFirstInit) {
//初始化三串数字
leftPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 - roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
middlePoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
rightPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 + roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
drawText(canvas);
startAnimation(); //开始动画
isFirstInit = true;
} else {
drawText(canvas);
}
}
/**
* 画数字
* @param canvas
*/
private void drawText(Canvas canvas) {
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setColor(roundColor);
canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, roundRadius, mPaint);
mPaint.setColor(textColor);
mPaint.setTextSize(textSize);
if (isLeftNumInvalidate) {
canvas.drawText(leftNum, leftPoint.getX(), leftPoint.getY(), mPaint);
isLeftNumInvalidate = false;
}
if (isMiddleNumInvalidate) {
canvas.drawText(middleNum, middlePoint.getX(), middlePoint.getY(), mPaint);
isMiddleNumInvalidate = false;
}
if (isRightNumInvalidate) {
canvas.drawText(rightNum, rightPoint.getX(), rightPoint.getY(), mPaint);
isRightNumInvalidate = false;
}
}
public void startAnim() {
if (isAnimStart(leftAnim)) {
leftAnim.start();
}
if (isAnimStart(middleAnim)) {
middleAnim.start();
}
if (isAnimStart(rightAnim)) {
rightAnim.start();
}
}
private boolean isAnimStart(ValueAnimator anim) {
return !anim.isStarted() || anim.isPaused();
}
public void pauseAnim() {
if (isAnimStop(leftAnim)) {
leftAnim.pause();
}
if (isAnimStop(middleAnim)) {
middleAnim.pause();
}
if (isAnimStop(rightAnim)) {
rightAnim.pause();
}
}
/**
* 在onDestroy方法中调用
*/
public void stopAnim() {
leftAnim.end();
middleAnim.end();
rightAnim.end();
leftAnim = null;
middleAnim = null;
rightAnim = null;
}
private boolean isAnimStop(ValueAnimator anim) {
return null != anim && anim.isRunning();
}
//开始动画
private void startAnimation() {
startLeft();
startMiddle();
startRight();
}
private void startLeft() {
final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 - roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 - roundRadius / 2, (float) (getMeasuredHeight() / 2 + roundRadius * (Math.sqrt(3) / 2) + textRect.height() / 2));
leftAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
leftAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
leftPoint = (CustomPoint) animation.getAnimatedValue();
isLeftNumInvalidate = true;
invalidate();
}
});
leftAnim.addListener(new CustomAnimListener() {
@Override
public void onAnimationRepeat(Animator animation) {
leftNum = getRandom();
}
});
leftAnim.setStartDelay(100);
leftAnim.setDuration(300);
leftAnim.setRepeatCount(ValueAnimator.INFINITE);
}
private void startMiddle() {
//初始化中间数字的开始点的位置
final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
//初始化中间数字的结束点的位置
final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 + roundRadius + textRect.height() / 2);
middleAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
//监听从起始点到终点过程中点的变化,并获取点然后重新绘制界面
middleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
middlePoint = (CustomPoint) animation.getAnimatedValue();
isMiddleNumInvalidate = true;
invalidate();
}
});
middleAnim.addListener(new CustomAnimListener() {
@Override
public void onAnimationRepeat(Animator animation) {
middleNum = getRandom();
}
});
middleAnim.setDuration(300);
middleAnim.setRepeatCount(ValueAnimator.INFINITE);
}
private void startRight() {
final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 + roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 + roundRadius / 2, (float) (getMeasuredHeight() / 2 + roundRadius * (Math.sqrt(3) / 2) + textRect.height() / 2));
rightAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
rightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
rightPoint = (CustomPoint) animation.getAnimatedValue();
isRightNumInvalidate = true;
invalidate();
}
});
rightAnim.addListener(new CustomAnimListener() {
@Override
public void onAnimationRepeat(Animator animation) {
rightNum = getRandom();
}
});
rightAnim.setStartDelay(150);
rightAnim.setDuration(300);
rightAnim.setRepeatCount(ValueAnimator.INFINITE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int size;
int mode;
int width;
int height;
size = MeasureSpec.getSize(widthMeasureSpec);
mode = MeasureSpec.getMode(widthMeasureSpec);
if (mode == MeasureSpec.EXACTLY) { //确定的值或者MATCH_PARENT
width = size;
} else { //表示WARP_CONTENT
width = (int) (2 * roundRadius);
}
mode = MeasureSpec.getMode(heightMeasureSpec);
size = MeasureSpec.getSize(heightMeasureSpec);
if (mode == MeasureSpec.EXACTLY) { //确定的值或者MATCH_PARENT
height = size;
} else { //表示WARP_CONTENT
height = (int) (2 * roundRadius);
}
setMeasuredDimension(width, height);
}
/**
* 获取0-9之间的随机数
*
* @return
*/
private String getRandom() {
int random = (int) (Math.random() * 9);
return String.valueOf(random);
}
}
12、好了,这就是完整的代码,该有的注释我都加上去了,有不懂得地方可以私信我,如果还有更好的解决方法也请私戳我,下面来看一下最后的效果:
好吧!看起来也不怎么和谐了,不过大家可以下载代码去跑一遍,真正运行起来的不是这个样子的,代码我已上传至GitHub,有需要的同学可以下载,star