加載動圖的實現及屬性動畫的使用

昨天在一個APP上面看見一個吃東西的加載圖,感覺挺簡單的,於是打算去實現。但是在實現的過程中踩了一些坑,那個圖也不能找到做參考了,於是自己琢磨了一個下午,終於實現了效果。因此這裏將整個過程做一個覆盤,以便自己能深刻的記住相關知識點。
本來應該像大佬一樣將圖片效果、引用方式、各個參數寫出來,但是鑑於我比較懶,不對,是沒有bintray.com的賬號。因此就將整個過程以及源碼(就一個類,有需要的朋友可以增加自定義屬性,供以後使用),先來一張效果圖鎮店:

效果圖

首先是佈局,我是這樣構思的:

佈局

將整個界面左右留下100px的內邊距,然後將界面一分爲二,左邊是一個嘴的圓形、右邊是三個圓球(Food),接下來這裏就比較容易了,就是確定尺寸,繪製扇形。

自定義View確定尺寸一般是onMeasure方法,在開發者藝術探索中曾看到這樣的描述:

measure過程中決定了View的寬/高,Measure完成以後,可以通過getMeasureWidth和getMeasureHeight方法來獲取到View測量後的寬/高,在幾乎所有的情況下它都等同於View最終的寬高。

但是我看到很多自定義View都沒有重寫這個方法,而是通過重寫onSizeChanged獲得整個View的寬高,原因是像我們實現的這種自定義控件一般情況不需要使用到Padding,因此我們不會通過onMeasure來獲取,因爲這個方法獲取相對而言複雜一些,而onSizeChanged直接將值傳過了,不需要進行任何處理就能獲得View的寬高。


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //View的寬度
        this.mWidth = w;
        //View的高度
        this.mHeight = h;
        //圓形半徑
        int raduis = (mWidth/2-100)/2;
        //繪製扇形的矩形
        mRectF.set(-raduis,-raduis,raduis,raduis);

    }

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(mRectF.right+50, mHeight / 2);
        canvas.drawArc(mRectF, 30, 300, true, paint);
    }

這裏我當時有一個疑問是繪製扇形的時候,我當時想的是,我繪製的角度是30°到330°,因此drawArc方法的起始角度應該是這樣:

canvas.drawArc(mRectF, 30, 330, true, paint);

結果繪製出來的圓形嘴的角度只有30°,後來測試了幾個角度才明白,它這裏的起始角度的確是起點的角度,但是重點角度不是圓形的角度,而是你繪製的角度。這裏有點繞,就是說終點角度指的是你需要繪製的角度是多少度,我需要露出60度的嘴,因此我繪製的角度應該是300度,不是330度。而330度的話,剩下30度的嘴也是正常的。因此如果想要繪製逆時針的扇形的話,可以嘗試終點角度爲負值,如下:

canvas.drawArc(mRectF, 150, -300, true, paint);

150度起點
關於繪製扇形角度的問題上面描述得並不準確,比如當我們繪製的起點角度爲30度時,則圖形又變得不能理解了:

canvas.drawArc(mRectF, 30, -300, true, paint);

30度起點

不管如何,我們可以實現效果就好,如果需要做扇形統計圖之類的動態變化的話,則需要深入瞭解起始角度的真實意義,我查看源碼沒有發現處理起始角度是具體數據:

private static native void native_drawArc(long nativeCanvas, float left, float top,
float right, float bottom,
float startAngle, float sweep, boolean useCenter,
long nativePaint);

drawArc
void drawArc (RectF oval,
float startAngle,
float sweepAngle,
boolean useCenter,
Paint paint)

Draw the specified arc, which will be scaled to fit inside the specified oval.

If the start angle is negative or >= 360, the start angle is treated as start angle modulo 360.

If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is negative, the sweep angle is treated as sweep angle modulo 360

The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0 degrees (3 o’clock on a watch.)

Parameters
oval RectF: The bounds of oval used to define the shape and size of the arc
This value must never be null.
startAngle float: Starting angle (in degrees) where the arc begins
sweepAngle float: Sweep angle (in degrees) measured clockwise
useCenter boolean: If true, include the center of the oval in the arc, and close it if it is being stroked. This will draw a wedge
paint Paint: The paint used to draw the arc
This value must never be null.
繪製指定的弧,其將被縮放以適合指定的橢圓。

如果起始角度爲負或> = 360,起始角度被視爲起始角度模數360。

如果掃掠角度> 360°,則橢圓形被完全繪製。 請注意,這與SkPath :: arcTo略有不同,它將掃描角度視爲360度。如果掃描角度爲負,則掃描角度被視爲360度掃描角度

弧線順時針拉伸。 0度的角度對應於0度(手錶3點鐘)的幾何角度。

繪製好了圓形的嘴,接下來我們可以考慮圓球的佈局了,我是這樣構思的:

佈局

