炫酷的loadingView

最近比較忙,煩心的事情也不少。就迷上了一款游戲《守望先鋒》,差點就沒回來。

言歸正傳,前些日子看到一個很炫酷的loadingView,看到的時候感覺,這個感覺怎么說呢,用英語說就是amazing(太TM吊了)。
我也僅僅只是通過別人的博客,加上一點自己的理解寫的這篇博客,目的是想要和大家分享,順便記錄一下。感覺實現一個這么炫酷的動畫還是感覺挺有成就感動的(畢竟菜鳥一枚)。這里先放上原博主的鏈接,感謝這位大神。這邊博客把實現過程已經寫的很清晰的建議有些自定義view基礎的人,先去看這里先放上原博主的博客,然后自己實現以下,我這里會對整個view的實現過程詳細的講一下。

此次時間有點倉促,沒有對代碼進行優化,同時也有部分原作者的代碼。希望大家諒解,主要是給大家提供一個思路。

先看一下效果圖:



怎么樣,我沒說錯吧,第一眼看見就眼前一亮。下面我們就對整個過程進行詳細的講解。

拆分動畫

  • 和葉子一樣顏色的進度條
  • 右側旋轉的白色電風扇
  • 漂浮的葉子(原博主說的很細致我這里直接引用原話)
    1.葉子的隨機產生;
    2.葉子隨著一條正余弦曲線移動;
    3.葉子在移動的時候旋轉,旋轉方向隨機,正時針或逆時針;
    4.葉子遇到進度條,似乎是融合進入;
    5.葉子不能超出最左邊的弧角;
    7.葉子飄出時的角度不是一致,走的曲線的振幅也有差別,否則太有規律性,缺乏美感;
  • 最后又一個結束動畫,風扇消失,然后“100%”出現

整個動畫就是這樣子的,難點就是繪制葉子要滿足以上的7點。

定義屬性

    private static final int DEFAULT_BG_OUTER = 0xfffde399; // 外部邊框的背景顏色
    private static final String DEFAULT_WHITE = "#fffefd";
    private static final int DEFAULT_BG_INNER = 0xffffa800;  //內部進度條的顏色
    private static final String DEFAULT_BG_FAN = "#fcce5b";  // 風扇 扇葉的顏色

    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 600;

    //振幅的強度
    private static final int LOW_AMPLITUDE = 0;
    private static final int NORMAL_AMPLITUDE = 1;
    private static final int HIGH_AMPLITUDE = 2;

    private static final int DEFAULT_AMPLITUDE = 20;

    // 葉子飄動一個周期所花的時間
    private static final int LEAF_FLY_TIME = 2000;
    private static final int LEAF_ROTATE_TIME = 2000;

    private Resources mResources;

    // 定義畫筆
    private Paint innerPaint;
    private Paint outerPaint;
    private Paint fanPaint;
    private Paint fanBgPaint;
    private Paint textPaint;

    // view的大小 和 “100%”的高度
    private int mWidth;
    private int mHeight;
    private float textHeight;

    //外部圓半徑 內部圓半徑  風扇背景的半徑
    private float outerRadius;
    private float innerRadius;
    private float fanBgRadius;

    //各種路徑
    private RectF outerCircle;
    private RectF outerRectangle;
    private RectF innerCircle;
    private RectF innerRectangle;
    private RectF fanWhiteRect;

    //電風扇 扇葉路徑
    private Path mPath;
    private Path nPath;

    // 定義結束的屬性動畫
    private ValueAnimator progressAnimator;
    private ValueAnimator completedAnimator;

    //進度值
    private float maxProgress = 100;
    private float currentProgress;
    private float completedProgress;

    //計算時間增量和progress增量
    private long preTime ;
    private long addTime;
    private float addProgress;
    private float preProgress;

    //先填充半圓的進度 和 長方形的時間
    private float firstStepTime;
    private float secondStepTime;

    //和葉片相關
    private Bitmap mLeafBitmap;
    private int mLeafWidth;
    private int mLeafHeight;
    private int mLeafFlyTime = LEAF_FLY_TIME;
    private int mLeafRotateTime = LEAF_ROTATE_TIME;
    private int mAddTime;
    private float mAmplitudeDisparity = DEFAULT_AMPLITUDE;

    //判斷是否加載完畢 然后執行結束動畫
    private boolean isFinished;

    //精度條的總長度
    private float mProgressWidth;

    private List<Leaf> leafInfos;

    //對 外面的邊框緩存
    private WeakReference<Bitmap> outBorderBitmapCache;

