本篇文章用到了仿华为01-圆环进度和小球 里面的知识点:属性动画和插值器
上面的文章中只有一个小球.我们把他看成是此处最前面的那个小球.小球做加速减速动画.
小球动画实现的难点在于各个小球 位置的确定和变化
参考第 1 个小球实现的原理.可以想到,后面的小球也需要做加速减速动画.
但是加速时要依次递减. 也就是第 1 小球的加速度>第 2 个小球>第 3 个小球>…这样小球就会渐渐分散开
减速时也要依次递减 也就是第 1 个小球减速最快,后面的减速慢.这样后面的小球会追上前面的,形成类似融合效果.
总结: 每个小球都做加速减速动画.用不同的加速度和减速度来实现分散和聚合效果.不同的加速度和减速度我们可以用幂函数来实现.
为了减少代码和方便理解.我们用第二种方法实现
第1个 | 第2个 | 第3个 | 第4个 | 第5个 | |
---|---|---|---|---|---|
比例 | 0 | 01.2 0 1.2 | 01.5 0 1.5 | 01.9 0 1.9 | 02.5 0 2.5 |
实际角度 | 0 | 0 | 0 | 0 | 0 |
比例 | 0.125 | 0.1251.2 0.125 1.2 | 0.1251.5 0.125 1.5 | 0.1251.9 0.125 1.9 | 0.1252.5 0.125 2.5 |
实际角度 | 45 | 30 | 16 | 7 | 2 |
比例 | 0.25 | 0.251.2 0.25 1.2 | 0.251.5 0.25 1.5 | 0.251.9 0.25 1.9 | 0.252.5 0.25 2.5 |
实际角度 | 90 | 68 | 30 | 45 | 26 |
比例 | 0.5 | 0.51.2 0.5 1.2 | 0.51.5 0.5 1.5 | 0.51.9 0.5 1.9 | 0.52.5 0.5 2.5 |
实际角度 | 180 | 156 | 127 | 96 | 63 |
比例 | 0.75 | 0.751.2 0.75 1.2 | 0.751.5 0.75 1.5 | 0.751.9 0.75 1.9 | 0.752.5 0.75 2.5 |
实际角度 | 270 | 254 | 233 | 208 | 175 |
比例 | 1 | 11.2 1 1.2 | 11.5 1 1.5 | 11.9 1 1.9 | 12.5 1 2.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 = new float[5];
ObjectAnimator circleAnimator;
// 画对号相关参数
boolean isDrawMark = false;
Path path;
int mMarkAlpha;
public float getAngle() {
return angle;
}
public void setAngle(float angle) {
this.angle = angle;
}
public DotRotateAndCheckMark(Context context) {
this(context, null);
}
public DotRotateAndCheckMark(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DotRotateAndCheckMark(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStrokeWidth(4);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.BLUE);
postDelayed(new Runnable() {
@Override
public void run() {
startCircleRun();
}
}, 800);
radio[0] = 1f;
radio[1] = 1.2f;
radio[2] = 1.5f;
radio[3] = 1.9f;
radio[4] = 2.5f;
path = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mWidth = getWidth();
mHeight = getHeight();
minSize = Math.min(mWidth, mHeight) * 0.25f;
maxRadius = 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 = 0; i < 5; i++) {
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() {
@Override
public void run() {
invalidate();
}
}, 20);
}
// 获取小球半径
private float getRadius(float angele, int pos) {
float radius;
if (angele <= 180) {
radius = (1 - (angele / 360)) * maxRadius;
} else {
radius = (angele / 360) * maxRadius;
}
return radius / radio[pos];
}
// 开始小球动画
private void startCircleRun() {
// 控制 angle 参数变化的属性动画
if (circleAnimator == null) {
circleAnimator = ObjectAnimator.ofFloat(this, "angle", 360);
circleAnimator.setDuration(1500);
circleAnimator.setRepeatCount(ValueAnimator.INFINITE);
circleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
circleAnimator.start();// 动画开始后,setAngele()会被反射调用从而设置 angele 值
} else {
circleAnimator.cancel();
circleAnimator.start();
}
}
// 开始对勾
public void drawCheckMark() {
if (circleAnimator != null) circleAnimator.cancel();
isDrawMark = true;
ValueAnimator animator = ValueAnimator.ofInt(0, 255);
animator.setDuration(2500);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.start();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mMarkAlpha = (int) animation.getAnimatedValue();
invalidate();
}
});
}
}