小球在2/3的圓球直徑的三個點依次排開,代碼如下:

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.mWidth = w;
        this.mHeight = h;
        //圓形半徑
        int raduis = (mWidth/2-100)/2;
        mRectF.set(-raduis,-raduis,raduis,raduis);
        //小球位置1
        centerX1 = raduis+2f/3*raduis-raduis*1/5f;
        //小球位置2
        centerX2 = raduis+4f/3*raduis-raduis*1/5f;
        //小球位置3
        centerX3 = raduis+6/3*raduis-raduis*1/5f;

    }


  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(mRectF.right+100, mHeight / 2);
        canvas.drawArc(mRectF, 150, -300, true, paint);
        paint.setColor(Color.GREEN);
        canvas.drawCircle(centerX1,0,mRectF.right*0.2f,paint);
        paint.setColor(Color.BLUE);
        canvas.drawCircle(centerX2,0,mRectF.right*0.2f,paint);
        paint.setColor(Color.YELLOW);
        canvas.drawCircle(centerX3,0,mRectF.right*0.2f,paint);
    }

逼叨叨了這麼久,終於把界面展示出來了,那麼我們這個動畫如何實現呢?網上關於屬性動畫的文章非常多,我覺得這個文章講得足夠詳細了—— Android屬性動畫完全解析!而且我們這裏的動畫根本用不上動畫集合,一個動畫就足夠,我的思路是在做一個動畫,動畫中小球的移動路徑爲X放向大圓球直徑的2/3,當移動了90%的時候,最前面的小球隱藏,打球閉合,動畫結束之後再次執行,將三個小球的初始位置依次往前一格提一步,就可以看到類似吃掉的效果了!
描述起來比較抽象,咱們再看看效果圖:

這裏寫圖片描述

然後我們可以看看動畫與繪製方法:

   public void startMoving(){
        if(getVisibility() != View.VISIBLE || isMoving){
            return;
        }
        isMoving = true;
        Moving = true;

        anim = ValueAnimator.ofFloat(0f,1f);
        anim.setDuration(2000);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentValue = (float) animation.getAnimatedValue();

                dex = currentValue*mRectF.right*2f/3;

                if(currentValue>0.9f ){
                    //嘴閉合
                    isClose = true;
                }else{
                    //嘴張開
                    isClose = false;
                }

                postInvalidate();
            }


        });
        anim.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                isClose = false;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                isMoving = false;

                if(Moving){
                    //動態切換小球位置,形成流式滾動
                    float dest = centerX1;
                    centerX1 = centerX3;
                    centerX3 = centerX2;
                    centerX2 = dest;
                    startMoving();
                }

            }

            @Override
            public void onAnimationCancel(Animator animation) {
                //當動畫停止時小球位置迴歸原點
                centerX1 = mRectF.right+2f/3*mRectF.right-mRectF.right*1/5f;
                centerX2 = mRectF.right+4f/3*mRectF.right-mRectF.right*1/5f;
                centerX3 = mRectF.right+6/3*mRectF.right-mRectF.right*1/5f;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        //保證小球勻速滾動
        anim.setInterpolator(new LinearInterpolator());
        anim.start();
    }


public void stopMoving(){
        Moving = false;
        anim.cancel();
    }


 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        paint.setColor(Color.RED);
        canvas.translate(mRectF.right+100, mHeight / 2);


        if(isClose){
            canvas.drawArc(mRectF, 0, 360, true, paint);
            float checkSize = 22f/15*mRectF.right;

            //將在第一個展示的小球不予繪製
            if(centerX1-dex>checkSize){
                paint.setColor(Color.GREEN);
                canvas.drawCircle(centerX1-dex,0,mRectF.right*0.2f,paint);

            }
            if(centerX2-dex>checkSize){
                paint.setColor(Color.BLUE);
                canvas.drawCircle(centerX2-dex,0,mRectF.right*0.2f,paint);

            }
            if(centerX3-dex>checkSize){
                paint.setColor(Color.YELLOW);
                canvas.drawCircle(centerX3-dex,0,mRectF.right*0.2f,paint);
            }

        }else{
            canvas.drawArc(mRectF, 30, 300, true, paint);
            paint.setColor(Color.GREEN);
            canvas.drawCircle(centerX1-dex,0,mRectF.right*0.2f,paint);
            paint.setColor(Color.BLUE);
            canvas.drawCircle(centerX2-dex,0,mRectF.right*0.2f,paint);
            paint.setColor(Color.YELLOW);
            canvas.drawCircle(centerX3-dex,0,mRectF.right*0.2f,paint);
        }


    }

我之前在動畫的實現過程中有一個坑,我在處理小球位置的時候,addUpdateListener接口的onAnimationUpdate方法裏面直接處理用小球的X座標減去動畫的更新值,如下:

 @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentValue = (float) animation.getAnimatedValue();

                dex = currentValue*mRectF.right*2f/3;
                centerX3-=dex;
                centerX2-=dex;
                centerX1-=dex;
                if(currentValue>0.9f ){
                    isClose = true;
                }else{
                    isClose = false;
                }

                postInvalidate();
            }