這里定義的屬性比較多,但是還是都通熟易懂的。

OnDraw()

我們這先看一下onDraw方法吧,整個的繪制流程是都放生在這個方法里面。我們先梳理一下繪制的流程,具體畫每個圖形后面我會詳細講解。

protected void onDraw(Canvas canvas) {

        //判斷背景有沒有緩存(這里的背景是指,黃色進度條外面的邊框)
        Bitmap outBorderBitmap = outBorderBitmapCache == null ? null : outBorderBitmapCache.get();

        if (outBorderBitmap == null || outBorderBitmap.isRecycled()) {
            outBorderBitmap = getBitmap();
            outBorderBitmapCache = new WeakReference<Bitmap>(outBorderBitmap);
        }
  
        //對畫布保存主要是要用Xfermode對圖像處理,主要是不想讓葉子飛出邊界
        //如果不了解Xfermode的同學建議先去看一下,很有用的一個東西
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null, Canvas.MATRIX_SAVE_FLAG |
                Canvas.CLIP_SAVE_FLAG |
                Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
                Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
                Canvas.CLIP_TO_LAYER_SAVE_FLAG);

        canvas.drawBitmap(outBorderBitmap, 0, 0, outerPaint);
        //        canvas.translate(mWidth / 10, mHeight / 2);

        //畫葉子
        drawLeaf(canvas);
        //恢復畫布
        canvas.restoreToCount(sc);

        canvas.translate(mWidth / 10, mHeight / 2);

        //畫內部圓
        drawInnerCircle(canvas);

        //畫風扇白色的背景
        canvas.drawArc(fanWhiteRect, 90, 360, true, fanPaint);

        //畫風扇的黃色背景
        canvas.save();
        canvas.scale(0.9f, 0.9f, 8 * outerRadius, 0);
        canvas.drawArc(fanWhiteRect, 90, 360, true, fanBgPaint);
        canvas.restore();

        //畫扇葉
        canvas.save();
        drawFan(canvas, true);
        canvas.restore();

        //結束動畫
        //結束動畫是指 電風扇的扇葉從扇葉變成100%字樣
        if (isFinished) {
            showCompletedText(canvas);
        } else {
            //這里重新繪制 主要是為了畫葉子
            invalidate();
        }

    }

首先我們先說一下畫背景(這里的背景指的是進度條外面的邊框)

先看一下具體實現

public Bitmap getBitmap() {
          //這里先產生一個一個畫布,畫布的大小就是view的大小
        Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight,Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.translate(mWidth / 10, mHeight / 2);

        canvas.drawArc(outerCircle, 90, 180, true, outerPaint);
        canvas.drawRect(outerRectangle, outerPaint);
        return bitmap;
    }

這里我們讓背景作為bitmap返回主要是 要使用Xfermode方法。先移動坐標系到我們想要的位置,然后背景是由一個左半圓和一個矩形組成。至于為什么要使用Xfermode,這里先說明一下,我們期望葉子是不可以飄出背景外的。(就是說葉子飄出背景外的地方要變成透明的)。

