仿华为02-旋转圆球和对勾

最终效果和原理

gif图片出错

效果分为三方面
多个小球按上图动画旋转
小球的大小按规律变化,模拟分散和聚合
最后画一个对勾

小球位置和动画的实现原理

本篇文章用到了仿华为01-圆环进度和小球 里面的知识点:属性动画和插值器
上面的文章中只有一个小球.我们把他看成是此处最前面的那个小球.小球做加速减速动画.

小球动画实现的难点在于各个小球 位置的确定和变化
参考第 1 个小球实现的原理.可以想到,后面的小球也需要做加速减速动画.
但是加速时要依次递减. 也就是第 1 小球的加速度>第 2 个小球>第 3 个小球>…这样小球就会渐渐分散开
减速时也要依次递减 也就是第 1 个小球减速最快,后面的减速慢.这样后面的小球会追上前面的,形成类似融合效果.

总结: 每个小球都做加速减速动画.用不同的加速度和减速度来实现分散和聚合效果.不同的加速度和减速度我们可以用幂函数来实现.

实现方式:
为每个小球设置一个属性动画,并自定义对应的插值器.
只给第 1 个小球设置属性动画,其余小球位置由第一个小球确定

为了减少代码和方便理解.我们用第二种方法实现

第1个 第2个 第3个 第4个 第5个
比例 0 0 1.2 0 1.5 0 1.9 0 2.5
实际角度 0 0 0 0 0
比例 0.125 0.125 1.2 0.125 1.5 0.125 1.9 0.125 2.5
实际角度 45 30 16 7 2
比例 0.25 0.25 1.2 0.25 1.5 0.25 1.9 0.25 2.5
实际角度 90 68 30 45 26
比例 0.5 0.5 1.2 0.5 1.5 0.5 1.9 0.5 2.5
实际角度 180 156 127 96 63
比例 0.75 0.75 1.2 0.75 1.5 0.75 1.9 0.75 2.5
实际角度 270 254 233 208 175
比例 1 1 1.2 1 1.5 1 1.9 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();
            }
        });
    }
}

备注
AndroidStudio 3.1.2, compileSdkVersion 27
小球的属性动画作用于属性 angle,对应 set() get() 方法不可或缺
小球动画是自动开启,画对勾方法对外暴露
没有对外提供 attr 属性设置
项目地址

不足
小球半径变化未能实现理想状态
小球变对号之间没有转场动画

猜你喜欢

转载自blog.csdn.net/hepann44/article/details/80765342
02-