Android自定义View(9)- 写一个加载控件

照例先看图:
Screenrecorder-2021-07-07-18-01-31-742[1]202177184151.gif
一、六个小圆的绘制及旋转原理

先看六个小圆动画实现原理,看图:


load.png

控件宽高已知,图中中心点 C 可求。半径 R 自定义(已知),图中∠a = (360 / 6)度。将这些参数带入公式,各点可求。下面给出公式:

Pi_x = (Width / 2) + R × sin (ΔB × a × i)
Pi_y = (Width / 2) - R × cos (ΔB × a × i)

上面公式中除了 ΔB 和 i ,其他参数都已知。而 i 代表的是图中的第 i 个圆,也就是从0 ~ 5 。ΔB 是旋转时所旋转的角度。所以,要实现六个圆沿着中心点 C 旋转一周,只需要使用属性动画产生从 0 ~ 360度的值代入 ΔB 即可求出各小圆圆心的实时坐标。下面是实现公式:

  // 循环绘制 6 个小圆
        for (int i = 0; i < colors.length; i++) {
            mPaint.setColor(colors[i]);
            float circleX = (float) (core.x + rotationRadius * Math.sin(i * 2 * Math.PI / 6 + deltaAngle));
            float circleY = (float) (core.y - rotationRadius * Math.cos(i * 2 * Math.PI / 6 + deltaAngle));
            canvas.drawCircle(circleX, circleY, miniCircleRadius, mPaint);
        }
二、照片显示部分的动画效果
circle.png

其实这部分效果的实现也很简单,只是花了一个圆,不断地改变圆的半径即可。这里画的是 Paint.Style.STROKE 样式的空心圆。从上图中可以看到,空心圆的半径并不只是空心部分的半径宽度。而是包括了空心部分半径再加上画笔线条宽度的一半。图中红色直线代表画笔线条宽度,而蓝色直线代表空心圆的真正半径
设画笔宽度为 W ,范围从 0 到 H(屏幕对角线的一半,可求)。那么空心圆半径公式:
R = H - (W / 2) ;

所以,只要在0 到 H范围内通过属性动画不断改变 画笔线条宽度 W 的值,就可以算出实时的半径 R。当线条宽度为最大,即等于 H 时,圆的空心部分为 0 ,圆的半径刚好等于画笔线宽的一半 W/2。而当画笔线宽为0时,圆的空心部分达到最大,就可以将背景照片完全显示出来。
半径计算公式:

float strokeWidth = sqrtDistance * (1 - value);
transparentPaint.setStrokeWidth(strokeWidth);
tpRadius = strokeWidth / 2 + (sqrtDistance - strokeWidth);

value是属性动画产生的值,从 0 ~ 1.
下面是完整代码:

/**
 *加载控件
 *
 * Ethan Lee
 */
public class RotationLoadingView extends View {
    public static final String TAG = "RotationLoadingView";
    /**
     * 控件宽高
     */
    private float mWidth, mHeight;
    /**
     * 6个小圆围绕旋转中心点的位置
     */
    private PointF core;
    /**
     * 6 个小圆围绕旋转的大半径
     */
    private float rotationRadius;
    /**
     * 6 个小圆的半径
     */
    private float miniCircleRadius;
    /**
     * 6 中颜色
     */
    private int[] colors;
    /**
     * 小圆画笔
     */
    private Paint mPaint;

    /**
     * 旋转角度,以中心点正上方为 0 度
     */
    private double deltaAngle = 0;

    /**
     * 空心大圆画笔
     */
    private Paint transparentPaint;
    /**
     * 控件对角线的一半
     */
    private float sqrtDistance;
    /**
     * 空心大圆半径
     */
    private float tpRadius = 0;

    // 属性动画
    private ValueAnimator mValueAnimator;
    private AnimationEndListener mAnimationEndListener;

    public RotationLoadingView(Context context) {
        this(context, null);
    }

