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源码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容