結果在動畫過程中後面的速度不斷加快,導致不能得到預期的效果,後來一步一步檢查才發現,這個屬性值沒有處理合適。

如果我們這個空間需要在橫豎屏都能使用的話,還需要對寬高進行一個判斷,修改如下:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.mWidth = w;
        this.mHeight = h;
        int normal = (w<h)?w:h;

        //圓形半徑
        float raduis = (normal/2-Padding)/2;
        mRectF.set(-raduis,-raduis,raduis,raduis);
        //小球位置1
        centerX1 = raduis+2f/3*raduis-raduis*1/5f;
        centerX2 = raduis+4f/3*raduis-raduis*1/5f;
        centerX3 = raduis+6/3*raduis-raduis*1/5f;

    }

如果需要封裝爲一個自定義控件,方便今後隨時能用的話,設置自定義屬性的時候可以設置View的padding屬性,然後通過onMeasure獲取,或者通過自定義屬性獲取,還有顏色、小球半徑(建議小於1/3大球半徑)等。
這個View就一個java文件,因此就不貼代碼連接了,直接顯示出來:

public class EatLoading extends View {
    private static final float Padding = 100;
    private int mWidth;
    private int mHeight;
    private Paint paint;    //畫筆
    private RectF mRectF;    //矩形
    private boolean isMoving = false;//當前動畫是否結束
    private boolean isClose = false;//圓形是否閉合
    private boolean Moving = false;//循環動畫
    private float centerX1,centerX2,centerX3;
    private float dex = 0;
    private ValueAnimator anim;

    public EatLoading(Context context) {
        super(context);
        initPaint();
    }

    public EatLoading(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public EatLoading(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }
    //初始化畫筆
    private void initPaint() {
        paint = new Paint();
        //設置畫筆模式:填充
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(30);
        //初始化區域
        mRectF = new RectF();
    }



    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.mWidth = w;
        this.mHeight = h;
        int normal = (w<h)?w:h;

        //圓形半徑
        float raduis = (normal/2-Padding)/2;
        mRectF.set(-raduis,-raduis,raduis,raduis);
        //小球位置1
        centerX1 = raduis+2f/3*raduis-raduis*1/5f;
        centerX2 = raduis+4f/3*raduis-raduis*1/5f;
        centerX3 = raduis+6/3*raduis-raduis*1/5f;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        paint.setColor(Color.RED);
        canvas.translate(mRectF.right+Padding, mHeight / 2);


        if(isClose){
            canvas.drawArc(mRectF, 0, 360, true, paint);
            float checkSize = 22f/15*mRectF.right;
            if(centerX1-dex>checkSize){
                paint.setColor(Color.GREEN);
                canvas.drawCircle(centerX1-dex,0,mRectF.right*0.2f,paint);

            }
            if(centerX2-dex>checkSize){
                paint.setColor(Color.BLUE);
                canvas.drawCircle(centerX2-dex,0,mRectF.right*0.2f,paint);

            }
            if(centerX3-dex>checkSize){
                paint.setColor(Color.YELLOW);
                canvas.drawCircle(centerX3-dex,0,mRectF.right*0.2f,paint);
            }

        }else{
            canvas.drawArc(mRectF, 30, 300, true, paint);
            paint.setColor(Color.GREEN);
            canvas.drawCircle(centerX1-dex,0,mRectF.right*0.2f,paint);
            paint.setColor(Color.BLUE);
            canvas.drawCircle(centerX2-dex,0,mRectF.right*0.2f,paint);
            paint.setColor(Color.YELLOW);
            canvas.drawCircle(centerX3-dex,0,mRectF.right*0.2f,paint);
        }


    }

    public void startMoving(){
        if(getVisibility() != View.VISIBLE || isMoving){
            return;
        }
        isMoving = true;
        Moving = true;

        anim = ValueAnimator.ofFloat(0f,1f);
        anim.setDuration(2000);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentValue = (float) animation.getAnimatedValue();

                dex = currentValue*mRectF.right*2f/3;
                centerX3-=dex;
                if(currentValue>0.9f ){
                    isClose = true;
                }else{
                    isClose = false;
                }

                postInvalidate();
            }


        });
        anim.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                isClose = false;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                isMoving = false;

                if(Moving){
                    float dest = centerX1;
                    centerX1 = centerX3;
                    centerX3 = centerX2;
                    centerX2 = dest;
                    startMoving();
                }

            }

            @Override
            public void onAnimationCancel(Animator animation) {
                centerX1 = mRectF.right+2f/3*mRectF.right-mRectF.right*1/5f;
                centerX2 = mRectF.right+4f/3*mRectF.right-mRectF.right*1/5f;
                centerX3 = mRectF.right+6/3*mRectF.right-mRectF.right*1/5f;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        anim.setInterpolator(new LinearInterpolator());
        anim.start();
    }

    public void stopMoving(){
        Moving = false;
        anim.cancel();
    }


}

參考文章:

Android開源:一款你不可錯過的可愛&小資風格的加載等待控件庫

發佈了61 篇原創文章 · 獲贊 28 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章