最终效果和原理
效果分为三方面- 多个小球按上图动画旋转
- 小球的大小按规律变化,模拟分散和聚合
- 最后画一个对勾
小球位置和动画的实现原理
本篇文章用到了仿华为01-圆环进度和小球 里面的知识点:属性动画和插值器
上面的文章中只有一个小球.我们把他看成是此处最前面的那个小球.小球做加速减速动画.
小球动画实现的难点在于各个小球 位置的确定和变化
参考第 1 个小球实现的原理.可以想到,后面的小球也需要做加速减速动画.
但是加速时要依次递减. 也就是第 1 小球的加速度>第 2 个小球>第 3 个小球>…这样小球就会渐渐分散开
减速时也要依次递减 也就是第 1 个小球减速最快,后面的减速慢.这样后面的小球会追上前面的,形成类似融合效果.
总结: 每个小球都做加速减速动画.用不同的加速度和减速度来实现分散和聚合效果.不同的加速度和减速度我们可以用幂函数来实现.
实现方式:- 为每个小球设置一个属性动画,并自定义对应的插值器.
- 只给第 1 个小球设置属性动画,其余小球位置由第一个小球确定
为了减少代码和方便理解.我们用第二种方法实现
| 第1个 | 第2个 | 第3个 | 第4个 | 第5个 |
---|
比例 | 0 | 01.201.2 | 01.501.5 | 01.901.9 | 02.502.5 |
实际角度 | 0 | 0 | 0 | 0 | 0 |
比例 | 0.125 | 0.1251.20.1251.2 | 0.1251.50.1251.5 | 0.1251.90.1251.9 | 0.1252.50.1252.5 |
实际角度 | 45 | 30 | 16 | 7 | 2 |
比例 | 0.25 | 0.251.20.251.2 | 0.251.50.251.5 | 0.251.90.251.9 | 0.252.50.252.5 |
实际角度 | 90 | 68 | 30 | 45 | 26 |
比例 | 0.5 | 0.51.20.51.2 | 0.51.50.51.5 | 0.51.90.51.9 | 0.52.50.52.5 |
实际角度 | 180 | 156 | 127 | 96 | 63 |
比例 | 0.75 | 0.751.20.751.2 | 0.751.50.751.5 | 0.751.90.751.9 | 0.752.50.752.5 |
实际角度 | 270 | 254 | 233 | 208 | 175 |
比例 | 1 | 11.211.2 | 11.511.5 | 11.911.9 | 12.512.5 |
实际角度 | 360 | 360 | 360 | 360 | 360 |
总结:
1. 以第 1 个小球(角度占360度比例)为幂函数底数,后面小球对应的幂为 1.2, 1.5, 1.9, 2.5
2. 因为第 1 个小球本身具有加速减速动画,以其为基准后所有小球就用相同动画特点
3. 起始,结束位置相同,速度变化不同,实现了小球间分散聚合
小球半径的计算
为使得聚散效果更逼真,小球的半径还需要变化.理想状态是0-180度时,前面小球变小,后面小球变大,仿佛是前面分解了部分融入后面. 180-360则相反.整个过程保持前面小球半径大于后面小球.
这里没能实现理想效果,小球半径是一起变大和变小的.
小球 1 半径的变化规律
上图是小球 1 的半径变化示意图,起始结束位置最大,180度时半径减半
假设小球 1 此时半径为 r , 小球 2 就为 r /1.2, 小球 3 是 r/1.5 …. 具体可以自己调整.
对勾的实现
画对勾是 View 对外暴露的方法.当需要的时候调用. 此时结束小球动画,设置画笔透明度setAlpha()
来画出一个缓慢浮现的对勾
代码
public class DotRotateAndCheckMark extends View {Paint mPaint;float mWidth;float mHeight;float minSize;float angle;float maxRadius;float[] radio &#61; new float[5];ObjectAnimator circleAnimator;boolean isDrawMark &#61; false;Path path;int mMarkAlpha;public float getAngle() {return angle;}public void setAngle(float angle) {this.angle &#61; angle;}public DotRotateAndCheckMark(Context context) {this(context, null);}public DotRotateAndCheckMark(Context context, &#64;Nullable AttributeSet attrs) {this(context, attrs, 0);}public DotRotateAndCheckMark(Context context, &#64;Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}private void init() {mPaint &#61; new Paint(Paint.ANTI_ALIAS_FLAG);mPaint.setStrokeWidth(4);mPaint.setStyle(Paint.Style.FILL);mPaint.setColor(Color.BLUE);postDelayed(new Runnable() {&#64;Overridepublic void run() {startCircleRun();}}, 800);radio[0] &#61; 1f;radio[1] &#61; 1.2f;radio[2] &#61; 1.5f;radio[3] &#61; 1.9f;radio[4] &#61; 2.5f;path &#61; new Path();}&#64;Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);mWidth &#61; getWidth();mHeight &#61; getHeight();minSize &#61; Math.min(mWidth, mHeight) * 0.25f;maxRadius &#61; minSize * 0.06f;canvas.translate(mWidth / 2, mHeight / 2);if (isDrawMark) {drawMark(canvas);} else {drawDots(canvas);}}private void drawMark(Canvas canvas) {mPaint.setStyle(Paint.Style.STROKE);mPaint.setStrokeCap(Paint.Cap.ROUND);mPaint.setStrokeJoin(Paint.Join.ROUND);path.moveTo(-minSize * 0.5f, -minSize * 0.15f);path.lineTo(-minSize * 0.05f, minSize * 0.3f);path.lineTo(minSize * 0.6f, -minSize * 0.4f);mPaint.setAlpha(mMarkAlpha);canvas.drawPath(path, mPaint);}private void drawDots(Canvas canvas) {mPaint.setStyle(Paint.Style.FILL);for (int i &#61; 0; i <5; i&#43;&#43;) {canvas.save();canvas.rotate((float) (Math.pow(angle / 360, radio[i]) * 360), 0, 0);canvas.drawCircle(0, -minSize, getRadius(angle, i), mPaint);canvas.restore();}postDelayed(new Runnable() {&#64;Overridepublic void run() {invalidate();}}, 20);}private float getRadius(float angele, int pos) {float radius;if (angele <&#61; 180) {radius &#61; (1 - (angele / 360)) * maxRadius;} else {radius &#61; (angele / 360) * maxRadius;}return radius / radio[pos];}private void startCircleRun() {if (circleAnimator &#61;&#61; null) {circleAnimator &#61; ObjectAnimator.ofFloat(this, "angle", 360);circleAnimator.setDuration(1500);circleAnimator.setRepeatCount(ValueAnimator.INFINITE);circleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());circleAnimator.start();} else {circleAnimator.cancel();circleAnimator.start();}}public void drawCheckMark() {if (circleAnimator !&#61; null) circleAnimator.cancel();isDrawMark &#61; true;ValueAnimator animator &#61; ValueAnimator.ofInt(0, 255);animator.setDuration(2500);animator.setInterpolator(new AccelerateDecelerateInterpolator());animator.start();animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {&#64;Overridepublic void onAnimationUpdate(ValueAnimator animation) {mMarkAlpha &#61; (int) animation.getAnimatedValue();invalidate();}});}
}
备注- AndroidStudio 3.1.2, compileSdkVersion 27
- 小球的属性动画作用于属性 angle,对应 set() get() 方法不可或缺
- 小球动画是自动开启,画对勾方法对外暴露
- 没有对外提供 attr 属性设置
- 项目地址
不足- 小球半径变化未能实现理想状态
- 小球变对号之间没有转场动画