Android TV使用貝賽爾曲線製作炫酷的開場動畫

2020-10-06 16:00:20

目錄

前言

效果展示

動畫一

動畫一效果

貝賽爾曲線

程式碼範例

動畫二

動畫二效果

時差動畫

物件導向繪製

總結


前言

很多App啟動的時候會用到炫酷的開場動畫。Android TV端也一樣,每一個不同的模組,產品經理都可能設計了不同的開場動畫。對於這些複雜的開場動畫,最重要的是學會拆分,只要拆分得當,就會變成一個個普通的動畫組合而成。今天給大家介紹一個曾經在專案中使用過的炫酷的開場動畫。

效果展示

上面的圖片中展示了一個曾在專案中使用的開場動畫,由於專案還還線上,所以僅僅截圖了其中一部分的動畫。對於這樣一個動畫,我們可以選擇放一張GIF圖片,但是對於每秒60幀的安卓來說,GIF的效果真的很難讓人滿意,而且GIF貌似不能做透明背景,這樣一來要做組合動畫就難了。所以我們不得不考慮用原生技術來解決這樣一個動畫效果。

這個動畫可以拆分成三個動畫,左邊紅色氣球一直左右搖擺,像是在風中搖曳。右邊一團紫色氣球上升,像是有人放飛了一簇氣球。紫色氣球飛完之後有一個橫幅逐漸展示。 最後橫幅的展示僅僅是個屬性動畫,今天不多做介紹,今天主要介紹前兩個動畫。

動畫一

動畫一效果

首先我們把第一個動畫拆分出來,單獨展示的話,它是這樣的:

這個動畫看似還比較簡單,我第一印象也是這樣,不就是一個氣球左右移動嗎?使用屬性動畫或者幀動畫都可以完成吧。實際操作中發現,下面的繩子是主要難點,繩子一端固定,一端隨氣球移動,而且繩子是曲線不是直線,這就排除了用屬性動畫移動氣球的解決方案。幀動畫理論上是可以的,只要分出足夠多幀就行,但是想要這麼如絲般潤滑,幀數一定不少,幀數越多圖片就越多,app體積就越大。筆者採用的方法是一張圖片搞定,上面的氣球是一張圖片,左右移動,下面的繩子直接畫出來。這裡就需要用到貝塞爾曲線。

貝賽爾曲線

貝塞爾曲線於1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來為汽車的主體進行設計。貝塞爾曲線最初由 Paul de Casteljau 於1959年運用de Casteljau 演演算法開發,以穩定數值的方法求出貝塞爾曲線。

需用到Path方法:

mPath.moveTo     移動到操作起始點座標,不會進行繪製,只用於移動移動畫筆

mPath.lineTo        從一個點連線到另一個點,用於進行直線繪製

mPath.quadTo(x1, y1, x2, y2)       生成二次貝塞爾曲線,(x1,y1) 為控制點,(x2,y2)為結束點

mPath.cubicTo(x1, y1, x2, y2, x3, y3)       生成三次貝塞爾曲線, (x1,y1) 為控制點,(x2,y2)為控制點,(x3,y3) 為結束點;即與二階區別多一個控制點

更多關於貝塞爾曲線的知識可以參考這篇文章:Android自定義View:Path之貝塞爾曲線

程式碼範例

只要把思路理清楚了,程式碼就比較簡單了,下面程式碼是畫氣球和曲線的程式碼:

canvas.drawBitmap(mBitmap, mLeft, 0, mPaint);

mEnd.x = mLeft + DisplayUtil.dip2px(getContext(),13);
mEnd.y = mBitmap.getHeight();

mControl.x = mEnd.x;
mControl.y = mStart.y - DisplayUtil.dip2px(getContext(),5);

mPath.reset();
mPath.moveTo(mStart.x, mStart.y);
mPath.quadTo(mControl.x, mControl.y, mEnd.x, mEnd.y);

canvas.drawPath(mPath, mPaint);

動畫二

動畫二效果

我們把第二個動畫的效果也拆分出來:

其實這個動畫相對簡單,途中總共有14個氣球,我們只要搞清楚每個氣球的軌跡,分別在各自的軌跡中繪製出來即可。

時差動畫

這裡面每個氣球不是同時啟動的,有的啟動早有的啟動晚,形成了一個時差效果。所以我們在設計這個動畫的時候需要考慮每個氣球的啟動時間,每個氣球可以延遲不同的時間啟動。

物件導向繪製

要同時繪製14個氣球,每個氣球起點、終點、啟動時間等都不同。考慮到這些因素,我們必須以物件導向的思路來繪製。如果使用android做過一些小遊戲的話,應該不難理解這個思路。比如做一個坦克大戰或者雷電遊戲,每顆子彈的飛行軌跡也不一樣,我們也是把子彈設計成一個類,傳入起點、終點等屬性,讓子彈自己去完成繪製的。

下面是我抽象出來的氣球類:

    private class Balloon {
        Bitmap bitmap;

        float endX;

        float endY;

        int delay;

        void onDraw(Canvas canvas, int currTime, int width, int height) {
            if (currTime < delay) {
                return;
            }
            float startX = (width - bitmap.getWidth()) / 2;
            float startY = height - bitmap.getHeight();
            int duration = DURATION;
            currTime = currTime - delay;
            if (currTime > DURATION) {
                currTime = DURATION;
            }
            float speedX = (endX - startX) / duration;
            float speedY = (endY - startY) / duration;

            float currX = startX + speedX * currTime;
            float currY = startY + speedY * currTime;
            canvas.drawBitmap(bitmap, currX, currY, null);
        }
    }

使用的時候初始化不同的氣球物件:

        mBalloons = new ArrayList<>();
        Balloon balloon1 = new Balloon();
        balloon1.bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.balloon_1);
        balloon1.delay = 0;
        balloon1.endX = 0;
        balloon1.endY = DisplayUtil.dip2px(getContext(), 45);
        mBalloons.add(balloon1);

        Balloon balloon2 = new Balloon();
        balloon2.bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.balloon_2);
        balloon2.delay = 50;
        balloon2.endX = DisplayUtil.dip2px(getContext(),195);
        balloon2.endY = DisplayUtil.dip2px(getContext(),67);
        mBalloons.add(balloon2);

最終繪製的時候,在view的onDraw方法中呼叫氣球的onDraw方法:

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

        for (Balloon balloon : mBalloons) {
            balloon.onDraw(canvas, mCurrTime, getWidth(), getHeight());
        }
    }

總結

工作中面對這樣的複雜動畫,不要被嚇住。再複雜的動畫我們拆分成一個個的小動畫之後會發現自己其實是可以實現的。

完整程式碼:GitHub地址,由於像這樣的動畫往往不具有通用性,所以沒辦法做成一個庫來發布,感興趣的小夥伴版可以直接看原始碼,大家相互交流學習。