天天看點

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

簡介

昨天在簡書上看到一篇文章,介紹了一個加載動畫的實作過程

一款Loading動畫的實作思路(一)

隻可惜原動畫是IOS上制作的,而看了一下,作者的實作思路比較複雜,于是趁着空閑寫了一個Android版本,這篇文章将給大家介紹一下實作過程。

首先讓我們來看一下動畫效果

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

動畫結構分析

從上面的gif圖中可以看到,這個加載動畫有成功,失敗兩種狀态,由于Gif速度比較快,我們再來分别看一張慢圖

1、成功狀态加載動畫

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

成功動畫的狀态轉移描述如下:

1、加載過程,畫藍色圓環,當進度為100%時,圓環完成

2、從右側抛出藍色小方塊,小方塊沿着曲線到達圓環正上方

3、藍色小方塊下落,下落過程中,逐漸變長,當方塊與圓圈接觸時,進入圓環的部分變粗,同時圓環逐漸被擠壓,變成橢圓形

4、方塊底端到達圓環中心後,發出三個分叉向圓周延伸,同時橢圓被撐大,逐漸恢複回圓形

5、圓環變綠色,畫出綠色勾√

整個過程可以說是比較複雜的,甚至對比原動畫,其實還有一些細節我沒有去實作,不過接下來我為大家逐個分解每個過程是怎麼實作的,而且并不難了解。每個小過程組合起來,就是一款炫酷動畫,希望大家都有信心去了解它。

自定義View,根據進度繪制圓形

首先我們來實作第一個過程,圓環的繪制。

在動畫效果中,圓環的完整程度,是根據實際的進度來衡量的,當加載完成,整個圓就畫好了。

是以我們自定義一個View控件,在其提供了一個setProgress()方法來給使用者設定進度

public class SuperLoadingProgress extends View {
    /**
     * 目前進度
     */
    private int progress = ;
    /**
     * 最大進度
     */
    private static final int maxProgress = ;

    ....
    public void setProgress(int progress) {
        this.progress = Math.min(progress,maxProgress);
        postInvalidate();
        if (progress==){
            status = ;
        }
    }
    ...
   }
           

有了這個進度以後,我們就調用postInvalidate()去讓控件重繪,其實就是觸發了其ondraw()方法,然後我們就再ondraw()方法裡面,繪制圓弧

對于圓弧的繪制,相信大家都不會陌生(陌生也沒有關系,因為很簡單),隻要調用一個canvas.drawArc()方法就可以了。

但是我要仔細觀察這裡的圓形效果,在單獨來看三張圖

圓弧起始狀态

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

圓弧運動狀态

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

圓弧最終狀态

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

可以看到,首先圓弧有一定的起始角度,我們知道,在Android坐标系中,0度其實是指水準向右開始的

也就是起點的起始角度,其實是-90度,終點的起始角度,其實-150度

而整個過程中,

起點:-90度,逆時針旋轉270度,最後回到0度位置

終點:-150度,與起點相差60度,最後相差360度,與起點重合

是以當progress=1,也就是動畫完成時,起點會減去270度,那麼對應每個progress

起點的位置應該是

-90-270*progress

當progress=1,終點和起點相差360度,而一開始就相差60度,是以整個過程就是多相差了300度,那麼對應每個progress,終點和起點應該相差

-(60+precent*300)

根據上面的結論,我們得到圓弧的具體繪制方式如下:

/**
    * 起始角度
    */
    private static final float startAngle = -;
    @Override
    protected void onDraw(Canvas canvas) {
        ...
        float precent = f*progress/maxProgress;//目前完成百分比
        //mRectF是代表整個view的範圍
        canvas.drawArc(mRectF, startAngle-*precent, -( + precent*), false, circlePaint);
    }
           

圓環完成,抛出小方塊

在圓環繪制完成以後,會抛出一個小方塊,小方塊沿曲線運動到圓環正上方,實際整個曲線,是一段圓弧

我們來看下圖

方塊運動狀态

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

運動狀态分析圖

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

從圖中可以看出,方塊運動的終點,距離圓心為2R

假設運動軌迹是某個圓的一段弧,那麼根據勾股定理有如下方程

