用Canvas和屬性動畫造一隻萌蠢的“小鬼”

最近沒事的時候想自己寫一個支持下拉刷新,上拉加載的自定義View。寫著寫著,就覺得最常見的“一個圈轉啊轉”的進度條太普通瞭。於是,就想看看有沒有更有趣的一點的加載效果。在GitHub上以”android loading”為關鍵字一搜索,就發現有作者開源瞭這麼一個庫:

這裡寫圖片描述

庫的地址是:https://github.com/ldoublem/LoadingView。裡面提供瞭很多有趣的加載動畫(非常棒),個人對其中如下一個效果產生瞭興趣:

這裡寫圖片描述

那麼,開源的好處就來瞭,立刻打開源碼瞧一瞧別人是怎麼實現的吧。一看發現沒有借助任何圖片,而就是通過canvas配合屬性動畫完成的整個效果。
按理說別人造好的輪子,我們直接拿來用就好瞭。但既然感興趣,為什麼不學習一下別人的思路,自己也來實現一個,從而得到提高呢?所以,綜合一想,自己也重新來畫一畫這個萌蠢萌蠢的小鬼吧。並通過此文來總結一下整個自定義view的思路和收獲。
(P.S:會借鑒原作者的思路,但具體實現細節會有不同,但思路當然才是最重要的,具體實現選擇自己喜歡的就好)


自定義View的建立

其實說起繪畫,就想起瞭小時候流行的一個口訣,是畫“丁老頭”的,印象中有“一個丁老頭兒,借我倆煤球兒,我說三天還,他說四天還..”之類的。其實就是這樣的,如果猛的一下讓我們畫個“老頭兒”出來,我們可能會有點懵逼。但按照口訣那樣一部分一部分的畫,似乎就變得容易多瞭。

所以,我們也可以模仿這個思路來畫這個小鬼。我們簡單分析一下,可以發現這個小鬼的構成其實就是:頭 + 眼睛 + 身體 + 影子。那麼,還等什麼呢?趕緊按照這個思路“開畫”吧!首先,我們當然是新建一個類,並讓其繼承View,而名字的話就叫GhostView好瞭。


onMeasure()

在正式開始“作畫”之前,我們肯定是做好相關的準備工作。比如,先確定好要用多大尺寸的“畫紙”。哈哈,其實也就是完成View的measure工作。我們知道自定義View的時候,如果使用默認的onMeasure()方法:WRAP_CONTENT也會被當做MATCH_PARENT來測量,所以其實要做的也很簡單:



    

這裡寫圖片描述

。。。。。。。。好吧,目前為止我們還看不到任何“萌蠢小鬼”的跡象。沒關系,一步一步的來。vcD4NCjxociAvPg0KPGgyIGlkPQ==”誰說鬼就沒影子”>誰說“鬼”就沒影子

有瞭小鬼的頭之後,我們接著做什麼呢?正常來說我們應該想著接著畫身體。但是從之前的效果圖我們可以看到,小鬼的底部是有一個影子的。所以,個人選擇先畫這個影子。因為:影子位於View的底部,先完成影子的繪畫,之後更方面我們確定小鬼身體的高度和位置。

其實所謂的影子也非常的簡單,就是一個灰蒙蒙的“橢圓形”而已:

    // 影子所占區域
    private RectF mRectShadow;
    // 小鬼身體和影子之間的舉例
    private int paddingShadow;

    private void drawShadow(Canvas canvas) {
        paddingShadow = mHeight / 10;
        mRectShadow = new RectF();
        mRectShadow.top = mHeight * 8 / 10;
        mRectShadow.bottom = mHeight * 9 / 10;
        mRectShadow.left = mWidth / 4;
        mRectShadow.right = mWidth * 3 / 4;
        canvas.drawArc(mRectShadow, 0, 360, false, mShadowPaint);
    }

這個時候,我們再來看一看效果變成瞭什麼樣子:

這裡寫圖片描述


重頭戲,加上身體

現在,我們就來到瞭最關鍵的部分瞭:為小鬼加上身體。其實總的來說,小鬼的身體就是在頭部大約半圓的位置,分別畫上兩條帶有弧度的延長線。但是,怎麼才能讓小鬼身體的這兩條線與頭部比較完美的融合呢?原作者在這裡使用瞭一些正弦、餘弦的公式來計算圓的弧度,從而完成瞭需要。然而,悔不及當初沒有好好念書啊,患上瞭暈“數學公式”的病。所以我機智的選擇用另一種方法,雖然沒那麼高大上,但是比較簡單。就像下面這樣:

    private Path mPath = new Path();
    // 小鬼身體胖過頭部的寬度
    private int mGhostBodyWSpace;

    private void drawBody(Canvas canvas) {
        mGhostBodyWSpace = mHeadRadius * 2 / 15;
        // 先畫右邊的身體
        mPath.moveTo(mHeadLeftX, mHeadCentreY);
        mPath.lineTo(mHeadRightX, mHeadCentreY);
        mPath.quadTo(mHeadRightX + mGhostBodyWSpace, mRectShadow.top - paddingShadow,
                     mHeadRightX - mGhostBodyWSpace, mRectShadow.top - paddingShadow);

        canvas.drawPath(mPath,mBodyPaint);
    }