畫進度條

    //先填充半圓
    private void drawInnerCircle(Canvas canvas) {
        firstStepProgress = innerRadius / (innerRadius + 7 * outerRadius);
        if (currentProgress > firstStepProgress) {
            canvas.drawArc(innerCircle, 90, 180, true, innerPaint);
            drawInnerRectangle(canvas);
        } else {
            //這里就是繪制半圓的執行(方法是繪制圓弧)
            canvas.drawArc(innerCircle, 180 - 90 * currentProgress / firstStepTime, 180 * currentProgress / firstStepTime, false, innerPaint);
        }
    }

    //填充剩下的長方形
    private void drawInnerRectangle(Canvas canvas) {
        secondStepProgress = 1 - firstStepProgress;
        //判斷是否結束,結束了會執行結束動畫
        if (currentProgress >= 1) {
            if (!isFinished) {
                isFinished = true;
                completedAnimator.start();
            }
        } else {
            canvas.drawRect(-1, -innerRadius, 7 * outerRadius * (currentProgress - firstStepProgress) / secondStepProgress, innerRadius, innerPaint);

        }
    }

進度條和背景是一樣的,都是先都前半圓和一個矩形組成的。先計算半圓所占進度,當currentProgress沒有超過firstStepProgress時候,先繪制半圓部分,之后繪制矩形。

繪制風扇

參數分別是 canvas 畫布,isNeedRotate 風扇是否旋轉。

    //畫扇葉
    private void drawFan(Canvas canvas, boolean isNeedRotate) {
        canvas.save();
        //加載的時候旋轉風扇,負數是逆時針旋轉,默認旋轉5圈
        if (isNeedRotate) {
            canvas.rotate(-currentProgress * 360 * 5, 8 * outerRadius, 0);
        }
        //結束動畫時候需要不斷的縮小風扇,然后“100%”從小變大
        if (completedProgress != 0) {
            canvas.scale(1 - completedProgress, 1 - completedProgress, 8 * outerRadius, 0);
        }
        //旋轉畫扇葉,扇葉使用path繪制的
        for (float i = 0; i <= 270; i = i + 90) {
            canvas.rotate(i, 8 * outerRadius, 0);
            canvas.drawPath(mPath, fanPaint);
        }
        //這個是風扇中間的小點
        canvas.drawCircle(8 * outerRadius, 0, 5 * (1 - completedProgress), fanPaint);
        canvas.restore();
    }

繪制結束動畫

結束動畫 這里我們用的是屬性動畫提供的0-1的值實現的。這個過程主要是把進度條補齊以及風扇消失,然后“100%”字樣顯示。

    //結束時動畫 展示“100%”字樣
    private void showCompletedText(Canvas canvas) {
        //補齊進度條
        canvas.drawRect(-1, -innerRadius, (7 + completedProgress) * outerRadius, innerRadius, innerPaint);
        canvas.drawArc(fanWhiteRect, 90, 360, true, fanPaint);
           
        //繪制風扇的背景
        canvas.save();
        canvas.scale(0.9f, 0.9f, 8 * outerRadius, 0);
        canvas.drawArc(fanWhiteRect, 90, 360, true, fanBgPaint);
        canvas.restore();
        
        if (completedProgress == 1) {
            textPaint.setTextSize(60);
            canvas.drawText("100%", 8 * outerRadius, textHeight, textPaint);
        } else {
            drawFan(canvas, completedProgress, false);
            textPaint.setTextSize(60 * completedProgress);
            canvas.drawText("100%", 8 * outerRadius, textHeight, textPaint);
        }

    }

繪制葉子

因為葉子是一直在飄蕩的,這里利用系統的時間,來計算葉子的坐標。


