版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
Demo源码
JD加载动画是这样的:
我做出来的是这样的:(修改前)
(微调后:)
中间的狗我不会画,所以我就画了个勾。
我把速度动画速度设置的很慢,这样的话方便观察和学习。
其实看到jd的加载动画,第一反应就是想到了 路径动画
我之前写过关于路径动画的用法:Android自定义控件开发入门与实战(6)路径动画
因为这个加载动画的路径都像是已经设好了,只要朝着指定方向绘制就行了。
难点主要在 画狗(他们用到了贝塞尔曲线,而我没有坐标,设计不出来),其他的话,其实还真没什么值得注意的地方。
所以这一篇就当做是对路径动画做一个复习吧= =。
1、观察动画顺序
从gif图,我们可以看出动画顺序为:
- 两个小圆向左向右移动,移动到画布的边界时消失
- 小圆消失时产生两个大圆,分别位于左上和右下,并且画布中间开始画狗
- 两个大圆开始顺时针转圈,狗的进度和大圆一样
- 在狗快要画完时,画布逐渐透明到消失
从上看出,我们需要:
- 一开始做小圆的移动动画
- 小圆移动完的一瞬间做大圆的转圈动画
- 做画勾动画
- 在画勾进度快结束时,做透明度动画
2、绘制路径
我们需要绘制勾的路径、大圆的路径,并用PathMeasure和路径进行联系。
我们需要在 onSizeChanged()
或者画布测量完的地方开始做
//初始化坐标参数
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//找出坐标原点,并初始化大圆半径和小圆半径
originX = (float) (getWidth() / 2);
originY = (float) (getHeight() / 2);
bigRadius = (float) getWidth() / 2;
smallRadius = bigRadius / 10;
//画勾
gouPath = new Path();
gouPath.moveTo(originX - bigRadius / 2, originY);
gouPath.lineTo(originX, originY + bigRadius / 2);
gouPath.lineTo(originX + bigRadius / 2, originY - bigRadius / 3);
//画大圆
bigPath1 = new Path();
bigPath1.moveTo(originX - bigRadius, originY);
bigPath1.addArc(0, 0, getWidth(), getHeight(), 180, 360);
bigPath2 = new Path();
bigPath2.moveTo(originX + bigRadius, originY);
bigPath2.addArc(0, 0, getWidth(), getHeight(), 0, 360);
//画布的形状path
canvasPath.addCircle(originX, originY, bigRadius, Path.Direction.CW);
//连接路径动画和路径
pathMeasureGou = new PathMeasure(gouPath, false);
pathMeasureBigCircle1 = new PathMeasure(bigPath1, false);
pathMeasureBigCircle2 = new PathMeasure(bigPath2, false);
}
上面在画圆的时候我用的是Path.addArc
而不是 Path.addCircle
,原因是addCircle的起点一定是x轴正方向,很显然,我们要的两个圆他们的起始位置都不同,所以不能让起点统一,而 addArc正好可以定义圆的起点。
3、懒加载定义动画
我们用ValueAnimator就能够计算出小圆移动、大圆转圈(和画勾同一个进度)、透明度变化的动画进度了
在小圆结束的时候,开始大圆转圈和画勾的动画,在画勾快要结束时,开始透明度度的动画
//两个小圆分开的动画
private ValueAnimator getSmallAnimation() {
if (animatorSmallCircle == null) {
animatorSmallCircle = ValueAnimator.ofFloat(0, 1);
animatorSmallCircle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//获取小圆的动画进度
smallProgress = (float) animation.getAnimatedValue();
invalidate();
}
});
animatorSmallCircle.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//在动画结束的时候,大圆开始转圈圈并且开始打钩,播放着两个动画
getGouAndBigCircleAnimation().start();
}
});
animatorSmallCircle.setDuration(700);
animatorSmallCircle.setInterpolator(new AccelerateInterpolator());
}
return animatorSmallCircle;
}
//打勾动画和大圆转圈的动画是同时进行的,所以公用一个动画
public ValueAnimator getGouAndBigCircleAnimation() {
if (animatorGouAndBigCircle == null) {
animatorGouAndBigCircle = ValueAnimator.ofFloat(0, 1);
animatorGouAndBigCircle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
bigProgress = (float) animation.getAnimatedValue();
//在打勾动画的进度快要结束时,设置透明度动画
if (bigProgress <= 0.3f) {
if (!getAnimatorAlpha().isStarted()) {
getAnimatorAlpha().start();
}
}
invalidate();
}
});
animatorGouAndBigCircle.setDuration(1000);
animatorGouAndBigCircle.setInterpolator(new LinearInterpolator());
}
return animatorGouAndBigCircle;
}
//透明度动画
public ValueAnimator getAnimatorAlpha() {
if (animatorAlpha == null) {
//透明度从1-0,用ObjectAnimator也可以做
animatorAlpha = ValueAnimator.ofFloat(1.0f, 0f);
animatorAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
alphaProgress = (float) animation.getAnimatedValue();
invalidate();
}
});
animatorAlpha.setDuration(1000);
animatorAlpha.setInterpolator(new AccelerateInterpolator());
}
return animatorAlpha;
}
4、onDraw绘制
我们需要根据动画计算出的进度,来绘制这些路径:
//就是开始各种动画
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//将画布裁剪成圆形,填充灰色
canvas.save();
canvas.clipPath(canvasPath);
canvas.drawColor(Color.parseColor("#f0f0f0"));
//小圆动画,在x轴上随着小圆动画进度而移动
if (smallProgress <= 0.955f && smallProgress >= 0) {
canvas.drawCircle(originX + (smallProgress * bigRadius), originY, smallRadius, smallPaint1);
canvas.drawCircle(originX - (smallProgress * bigRadius), originY, smallRadius, smallPaint2);
}
//打勾动画,大圆转圈的动画,大圆是画布周长的长度是1/4
if (bigProgress <= 1f && bigProgress >= 0) {
float bigStop1 = pathMeasureBigCircle1.getLength() * bigProgress;
dstBigPath1.reset();
pathMeasureBigCircle1.getSegment(bigStop1 - (pathMeasureBigCircle1.getLength() * 0.25f), bigStop1, dstBigPath1, true);
float bigStop2 = pathMeasureBigCircle2.getLength() * bigProgress;
dstBigPath2.reset();
pathMeasureBigCircle2.getSegment(bigStop1 - (pathMeasureBigCircle2.getLength() * 0.25f), bigStop2, dstBigPath2, true);
float gouStop = pathMeasureGou.getLength() * bigProgress;
dstGouPath.reset();
pathMeasureGou.getSegment(0, gouStop, dstGouPath, true);
}
//透明度动画
if (alphaProgress >= 0f && alphaProgress <= 1f) {
bigPaint1.setAlpha((int) (255 * alphaProgress));
bigPaint2.setAlpha((int) (255 * alphaProgress));
gouPaint.setAlpha((int) (255 * alphaProgress));
}
//绘制路径
canvas.drawPath(dstGouPath, gouPaint);
canvas.drawPath(dstBigPath1, bigPaint1);
canvas.drawPath(dstBigPath2, bigPaint2);
}
到这里,一个简单的加载动画就做完啦
小结
- 路径动画不难,而且实用性特别高,效果很棒,我一开始以为jd的加载动画很炫酷所以会很复杂,但是用这个做法,其实把动画思路理清楚了,还是蛮简单的
- Path的
addCircle
和addArc
有区别,addArc相比于addCircle更适合路径动画,因为它可以定义圆的起点 - 我这里没有用到贝塞尔曲线,因为我没有考虑到绘图坐标,以后会想办法去用一下。
后续编辑
因为那片灰色的画布一开始是没有的,所以我把他抽成一个圆出来,在做透明动画时,将这个圆也搞消失。
canvas.drawCircle(originX,originY,canvasRadius,canvasPaint);
...
//透明度动画
if (alphaProgress >= 0f && alphaProgress <= 1f) {
this.setAlpha(alphaProgress);
}