這裡寫圖片描述

上圖中左邊的部分就是我們目前為止得到的效果;而右邊就是通過把畫筆設置為stroke來解釋這樣做的原理,實際上就是:先通過lineTo在小鬼頭部的中間畫一條直徑,這個時候path的LastPoint就到瞭最右邊的這個點,然後我們從這個點在右邊向下畫一條二階貝塞爾曲線,就有瞭小鬼右邊身體的輪廓瞭。

那麼接著我們該做什麼呢?回憶一下,我們發現小鬼的身體下方是有“波紋”的,就想裙子的褶皺一樣,所以我們現在就給添上裙子。其實原理仍然很簡單,這個時候path的LastPoint也已經移動到瞭小鬼右邊身體的下面,我們從這裡開始向左不斷畫多個貝塞爾曲線形成裙褶就行瞭:

    // 單個裙褶的寬高
    private int mSkirtWidth, mSkirtHeight;
    // 裙褶的個數
    private int mSkirtCount = 7;

    private void drawBody(Canvas canvas) {
        mGhostBodyWSpace = mHeadRadius * 2 / 15;
        mSkirtWidth = (mHeadRadius * 2 - mGhostBodyWSpace * 2) / mSkirtCount;
        mSkirtHeight = mHeight / 16;

        // ......

        // 從右向左畫裙褶
        for (int i = 1; i <= mSkirtCount; i++) {
            if (i % 2 != 0) {
                mPath.quadTo(mHeadRightX - mGhostBodyWSpace - mSkirtWidth * i + (mSkirtWidth / 2), mRectShadow.top - paddingShadow - mSkirtHeight,
                        mHeadRightX - mGhostBodyWSpace - (mSkirtWidth * i), mRectShadow.top - paddingShadow);
            } else {
                mPath.quadTo(mHeadRightX - mGhostBodyWSpace - mSkirtWidth * i + (mSkirtWidth / 2), mRectShadow.top - paddingShadow + mSkirtHeight,
                        mHeadRightX - mGhostBodyWSpace - (mSkirtWidth * i), mRectShadow.top - paddingShadow);
            }
        }

        canvas.drawPath(mPath,mBodyPaint);
    }

這裡寫圖片描述

可以看到到瞭現在,基本就能看見整個小鬼的輪廓瞭,但我們註意到小鬼左邊似乎有點僵硬。沒關系,我們也給他加上一點對應的弧度就行瞭:

        mPath.quadTo(mHeadLeftX - mGhostBodyWSpace, mRectShadow.top - paddingShadow, mHeadLeftX, mHeadCentreY);

這裡寫圖片描述


畫“鬼”點睛

到瞭現在,我們的繪圖工作其實基本就已經完成瞭。但眼睛是心靈的窗戶,少瞭眼睛,這個小鬼看上去有點四不像的感覺。趕緊加上眼睛吧!

同樣的,眼睛的繪制其實也非常簡單,就在先要的位置,畫上兩個黑色的小圓就可以瞭:

    private void drawEyes(Canvas canvas) {
        canvas.drawCircle(mHeadCentreX , mHeadCentreY, mHeadRadius / 6, mEyesPaint);
        canvas.drawCircle(mHeadCentreX + mHeadRadius / 2, mHeadCentreY, mHeadRadius / 6, mEyesPaint);
    }

現在我們所有的繪制工作就完成瞭,把之前粉紅色的背景顏色去掉,再看看效果,是不是有點呆萌的趕腳瞭呢?

這裡寫圖片描述


讓小鬼動起來

現在小鬼我們已經畫完瞭,剩下的工作自然就是讓它動起來,別死氣沉沉的。而我們已經知道瞭,這個工作就是通過屬性動畫來完成的。

那麼,我們可以添加一個最簡單的位移動畫,比如說這樣做:

    private void startAnim(){
        ObjectAnimator animator = ObjectAnimator.ofFloat(this,"translationX",0,500);
        animator.setRepeatMode(ObjectAnimator.RESTART);
        animator.setRepeatCount(ObjectAnimator.INFINITE);
        animator.setDuration(5000);
        animator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // ......
        startAnim();
    }

這裡寫圖片描述

可以看到這樣“小鬼”就已經動起來瞭,不過現在它肯定沒有那麼萌瞭。因為它的行進路徑和恐怖片裡那些白衣幽靈看上去一樣一樣的。不過這裡主要是表達個意思嘛,要實現作者原本的那個動畫效果實際上也不難,我們分析一下可以發現它主要有幾個動作:就是小鬼在行進的同時還會上下跳動,並且底部的影子會隨著小鬼跳起和落下而改變大小,那麼我們就可以借助ValueAnimator來實現。
簡單來說,要做的工作就是之前描繪小鬼時的相關屬性(例如小鬼的頭部的圓心坐標,影子的rect的寬度等)不要寫死,而是與某個值產生關聯。然後我們用ValueAnimator來監聽和不斷的改變這個值,然後讓view不斷重繪,就可以得到響應的一些動畫效果瞭。

You May Also Like