private class Leaf {
        // 在繪制部分的位置
        float x, y;
        // 控制葉子飄動的幅度
        int type;
        // 旋轉角度
        int rotateAngle;
        // 旋轉方向--0代表順時針,1代表逆時針
        int rotateDirection;
        // 起始時間(ms)
        long startTime;
    }

    /**
     * 畫葉子
     */
    private void drawLeaf(Canvas canvas) {

        long currentTime = System.currentTimeMillis();
        canvas.save();
        //這里進行了 一次畫布平移
        canvas.translate(mWidth / 10 - innerRadius, mHeight / 2 - outerRadius);
        for (Leaf leaf : leafInfos) {
            //如果系統當前的時間大于葉子開始繪制的時間,就去獲取葉子的坐標
            if (currentTime > leaf.startTime && leaf.startTime != 0) {
                getLocation(leaf, currentTime);
                // 通過時間關聯旋轉角度,則可以直接通過修改LEAF_ROTATE_TIME調節葉子旋轉快慢
                float rotateFraction = ((currentTime - leaf.startTime) % mLeafRotateTime)
                        / (float) mLeafRotateTime;
                int angle = (int) (rotateFraction * 360);
                int rotate = leaf.rotateDirection == 0 ? angle + leaf.rotateAngle : -angle
                        + leaf.rotateAngle;
                //用矩陣進行坐標轉換
                Matrix matrix = new Matrix();
                matrix.reset();
                matrix.postTranslate(leaf.x, leaf.y);

                matrix.postRotate(rotate, leaf.x + mLeafWidth / 2, leaf.y + mLeafHeight / 2);
                //對畫筆設置Xfermode
                outerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
                canvas.drawBitmap(mLeafBitmap, matrix, outerPaint);
                outerPaint.setXfermode(null);
            } else {
                continue;
            }

        }
        canvas.restore();
    }

    //獲取葉子當前的位置
    public void getLocation(Leaf leaf, long currentTime) {
        //計算當前的時間和葉子繪制的時間的差值
        long intervalTime = currentTime - leaf.startTime;
        if (intervalTime < 0) {
            //不對此片葉子進行繪制,還沒到它出場的時間
            return;
        } else if (intervalTime > mLeafFlyTime) {
            //重置葉子的出場時間
            leaf.startTime = System.currentTimeMillis()
                    + new Random().nextInt(mLeafFlyTime);
        }
        float fraction = (float) intervalTime / mLeafFlyTime;
        leaf.x = getLeafX(fraction);
        leaf.y = getLeafY(leaf);
    }

    //獲取葉子x坐標
    public float getLeafX(float fraction) {
        return mProgressWidth * (1 - fraction);
    }

    //獲取葉子y坐標,用到sin函數,多處用到random是為了讓葉子顯的更加自然
    public float getLeafY(Leaf leaf) {
        float w = (float) (2 * Math.PI / mProgressWidth);
        float a = outerRadius / 2;
        switch (leaf.type) {
            case LOW_AMPLITUDE:
                // 小振幅 = 中等振幅 - 振幅差
                a = -mAmplitudeDisparity;
                break;
            case NORMAL_AMPLITUDE:
                break;
            case HIGH_AMPLITUDE:
                // 小振幅 = 中等振幅 + 振幅差
                a = +mAmplitudeDisparity;
                break;
            default:
                break;
        }

        return (float) (a * Math.sin((w * leaf.x))) - mLeafHeight / 2 + outerRadius;
    }



最后放上效果圖

可能看著和原著有點.......,嘿嘿,原諒我沒有進行優化,大家看看思路就可以了。代碼地址

本文參考了一個絢麗的loading動效分析與實現!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,761評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,177評論 4 61
  • 轉自 http://blog.csdn.net/shimiso** 前言:**介于許多人對項目經理這個職位的陌生和...
    酒紅色的小貓一閱讀 518評論 0 4
  • 貓大人的一個“好消息”:我被選上班級圖書管理員啦! 此處鼓掌三百下! 嘿嘿,老子也是圖書管理員哦~博覽群書之后寫出...
    瞅瞅君與貓大人閱讀 1,011評論 0 2
  • 自古以來,中國的謙卑文化教育我們說:謙虛使人進步,驕傲使人落后。讓我們一直活在一種壓抑的情緒里。明明心里很開心,卻...
    呂桂平閱讀 1,193評論 0 0