(X+R)^2 + (2R)^2 = (X+2R)^2

解得X=R/2(其實也很容易解,就是勾三股四玄五)

假設我們希望方塊在500ms内從起點運動到終點,那麼我們就需要提供一個計時器,告訴我們現在運動了多少毫秒,然後根據這個時間,計算出方塊目前位置

另外,由于方塊本身有一定的長度,是以方塊也有自己的起始端和末端。但是兩者的運動軌迹是一樣的,隻是先後不同。

//抛出動畫
        endAngle = (float) Math.atan(/);
        mRotateAnimation = ValueAnimator.ofFloat(, endAngle* );
        mRotateAnimation.setDuration();
        mRotateAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        mRotateAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                curSweepAngle = (float) animation.getAnimatedValue();//運動了多少角度
                invalidate();
            }
        });
           

每次獲得新角度,我們就去重新繪制方塊的位置:

/**
     * 抛出小方塊
     * @param canvas
     */
    private void drawSmallRectFly(Canvas canvas){
        canvas.save();
        canvas.translate(radius /  + strokeWidth,  * radius + strokeWidth);//将坐标移動到大圓圓心
        float bigRadius = *radius/;//大圓半徑
        //方塊起始端坐标
        float x1 = (float) (bigRadius*Math.cos(curSweepAngle));
        float y1 = -(float) (bigRadius*Math.sin(curSweepAngle));
        //方塊末端坐标
        float x2 = (float) (bigRadius*Math.cos(curSweepAngle+*endAngle+*endAngle*(-curSweepAngle/*endAngle)));//
        float y2 = -(float) (bigRadius*Math.sin(curSweepAngle+*endAngle+*endAngle*(-curSweepAngle/*endAngle)));
        canvas.drawLine(x1, y1, x2, y2, smallRectPaint);//小方塊,其實是一條直線
        canvas.restore();        
        canvas.drawArc(mRectF, , , false, circlePaint);//藍色圓環
    }
           

抛出完成,方塊下落

可以說下落過程,是整個動畫中最複雜的過程了,包括方塊下落,圓環擠壓,方塊變粗三個過程,整個過程,從方塊下落開始,到方塊底部到底圓心

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

首先是方塊的下落,這個容易了解,方塊會逐漸變長,因為在相同時間内,起始端和末端運動的距離不一樣

我們拿末端作為例子,這裡要使用到一個知識,就是P**ath路徑類**

這是Android提供的一個類,代表我們制定的一段路徑執行個體,對于方塊末端來說,其運動的路徑就是從頂部,到圓心

Path downPath1 = new Path();//起始端路徑
    downPath1.moveTo(*radius+strokeWidth,strokeWidth);
    downPath1.lineTo( * radius+strokeWidth, radius+strokeWidth);
    Path downPath2 = new Path();//末端路徑
    downPath2.moveTo( * radius+strokeWidth, strokeWidth);
    downPath2.lineTo( * radius+strokeWidth,  * radius+strokeWidth);
           

那麼問題來了,有了運動路徑以後,我們希望有動畫,起始就是希望,我們給定一個動畫時間,我們可以獲得在這段時間的某個點上,起始端/末端運動到路徑的哪個位置

那麼有了路徑以後,我們能不能獲得路徑上的任意一個位置呢?答案是使用PathMeasure類。

可能有許多朋友對這個類不熟悉,可以參考一些文章,或者看看官方API介紹

看PathMeasure大展身手

我們首先來看,怎麼初始化一個PathMeasure,很簡單,傳入一個Path對象即可,false表示不閉合這個路徑

downPathMeasure1 = new PathMeasure(downPath1,false);
    downPathMeasure2 = new PathMeasure(downPath2,false);
           

由于動畫有一定時間,我們又需要一個計時器

//下落動畫        
    mDownAnimation = ValueAnimator.ofFloat(f, f );
    mDownAnimation.setDuration();
    mDownAnimation.setInterpolator(new AccelerateInterpolator());
    mDownAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            downPrecent = (float) animation.getAnimatedValue();
            invalidate();
        }
    });
           

接下來是使用PathMeasure獲得下落過程中,起始端和末端的坐标

//下落方塊的起始端坐标
    float pos1[] = new float[];
    float tan1[] = new float[];
    downPathMeasure1.getPosTan(downPrecent * downPathMeasure1.getLength(), pos1, tan1);
    //下落方塊的末端坐标
    float pos2[] = new float[];
    float tan2[] = new float[];
    downPathMeasure2.getPosTan(downPrecent * downPathMeasure2.getLength(), pos2, tan2);
           

getPosTan()方法,第一個參數是指想要獲得的路徑長度,例如你設定的Path長度為100

那麼你傳入60,就會獲得長度為60時的終點坐标(文字真的表達不好/(ㄒoㄒ)/~~,大家可以去看API)

根據起始端和末端的坐标*,我們繪制一條直線,就是小方塊啦!

方塊下落,進入圓内部分變粗,圓被擠壓變形

接下來要處理一個更加複雜的問題,就是進入圓環中的方塊部分,要變粗。

為了解決這個問題,我們就需要分辨方塊哪部分在圓内,哪部分在圓外,這個判斷起來本身就很麻煩,況且,圓環還會被壓縮!也就是園内圓外,沒有一個固定的分界點!

怎麼區分圓内圓外呢?我決定自己判斷太麻煩了,後來想到一個辦法,判斷交集!

我們知道,Android提供了API,讓我們可以判斷兩個Rect是否相交,也可以獲得它們的相交部分(也就是重合部分),還可以獲得非重合部分。

假設我把方塊看成是一個矩形,圓環看成一個矩形,那麼問題就簡單了,我就可以調用API計算出進入圓内的部分,和在圓外的部分了。

如下圖:

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

我們知道,其實圓/橢圓,都是依靠一個矩形确定的,在這個動畫中,我們希望圓被擠壓成橢圓,最終縮放比例為0.8,大概是這樣的

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

利用前面提到的計時器,我們可以根據目前時間,知道圓被擠壓的比例,實作擠壓效果

//橢圓形區域
    Rect mRect = new Rect(Math.round(mRectF.left),Math.round(mRectF.top+mRectF.height()*f*downPrecent),
                Math.round(mRectF.right),Math.round(mRectF.bottom-mRectF.height()*f*downPrecent));
           

這樣,我們就有了代表橢圓的矩形。由于在一步中,我們知道了小方塊的起始端和末端坐标,我們可以使這個兩個坐标,分别向左右偏移一定距離,進而獲得4個坐标,來建立矩形。

最後,我們直接利用兩個矩形,取交集和非交集,具體實作如下:

//非交集
        Region region1 = new Region(Math.round(pos1[])-strokeWidth/,Math.round(pos1[]),Math.round(pos2[]+strokeWidth/),Math.round(pos2[]));
        region1.op(mRect, Region.Op.DIFFERENCE);
        drawRegion(canvas, region1, downRectPaint);

        //交集
        Region region2 = new Region(Math.round(pos1[])-strokeWidth/,Math.round(pos1[]),Math.round(pos2[]+strokeWidth/),Math.round(pos2[]));
        boolean isINTERSECT = region2.op(mRect, Region.Op.INTERSECT);
        drawRegion(canvas, region2, downRectPaint);
           

Region是Android提供的,用于處理區域運算問題的一個類,使用這個類,我們可以很友善進行Rect交集補集等運算,不了解的朋友,檢視API

最後繪制這兩個區域,并且加上一個判斷,就是這個兩個矩形是否有相交,如果沒有,那麼圓環就不用被擠壓,直接繪制圓環即可。

//橢圓形區域
    if(isINTERSECT) {//如果有交集
        float extrusionPrecent = (pos2[]-radius)/radius;
        RectF rectF = new RectF(mRectF.left, mRectF.top + mRectF.height() *  * extrusionPrecent, mRectF.right, mRectF.bottom - mRectF.height() *  * extrusionPrecent);//繪制橢圓
        canvas.drawArc(rectF, , , false, circlePaint);
    }else{
        canvas.drawArc(mRectF, , , false, circlePaint);//繪制圓
    }
           

下落完成,繪制三叉

對于三叉的繪制,就沒有什麼特别的了,其實三叉就是三條Path路徑,我們用類似前面的做法,利用一個計時器,三個Path,對應三個PathMeasure,就可以動态繪制出路徑了。

/**
     * 繪制分叉
     * @param canvas
     */
    private void drawFork(Canvas canvas) {
        float pos1[] = new float[];
        float tan1[] = new float[];
        forkPathMeasure1.getPosTan(forkPrecent * forkPathMeasure1.getLength(), pos1, tan1);
        float pos2[] = new float[];
        float tan2[] = new float[];
        forkPathMeasure2.getPosTan(forkPrecent * forkPathMeasure2.getLength(), pos2, tan2);
        float pos3[] = new float[];
        float tan3[] = new float[];
        forkPathMeasure3.getPosTan(forkPrecent * forkPathMeasure3.getLength(), pos3, tan3);

        canvas.drawLine( * radius+strokeWidth, radius+strokeWidth,  * radius+strokeWidth,  * radius+strokeWidth, downRectPaint);
        canvas.drawLine( * radius+strokeWidth,  * radius+strokeWidth, pos1[], pos1[], downRectPaint);
        canvas.drawLine( * radius+strokeWidth,  * radius+strokeWidth, pos2[], pos2[], downRectPaint);
        canvas.drawLine( * radius+strokeWidth,  * radius+strokeWidth, pos3[], pos3[], downRectPaint);
        //橢圓形區域
        RectF rectF = new RectF(mRectF.left, mRectF.top + mRectF.height() * f * (-forkPrecent), 
                mRectF.right, mRectF.bottom - mRectF.height() * f * (-forkPrecent));
        canvas.drawArc(rectF, , , false, circlePaint);
    }
           

最後,還要記得将橢圓還原成圓,其實就是壓縮的逆過程

效果如下:

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

繪制綠色勾√

綠色勾的繪制其實也和上面的做法類似,需要一個計時器,一個Path,對應的PathMeasure即可

勾的路徑如下:

//初始化打鈎路徑
        Path tickPath = new Path();
        tickPath.moveTo( * radius+strokeWidth,  * radius+strokeWidth);
        tickPath.lineTo( * radius +  * radius+strokeWidth,  * radius +  * radius+strokeWidth);
        tickPath.lineTo(*radius+ * radius+strokeWidth,*radius- * radius+strokeWidth);
        tickPathMeasure = new PathMeasure(tickPath,false);
           

最後将路徑動态繪制出現,到這裡大家都很熟悉這個做法了。但是這裡我使用了另外一個方法,這個方法可以根據進度,直接傳回目前路徑成一個Path對象

/**
     * 繪制打鈎
     * @param canvas
     */
    private void drawTick(Canvas canvas) {
        Path path = new Path();
        /*
         * On KITKAT and earlier releases, the resulting path may not display on a hardware-accelerated Canvas. 
         * A simple workaround is to add a single operation to this path, such as dst.rLineTo(0, 0).
         */
        tickPathMeasure.getSegment(, tickPrecent * tickPathMeasure.getLength(), path, true);//該方法,可以獲得整個路徑的一部分
        path.rLineTo(, );//解決Android本身的一個bug
        canvas.drawPath(path, tickPaint);//繪制出這一部分
        canvas.drawArc(mRectF, , , false, tickPaint);
    }
           

于是我們在一定時間内,逐漸獲得勾這個路徑的一部分,知道獲得整個勾,并将其繪制出來!

最終效果如下:

一款炫酷Loading動畫--加載成功簡介動畫結構分析自定義View,根據進度繪制圓形圓環完成,抛出小方塊抛出完成,方塊下落方塊下落,進入圓内部分變粗,圓被擠壓變形下落完成,繪制三叉繪制綠色勾√寫在最後

寫在最後

本篇文章,首先介紹成功加載的動畫實作過程,下一篇文章将會接着介紹加載失敗過程的實作。

通過這篇文章,我們應該熟悉了Path,PathMeasure,Region等一系列API,利用這些API,我們可以友善得繪制出路徑效果。

每個步驟組合起來,就是一個好看的,複雜的動效。對于API不熟悉的朋友,建議用到的時候去查官方文檔,或者看看其他朋友的一些介紹基礎的文章。

最後,提供源碼下載下傳位址和github位址,歡迎大家下載下傳和star

繼續閱讀