    public RotationLoadingView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RotationLoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initRes(context, attrs, defStyleAttr);
    }

    private void initRes(Context context, AttributeSet attrs, int defStyleAttr) {
        int blue = Color.parseColor("#3079F6");
        int red = Color.parseColor("#E41A1A");
        int green = Color.parseColor("#33C339");
        int purple_500 = Color.parseColor("#FF6200EE");
        int teal_700 = Color.parseColor("#FF018786");
        int yellow = Color.parseColor("#BFAC03");
        colors = new int[]{blue, red, green, purple_500, teal_700, yellow};
        mPaint = new Paint();
        mPaint.setColor(colors[0]);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        core = new PointF();
        transparentPaint = new Paint();
        transparentPaint.setColor(getResources().getColor(R.color.white));
        transparentPaint.setStyle(Paint.Style.STROKE);
        transparentPaint.setAntiAlias(true);
        transparentPaint.setDither(true);
    }

    /**
     * 对外接口,动画开始
     */
    public void startAnimator(){
        dataReset();
        getRotationAnimator();
    }

    /**
     * 对外接口,取消动画
     */
    public void setAnimatorCancel(){
        if (mValueAnimator != null) {
            mValueAnimator.cancel();
            mValueAnimator = null;
        }
    }

    /**
     * 开始属性动画
     */
    private void getRotationAnimator() {
        if (mValueAnimator != null) {
            mValueAnimator.cancel();
            mValueAnimator = null;
        }
        mValueAnimator = new ValueAnimator();
        // 将动画分成4小段,用于控制4段效果
        mValueAnimator.setFloatValues(0, 1, 2, 3, 4);
        // 总时长
        mValueAnimator.setDuration(4000);
        mValueAnimator.addUpdateListener(this::dealWithValue);
        mValueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                Log.d(TAG, "onAnimationStart");
                dataReset();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                Log.d(TAG, "onAnimationEnd");
                setAnimationEnd();
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                Log.d(TAG, "onAnimationCancel");
                dataReset();
                setAnimationEnd();
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                Log.d(TAG, "onAnimationRepeat");
            }
        });
        mValueAnimator.start();
    }

    /**
     * 开始计算绘制参数
     *
     * @param animator
     */
    private void dealWithValue(ValueAnimator animator) {
        float value = (float) animator.getAnimatedValue();
        Log.d(TAG, "V = " + value);

        if (value > 3){   // 计算第四段动画参数
            // -3 使 value 从 0 变到 1
            value = value - 3;
            // 计算大圆的参数
            float strokeWidth = sqrtDistance * (1 - value);
            transparentPaint.setStrokeWidth(strokeWidth);
            tpRadius = strokeWidth / 2 + (sqrtDistance - strokeWidth);
            // 计算6 个小圆的参数
            deltaAngle = (1 - value) * 4 * Math.PI;
            value = (float) (value * 1.25);
            rotationRadius = sqrtDistance * value;
        }else if (value > 2){ // 计算第三段动画参数
            // -2 使 value 从 0 变到 1
            value = value - 2;
            deltaAngle = (1 + value) * 2 * Math.PI;
            rotationRadius = (3 * mWidth / 8) * (1 - value);
        }else if (value > 1){ // 计算第二段动画参数
            // -1 使 value 从 0 变到 1
            value = value - 1;
            rotationRadius = (mWidth / 4) * (1 + value / 2);
        }else {  // 计算第一段动画参数
            deltaAngle = value * 2 * Math.PI;
        }
        // 有时候一个轮回下来 value都没有 1
        // 重绘
        invalidate();
    }

    /**
     * 重置参数
     */
    private void dataReset() {
        deltaAngle = 0;
        rotationRadius = mWidth / 4;
        tpRadius = sqrtDistance / 2;
        transparentPaint.setStrokeWidth(sqrtDistance);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mWidth = getWidth();
        mHeight = getHeight();
        // 初始化参数
        core.set(mWidth / 2, mHeight / 2);
        miniCircleRadius = mWidth / 32;
        sqrtDistance = (float) Math.sqrt(mWidth * mWidth / 4 + mHeight * mHeight / 4);
        dataReset();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 绘制背景大圆
        canvas.drawCircle(core.x, core.y, tpRadius, transparentPaint);
        // 循环绘制 6 个小圆
        for (int i = 0; i < colors.length; i++) {
            mPaint.setColor(colors[i]);
            float circleX = (float) (core.x + rotationRadius * Math.sin(i * 2 * Math.PI / 6 + deltaAngle));
            float circleY = (float) (core.y - rotationRadius * Math.cos(i * 2 * Math.PI / 6 + deltaAngle));
            canvas.drawCircle(circleX, circleY, miniCircleRadius, mPaint);
        }
    }

    public interface AnimationEndListener {
        void animationEnd();
    }

    public void setAnimationEndListener(AnimationEndListener animationEndListener) {
        mAnimationEndListener = animationEndListener;
    }

    public void setAnimationEnd() {
        if (mAnimationEndListener != null) {
            mAnimationEndListener.animationEnd();
        }
    }
}

Demo在:Github源码

你可能感兴趣的:(Android自定义View(9)- 写一个加载控件)