來一份Android動畫全家桶

crazysunj發表於2018-05-20

前言

自上次《MTRVA2.0來啦》釋出後,馬上就有小夥伴問我有哪些Android動畫,過了一段時間又有小夥伴問我啥時候釋出Android動畫。其實,在寫《MTRVA2.0來啦》的時候,這次要講的Android動畫已經完成的差不多了,而在寫這篇文章的時候,下個版本的內容也快寫的差不多了(捂臉)。想提前學習的同學可以去我的開源專案CrazyDaily的develop分支,然後再跟我的文章過一遍,挺不錯的學習方法。廢話不多說,Android動畫似乎已經是老生常談的技術點,老生常談的技術感覺總是潛移默化成為Android程式設計師的必備技能。今天,大家再跟我一起過一遍Android動畫及進階使用。

來一份Android動畫全家桶

效果圖

國際慣例,No picture,say a J8!

來一份Android動畫全家桶

先來看看今天講解的內容吧!

  • lottie
  • 3D動畫
  • 列表側滑刪除
  • 列表展開閉合
  • 轉場動畫進階
  • 衛星選單

一個大類可能包含多種動畫,比如轉場動畫用到了貝塞爾曲線的路徑動畫。再則說到Android動畫總是避免不了扯到View,所以期間會帶大家玩一玩自定義View。

高能預警:本篇較長,可以先選擇自己喜歡的內容進行閱讀或者收藏一下有空再看。

溫馨提示:想看針對第五章節額外附送的屬性動畫原始碼分析請點這裡

分析

這次的分析隨著效果圖的展示順序一一講解,首先,向我們迎面走來的是...額,不是,首先,是我們的可能是動畫趨勢的lottie動畫。

lottie

簡介

lottie可能是一個革命性的動畫庫。為什麼這麼說?當然也只是我想這麼說,先來看看lottie的震撼特點:

  • 跨平臺,支援Web、Android、iOS和React Native
  • 線上更新動畫,意味著不用發版和減少資源的佔用體積等
  • 相對於原生動畫和GIF更為輕量
  • 程式碼實現簡單,易於上手和維護

我們再來看看這樣的一個效果圖(官方效果圖):

來一份Android動畫全家桶

這麼說吧,這種效果原生肯定是能寫的,但是非常費腦子和精力,不信的同學可以嘗試一下。其次用幀動畫,缺點也很明顯,資源佔用很大。最簡單的就是一張GIF圖片,沒有什麼動畫是GIF搞不定的(手動滑稽),但這應該是最差的方案了。

而lottie只要一份記錄動畫資訊的json檔案,就可以在各大平臺跑起來。是不是很炫酷?

Android

So Easy!除了這個詞我還真沒想到怎麼形容。廢話不多說,先上程式碼:

implementation 'com.airbnb.android:lottie:2.5.4' //gradle依賴

<com.airbnb.lottie.LottieAnimationView
        android:id="@+id/splash_lottie"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/splash_height"
        app:lottie_autoPlay="true"
        app:lottie_fileName="lottie/mtrva.json"
        app:lottie_imageAssetsFolder="images/mtrva/" />
複製程式碼

是的,就是這麼簡單,只要把你的json檔案傳給LottieAnimationView,它就可以流暢地播放起來,像如何在程式碼中使用及其它我就不一一貼出來了,大家可以點這裡

擴充套件

這才是重頭戲,面試的時候說自己會實現複雜的動畫可能是個賣點,但現在似乎這個工作可以被設計師取代,我們先不談什麼級別的設計師,我們先來說說為啥我們Android程式設計師不可以來完成這份工作呢?跟我搶飯碗?不可能!

正如專案中Splash頁面底下文字[什麼都懂一點,生活更多彩一些][廋金體],感興趣就玩一玩,真的很有意思!

簡單分析一下,Splash頁面的動畫,logo圖示沿著s型的路徑,淡入,放大。很簡單的一個動畫,原生實現也是很簡單的。重點在於如何開發這樣的lottie動畫,只需要Adobe After Effects+bodymovin就可以輕鬆匯出一份這樣的json檔案。而詳細的安裝及環境配置可以點這裡,不過英文要好哦,不然只能看看蹩腳的翻譯。

簡單的開發可以很快入門,我剛玩了一會兒,就開發了這樣的一個效果:

來一份Android動畫全家桶

很可惜,好像匯出在Android端執行不太相容,沒有達到預期的效果,可能是我使用姿勢不對。就玩了一會兒,弊端就體現出來了,這也是各種跨端的弊端,相容性問題。一個庫開源,使用者總是避免不了踩坑,例如上面xml中lottie_imageAssetsFolder屬性新增是因為json中有圖片資源,需要圖片資源的路徑且圖片資源名要改為image_0,具體原因可以開啟json檔案瞧一瞧。

那麼有同學問有沒有什麼現成的json嗎?AE這玩意好像學起來挺麻煩的,這個肯定有,lottiefiles挺不錯的一個網站,點選preview可以拖拽你的json檔案進行預覽和簡單地編輯。

關於lottie動畫介紹差不多就到這裡,關鍵點都說了,剩下的可能是你的AE熟練度了。這玩意沒法速成,需要大量經驗積累!難點並不是技術,而是創意!創意!創意!

來一份Android動畫全家桶

3D動畫

進入首頁,最先刺激我眼球的是右下角的妹子,其次是它的動畫,這樣的效果我最先在百度貼吧上看到的,現在好像去掉了,這也是我第一家公司面試的時候的作品,很有親切感!

我們簡單地拆解一下動畫,可以分為這些:3D翻轉、平移、陰影漸變和vignette。

3D翻轉

這個動畫的核心,用的是補間動畫,你也可以植入view中,我相信這對你來說並不難。

拆解動畫:view分為兩面,一面翻上去或者一面翻下去,然後展示另一面,所以分為4個動畫TopInAnimation、TopOutAnimation、BottomInAnimation和BottomOutAnimation,Top和Bottom為反向操作,因此這裡只分析Top。

如果沒圖,我猜有些同學不好理解,這裡給出一張中間單幀圖,畫得不好,諒解,諒解。

來一份Android動畫全家桶

不知道大家瞭解setRotationX這個方法嗎,不清楚的可以點這裡。最常見就是我們的車輪,車軸就是X軸,然後側著看。這樣一想,是不是A和B都在做rotationX動畫,但這是不夠的,假如A面繞的X軸是高度中間等分線,直到A消失,也是消失在等分線的位置,腦補一下,而事實是A消失於頂部水平線,因此得做平移動畫,也就是一邊rotationX一邊translationY。

瞭解這個後,我們再來了解兩個核心類CameraMatrix。篇幅原因,只給出連結,大家可以深入瞭解,其實就算整片文章都介紹,那也說不完。這裡說一下Camera的主要作用是將3D模型投影在2D的螢幕上,而Matrix主要通過一個3x3的矩陣對影像進行平移、旋轉、縮放和錯切變化。

在上程式碼之前,補充一個知識,左右手座標系。

來一份Android動畫全家桶

Camera是基於左手座標系的,它也應該是基於OpenGL的,OpenGL貌似是右手座標系,而Android螢幕座標系的Y軸方向正好與Camera的Y軸方向相反。

來一份Android動畫全家桶

Camera比較好理解,你可以想象攝影大哥扛著攝像機對著螢幕左上角(原點),這個挺形象吧!Camera的預設位置是(0,0,-8),單位是英寸。Matrix相對比較複雜一點,大家可以看看這篇文章Android中影像變換Matrix的原理、程式碼驗證和應用,這是我早期學習時收藏的文章,優秀!

簡單介紹完這兩大核心類,我們來看看在專案中的運用:

static class TopOutAnimation extends Animation {
	...
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        mCamera.save();
        final float rotate = -DEGREE + DEGREE * interpolatedTime;
        mCamera.rotateX(rotate);
        mCamera.getMatrix(mMatrix);
        mCamera.restore();
        mMatrix.preTranslate(-mWidth / 2, 0);
        mMatrix.postTranslate(mWidth / 2, mHeight - mHeight * interpolatedTime);
        t.getMatrix().postConcat(mMatrix);
    }
}

 static class TopInAnimation extends Animation {
 	...
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        mCamera.save();
        final float rotate = DEGREE * interpolatedTime;
        mCamera.translate(0, -mHeight + interpolatedTime * mHeight, 0);
        mCamera.rotateX(rotate);
        mCamera.getMatrix(mMatrix);
        mCamera.restore();
        mMatrix.preTranslate(-mWidth / 2, -mHeight);
        mMatrix.postTranslate(mWidth / 2, 0);
        t.getMatrix().postConcat(mMatrix);
    }
}
複製程式碼

先來分析一下TopOutAnimation,當interpolatedTime=0的時候,rotate=-90即模型是與螢幕是垂直的,當interpolatedTime=1的時候,rotate=0即正常位置。preTranslate是在旋轉前平移模型的位置,至於為什麼是-mWidth/2和0,原因也很簡單,記住Camera核心作用就行了,就是我上面說的,將3D模型投影在2D的螢幕上,由於Camera的初始位置就是螢幕的原點,做旋轉的時候,投影到畫布的圖形肯定是不正常的,因為不是正向的,那麼只要在camera旋轉前向左平移寬度的一半即為正向,但要在旋轉後回到原來的位置,因此呼叫postTranslate且x值為mWidth/2。我相信這個比較好理解,所以不貼圖了,至於preTranslate的y值是0是因為我們初始化定義的camera的rotateX是-90即與螢幕垂直,是這樣的一個變換過程:

來一份Android動畫全家桶

有同學問,那我可不可一開始就在底部,這樣我postTranslate的時候就不用移動了,從邏輯上一點毛病都沒有,但事實上效果詫異,為什麼呢?因為Matrix操作的是我們的模型而非螢幕,甚至移動的距離是原來的幾倍,還發現變小了,為什麼呢?這很簡單,你離光源越遠,肯定越小,至於為啥移動距離是原來的幾倍,我給你畫張圖:

來一份Android動畫全家桶

來一份Android動畫全家桶

那麼是不是就這一種方法呢?那肯定不是,感興趣的同學可以自己去嘗試,自己動手實踐過才印象最深刻。回到我們討論點,由於我們preTranslate的y值是0使得投影效果在最頂部因此需要最終的view從底部不斷往上偏移,故呼叫postTranslate的y值是mHeight-mHeight*interpolatedTime。

有了TopOutAnimation的基礎分析,我們理解TopInAnimation相對容易一點。

上面的程式碼其實可以改成這樣:

 static class TopInAnimation extends Animation {
 	...
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        mCamera.save();
        final float rotate = DEGREE * interpolatedTime;
        mCamera.rotateX(rotate);
        mCamera.getMatrix(mMatrix);
        mCamera.restore();
        mMatrix.preTranslate(-mWidth / 2, -mHeight);
        mMatrix.postTranslate(mWidth / 2, mHeight - interpolatedTime * mHeight);
        t.getMatrix().postConcat(mMatrix);
    }
}
複製程式碼

那是不是Camera的y軸平移等同於Matrix的postTranslate平移呢?只能說近似等同。這次我直接畫變換過程:

來一份Android動畫全家桶

最後跟TopOutAnimation一樣呼叫postTranslate的平移就行了。

關於3D翻轉就先介紹到這,不懂的可以問我,如果哪裡不對的或者有歧義的地方歡迎指正。

平移

這個就比較簡單了,直接上程式碼:

public void start(boolean isTop, int index) {
        final float distance = -mTranslationYDistance * index;
    if (isTop) {
        mForegroundView.startAnimation(mTopOutAnimation);
        mBackgroundView.startAnimation(mTopInAnimation);
        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f)
                .setDuration(DURATION);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(animation -> {
            final float value = (float) animation.getAnimatedValue();
            final float oppositeValue = 1 - value;
            ...
            setTranslationY(distance * oppositeValue);
        });
        animator.start();
    } else {
        mForegroundView.startAnimation(mBottomInAnimation);
        mBackgroundView.startAnimation(mBottomOutAnimation);
        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f)
                .setDuration(DURATION);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(animation -> {
            final float value = (float) animation.getAnimatedValue();
            final float oppositeValue = 1 - value;
			...
            setTranslationY(distance * value);
        });
        animator.start();
    }
}
複製程式碼

程式碼也給出3D翻轉的呼叫,向上平移的時候向下翻轉,向下平移的時候向上翻轉,平移動畫用的是ValueAnimator,因為我們還需要根據value計算陰影的顏色,一起來看看吧!

陰影漸變

為啥要搞個漸變呢?因為要真,當立方體翻滾的時候,由於光線原因,一部分肯定越來越暗,一部分越來越亮。臥槽,物理這麼6?還行,也就每次考試90來分。

來一份Android動畫全家桶

//top
int foreStartColor = (int) mArgbEvaluator.evaluate(oppositeValue * 0.8f, 0x00000000, 0xff000000);
int foreCenterColor = (int) mArgbEvaluator.evaluate(oppositeValue * 0.4f, 0x00000000, 0xff000000);
int[] foreColors = {foreStartColor, foreCenterColor, 0x00000000};
int backStartColor = (int) mArgbEvaluator.evaluate(value * 0.8f, 0x00000000, 0xff000000);
int backCenterColor = (int) mArgbEvaluator.evaluate(value * 0.4f, 0x00000000, 0xff000000);
int[] backColors = {backStartColor, backCenterColor, 0x00000000};
mForegroundDrawable.setColors(foreColors);
mBackgroundDrawable.setColors(backColors);
//bottom
int foreStartColor = (int) mArgbEvaluator.evaluate(value * 0.8f, 0x00000000, 0xff000000);
int foreCenterColor = (int) mArgbEvaluator.evaluate(value * 0.4f, 0x00000000, 0xff000000);
int[] foreColors = {foreStartColor, foreCenterColor, 0x00000000};
int backStartColor = (int) mArgbEvaluator.evaluate(oppositeValue * 0.8f, 0x00000000, 0xff000000);
int backCenterColor = (int) mArgbEvaluator.evaluate(oppositeValue * 0.4f, 0x00000000, 0xff000000);
int[] backColors = {backStartColor, backCenterColor, 0x00000000};
mForegroundDrawable.setColors(foreColors);
mBackgroundDrawable.setColors(backColors);
複製程式碼

mArgbEvaluator是Android提供的ArgbEvaluator類用來根據[0,1]某個值獲取兩種顏色漸變過程該進度時候的顏色值,好像挺拗口。我們這裡操作的是drawable來實現漸變,那麼drawable是誰的呢?是使用者設定的子view嗎?那肯定不是,不然要被打死。是我們在獲取使用者子view的時候在這基礎上新增了一個ImageView與子view平級,上程式碼:

 @Override
protected void onFinishInflate() {
    super.onFinishInflate();
    ...
    View foregroundView = getChildAt(1);

    FrameLayout foregroundLayout = new FrameLayout(context);
    LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

    removeAllViewsInLayout();
    ...
    ImageView foregroundImg = new ImageView(context);
    ...
    foregroundImg.setImageDrawable(mForegroundDrawable);
    ...
    foregroundLayout.addView(foregroundView);
    foregroundLayout.addView(foregroundImg);

    addViewInLayout(foregroundLayout, 0, params);
    ...
    mForegroundView = (FrameLayout) getChildAt(1);
}
複製程式碼

這裡只給出ForegroundView的程式碼,其它貼出來毫無意義,程式碼也很簡單,在子view外層套一層FrameLayout,然後新增負責陰影漸變的ImageView,漸變效果與我們平移時進度有關。

vignette

之所以加這個,跟我們的陰影漸變一樣,要真。什麼是vignette?vignette大概就是暗角的意思。舉個例子?

來一份Android動畫全家桶

其主要特徵就是在影像四個角有較為顯著的亮度下降。這種效果自己去研究又是一堆公式,好在有大佬提供了方便。這裡我們使用GPUImage來達到這樣的效果,該庫主要提供各種各樣的影像處理濾鏡,並且支援照相機和攝像機的實時濾鏡。很可惜的是這玩意創意來自於iOS。

那我們如何在Android中使用呢?

GPUImageVignetteFilter filter = new GPUImageVignetteFilter();
filter.setVignetteCenter(new PointF(0.5f, 0.5f));
filter.setVignetteColor(new float[]{0.0f, 0.0f, 0.0f});
filter.setVignetteStart(0.0f);
filter.setVignetteEnd(0.75f);
GPUImage gpuImage = new GPUImage(context);
BitmapDrawable bitmapDrawable = (BitmapDrawable) ContextCompat.getDrawable(context, id);
gpuImage.setImage(bitmapDrawable.getBitmap());
gpuImage.setFilter(filter);
Bitmap bitmap = gpuImage.getBitmapWithFilterApplied();
複製程式碼

id是我們的資源id,主要就是要拿到一個bitmap物件,其它還支援file和uri。vignetteCenter就是我們向外擴充套件的起始點,一般在中心,例如我們上面的圖片例子,vignetteColor是暗角的顏色,vignetteStart和vignetteEnd就是漸變程度。

3D動畫是不是So Easy?是不是很炫酷?想不想自己實現一個?

來一份Android動畫全家桶

關於3D動畫先到這結束了,佔用篇幅相對較長,看到這挺有耐心。

列表側滑刪除

long long ago,這個效果貌似是模仿...模仿啥來著?記不起來了,算了,早期的時候我也寫過這個玩意,程式碼找不到了,哈哈,其實挺簡單,主要就是觸控事件的運用,難點可能是使用者體驗,滑起來卡卡的,那玩個毛。

但現在都什麼年代了,實現這種效果更簡單了,Android直接給我們提供了ItemTouchHelper快速實現拖拽和側滑刪除。但需要簡單地修改一下,因為Android提供給我們的好像與預期差了那麼一點點,由於我比較懶且正好看到一個庫itemtouchhelper-extension是針對側滑刪除的,體驗還不錯,但還是沒法滿足我的需求,例如我想使用者關閉介面的時候知道選單是否是開啟狀態。那我只能使用CV大法然後修改,果然最靈活的還是CV原始碼進行修改。

本節主要是想介紹ItemTouchHelper,除了這,Android還提供了很多RecyclerView的幫助類,例如DiffUtil[MTRVA就是基於這個],SnapHelper[幫助我們在某一個地方停住,例如你想嘗試用RecyclerView實現卡片滑動,不妨試試],這兩個比較常用,當然還有其它的哦,大家不妨去看看這個包[androidx.recyclerview.widget]裡面的東西,當然支援包裡面也有,說不定有你想要的。

列表展開閉合

關於這個點貌似挺尷尬的,因為用MTRVA可以輕鬆實現,我都不知道要不要介紹,關鍵有幾個使用者同學會問這個,趁這個機會簡單說一下吧。

比如效果圖中到聯絡人列表有展開閉合的效果,怎麼實現的呢?首先我們要排除直接操作view,比如讓view隱藏等等。這是不可取的,應該用資料去驅動UI,這個我已經強調很多遍了。先看一看程式碼:

private void switchStatus(ContactHeader item, View icon) {
    final int index = mData.indexOf(item);
    final String flag = item.getIndex();
    if (item.isFlod()) {
        icon.animate().rotation(90).setDuration(500).start();
        mHelper.addData(index + 1, item.getChilds());
    } else {
        icon.animate().rotation(-90).setDuration(500).start();
        ...
        childs = mHelper.removeData(index + 1, count);
        item.setChilds(childs);
    }
    item.switchStatus();
    mHelper.setData(index, item);
}
複製程式碼

很簡單的邏輯,如果當前是閉合狀態,icon需要選擇90度且新增資料,反之,旋轉-90度並移除資料。

關鍵點就在於addData和removeData,底層呼叫的是adapter的notifyItemRangeInserted和notifyItemRangeRemoved方法,因此是附帶動畫的,且對陣列越界做了優化,例如addData的position大於或等於集合數量,那麼直接將應新增的資料直接新增在集合的末尾,而不是拋異常。

本節主要希望大家利用新增和移除資料來達到展開閉合的效果。

轉場動畫進階

每次玩Material Design產品的時候,總是有很多炫酷的轉場動畫刺激我。相信也有很多同學跟我一樣,很喜歡這種風格。今天我就帶大家瞭解一下轉場動畫並自定義轉場動畫,走起!

常見轉場動畫

場景動畫核心框架

  • Scene:用來儲存場景應用中View 的屬性集合
  • Transition:負責元素的過渡,你可以在不同場景根據屬性值操作元素打造不同的動畫
    • 普通過渡
      • Explode:從場景中心進入或移除,一種爆炸的感覺
      • Fade:最熟悉的淡入淡出
      • Slide:從場景邊緣進入或移除
      • AutoTransition:預設過渡,Fade+ChangeBounds
    • 共享元素過渡
      • ChangeBounds:根據場景前後佈局界限變化執行過渡動畫
      • ChangeClipBounds:根據場景前後getClipBounds變化執行過渡動畫
      • ChangeImageTransform:根據場景前後ImageView的矩陣變化執行過渡動畫
      • ChangeScroll:根據場景前後目標滾動屬性的變化執行過渡動畫
      • ChangeTransform:根據場景前後檢視縮放和旋轉變化執行過渡動畫,當然也可以根據父檢視的改變
    • 場景切換呼叫[共享元素新增SharedElement]
      • setEnterTransition:A->B,B進入的過渡
      • setExitTransition:A->B,A退出的過渡
      • setReturnTransition:B->A,B返回的過渡
      • setReenterTransition:B->A,A重進的過渡
  • TransitionManager:把上面Scene和Transition結合起來,常見的有通過setTransition(Scene, Transition)結合

實戰

前一小節貌似有點翻譯的味道,但很多都是我親自體驗總結的,同時這也是必不可少的,我們要理論結合實踐,再從實踐中領悟真理。

來一份Android動畫全家桶

場景分析

以聯絡人列表頁為出發點,點選條目跳轉到聯絡人詳情頁。期間發生了什麼呢?

列表頁條目的頭像(其實過渡的時候已經是詳情頁的頭像了)以貝塞爾曲線路徑移動且縮放至詳情頁的頭像;地址和名字平滑過渡到詳情頁;在共享元素過渡的時候,下一個場景也開始了進場動畫,右下角的選單跟小球一樣彈跳下來同時背景以螢幕中心以圓形向外擴散。

程式碼分析

首先我們先分析一下進場動畫,因為這個相對比較好理解。

public class CircularRevealTransition extends Visibility {

	private static final String PROPNAME_ALPHA = "crazysunj:circularReveal:alpha";
	private static final String PROPNAME_RADIUS = "crazysunj:circularReveal:radius";
	private static final String PROPNAME_TRANSLATION_Y = "crazysunj:circularReveal:translationY";
	
	@Override
	public void captureStartValues(TransitionValues transitionValues) {
	    super.captureStartValues(transitionValues);
	    transitionValues.values.put(PROPNAME_ALPHA, 0.2f);
	    final View view = transitionValues.view;
	    transitionValues.values.put(PROPNAME_RADIUS, 0);
	    transitionValues.values.put(PROPNAME_TRANSLATION_Y, -view.getBottom());
	}
	
	@Override
	public void captureEndValues(TransitionValues transitionValues) {
	    super.captureEndValues(transitionValues);
	    transitionValues.values.put(PROPNAME_ALPHA, 1.0f);
	    final View view = transitionValues.view;
	    int radius = (int) Math.hypot(view.getWidth(), view.getHeight());
	    transitionValues.values.put(PROPNAME_RADIUS, radius);
	    transitionValues.values.put(PROPNAME_TRANSLATION_Y, 0);
	}
	
	@Override
	public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
	    if (null == startValues || null == endValues) {
	        return null;
	    }
	    final int id = view.getId();
	    switch (id) {
	        case R.id.satellite_menu:
	            int startTranslationY = (int) startValues.values.get(PROPNAME_TRANSLATION_Y);
	            float startAlpha = (float) startValues.values.get(PROPNAME_ALPHA);
	            int endTranslationY = (int) endValues.values.get(PROPNAME_TRANSLATION_Y);
	            float endAlpha = (float) endValues.values.get(PROPNAME_ALPHA);
	            PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", startTranslationY, endTranslationY);
	            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", startAlpha, endAlpha);
	            ObjectAnimator menuAnim = ObjectAnimator.ofPropertyValuesHolder(view, translationY, alpha);
	            menuAnim.setInterpolator(new BounceInterpolator());
	            menuAnim.setDuration(1000);
	            return menuAnim;
	        case R.id.cool_bg_view:
	            int startRadius = (int) startValues.values.get(PROPNAME_RADIUS);
	            int endRadius = (int) endValues.values.get(PROPNAME_RADIUS);
	            Animator coolAnim = new NoPauseAnimator(ViewAnimationUtils.createCircularReveal(view, view.getWidth() / 2, view.getHeight() / 2, startRadius, endRadius));
	            coolAnim.setDuration(1000);
	            coolAnim.setInterpolator(new AccelerateDecelerateInterpolator());
	            return coolAnim;
	        default:
	            return null;
	    }
	}
	
	@Override
	public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
	  ...
	}
	...
}   
複製程式碼

captureStartValues用來儲存動畫需要的初始值,key-value的形式,至於key可以模仿系統以兩個':'分離,captureEndValues用來儲存動畫需要的結束值。我們這裡,小球透明度從0.2開始,同時從相同x的螢幕頂部掉落,背景初始的半徑為0;結束時小球透明度正常,小球回到老地方,背景擴充套件至滿屏,這裡半徑取了背景寬高的平方和的二次方根,關於值大家可以自己調。

重點是繼承了Visibility,像這種進場動畫最好繼承Visibility,因為它很好地提供了view的出現和消失方法。

我們再來看看onAppear方法,我們利用id來標識一個view,但這樣就不靈活了。我們利用PropertyValuesHolder把translationY和alpha在一個view上同時執行,像這種針對同一個view且需要執行多個屬性動畫,就可以採用PropertyValuesHolder。背景的圓形擴散可以用ViewAnimationUtils來建立,這是Android提供的,必屬精品。傳入操作的view,擴散點座標以及起始和結束半徑。

onDisappear就是一個相反的過程,就不介紹了。我們再來看看共享元素動畫程式碼:

public class BezierTransition extends Transition {
	...
    public BezierTransition() {
        setPathMotion(new PathMotion() {
            @Override
            public Path getPath(float startX, float startY, float endX, float endY) {
                Path path = new Path();
                path.moveTo(startX, startY);
                float controlPointX = (startX + endX) / 4;
                float controlPointY = (startY + endY) * 1.0f / 2;
                path.quadTo(controlPointX, controlPointY, endX, endY);
                return path;
            }
        });
    }

    private void captureValues(TransitionValues transitionValues) {
        Rect rect = new Rect();
        transitionValues.view.getGlobalVisibleRect(rect);
        transitionValues.values.put(PROPNAME_SCREEN_LOCATION, rect);
    }

    @Override
    public void captureStartValues(@NonNull TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(@NonNull TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot,
                                   TransitionValues startValues, TransitionValues endValues) {
        if (null == startValues || null == endValues) {
            return null;
        }
        final int id = startValues.view.getId();
        if (id <= 0) {
            return null;
        }
        Rect startRect = (Rect) startValues.values.get(PROPNAME_SCREEN_LOCATION);
        Rect endRect = (Rect) endValues.values.get(PROPNAME_SCREEN_LOCATION);
        final View view = endValues.view;
        Path path = getPathMotion().getPath(startRect.centerX(), startRect.centerY(), endRect.centerX(), endRect.centerY());
        return ObjectAnimator.ofObject(view, new PropPosition(PointF.class, "position", new PointF(endRect.centerX(), endRect.centerY())), null, path);
    }
    ...
}
複製程式碼

首先發現我們繼承的是Transition,其次在建構函式裡面我們執行了setPathMotion方法。PathMotion是Transition的一種擴充套件,提供以某種路徑運動,有兩個實現類ArcMotionPatternPathMotion,感興趣的可以研究下它的演算法。前者是三階貝塞爾曲線,後者是向量曲線。

我們先來看看我們自己實現的有點low的演算法,哈哈,就是一個簡單的貝塞爾路徑,難點可能是控制點的計算,如何讓路徑更優雅,這個艱鉅的任務就交給你們了。

如果繼承Transition,我們實現的是createAnimator,拿到我們建立的path,通過ObjectAnimator傳入PropPosition實現的。

static class PropPosition extends Property<View, PointF> {
    PointF endPoint;
    PropPosition(Class<PointF> type, String name, PointF endPoint) {
        super(type, name);
        this.endPoint = endPoint;
    }
    @Override
    public void set(View view, PointF value) {
        int x = Math.round(value.x);
        int y = Math.round(value.y);
        int startX = Math.round(endPoint.x);
        int startY = Math.round(endPoint.y);
        int transY = y - startY;
        int transX = x - startX;
        view.setTranslationX(transX);
        view.setTranslationY(transY);
    }
    @Override
    public PointF get(View object) {
        return null;
    }
}

ObjectAnimator ofObject (T target, 
                Property<T, V> property, 
                TypeConverter<PointF, V> converter, 
                Path path)
複製程式碼

可能ObjectAnimator的這個方法我們不常用,其實也很簡單,就是根據傳入的path,每個進度會回撥一個物件,我們這裡是PointF,由於預設的就是PointF,所以我們第三個引數傳null就行了。假設我們需要回撥的是Point物件,那麼我們要實現一個TypeConverter<PointF, Point>,很好理解,就是用來型別轉換的。

PropPosition的set方法回撥中會返回每個進度根據path計算出來的PointF,這樣我們就可以通過結束點計算出view需要平移的距離。

我知道大家很好奇是怎麼傳值的,這裡我單獨寫了一篇文章玩一玩Android屬性動畫原始碼來輔助大家理解。

來一份Android動畫全家桶

回到我們的主題,既然類已經寫完了,如何呼叫的呢?

//詳情頁
private void initTransition() {
    Window window = getWindow();
    TransitionSet set = new TransitionSet();
    AutoTransition autoTransition = new AutoTransition();
    autoTransition.excludeTarget(R.id.ic_head, true);
    autoTransition.addTarget(R.id.tx_name);
    autoTransition.addTarget(R.id.ic_location);
    autoTransition.addTarget(R.id.tx_location);
    autoTransition.setDuration(600);
    autoTransition.setInterpolator(new DecelerateInterpolator());
    set.addTransition(autoTransition);
    
    BezierTransition bezierTransition = new BezierTransition();
    bezierTransition.addTarget(R.id.ic_head);
    bezierTransition.excludeTarget(R.id.tx_name, true);
    bezierTransition.excludeTarget(R.id.ic_location, true);
    bezierTransition.excludeTarget(R.id.tx_location, true);
    bezierTransition.setDuration(600);
    bezierTransition.setInterpolator(new DecelerateInterpolator());
    set.addTransition(bezierTransition);
    
    CircularRevealTransition transition = new CircularRevealTransition();
    transition.excludeTarget(android.R.id.statusBarBackground, true);
    
    window.setEnterTransition(transition);
    window.setReenterTransition(transition);
    window.setReturnTransition(transition);
    window.setExitTransition(transition);
    
    window.setSharedElementEnterTransition(set);
    window.setSharedElementReturnTransition(set);
}
複製程式碼

根據前面知識普及,我相信不需要解釋太多,可能需要解釋的地方就是addTarget和excludeTarget。addTarget就是指定參與過渡的view,excludeTarget就是排除過渡的view。

有些小夥伴可能對那個背景很好奇,它並不是一張背景圖,中間以貝塞爾曲線分割,下面是白色,上面是前一個場景高斯模糊。貝塞爾曲線雖然不是什麼很新鮮的東西,但是運用廣泛,比如lottie章節開發用的AE中鋼筆的運用就是貝塞爾曲線。

小總結

整個過程下來,是不是發現轉場動畫也並不難,有些同學看到這可能已經自己寫了幾個過渡動畫了。這我就很開心了,能幫到大家真正運用這些知識。當然了不要忘了新增windowContentTransitions屬性哦,還有windowAllowEnterTransitionOverlap和windowAllowReturnTransitionOverlap來控制兩個場景的動畫要不要同步。NM的,咋不早說?

來一份Android動畫全家桶

衛星選單

我記得在我畢業的時候,這玩意挺火的,看起來也很牛皮,實則實現起來很簡單。可以說是動畫的入門,那為啥要放這裡呢?我就放這裡,沒說我要分析啊,告訴一下大家專案有這個動畫。

騷聊

又到了緊張刺激的騷聊環節。到這裡,肯定有小夥伴質疑我了,你確定這是Android動畫全家桶嗎?我可以很負責任的告訴你,是。只不過是普通規格的全家桶。無論是動畫的種類還是動畫的用法肯定是不全的,但是常用的已經八九不離十,重點是給大家總結個大概,感興趣的可以深入瞭解。

趁這次機會,我回答一下,很多同學問我的問題,Android到什麼地步才算厲害?首先我覺得這種問題很無聊,其次我自己的水平也並不高,不知道有沒有資格回答。

鄙人在這發表一得之見,很多同學可能認為懂底層原始碼的人才是牛皮。懂底層確實很牛皮,但不是最牛皮的,只要你有耐心去閱讀分析,不斷深入,我相信你也可以和大佬一樣寫出深刻的原始碼分析,可能用的時間比大佬長那麼一點,那麼結果就出來了,學習能力。在這技術不斷迭代更新的時代,最重要的是學習能力,因為可能你今天用的技術明天就被棄用了,當然這可能誇張一點,正如今天的Android動畫分享,學習能力強的人看一遍自己過一遍已經可以舉一反三了,再則,原始碼也是人寫的,也是有迭代的,那你是不是需要重新分析一遍?看原始碼更多的是看大牛如何寫程式碼,然後學以致用,今天主題動畫的難點不是原始碼也不是如何使用,而是動畫的創意!

哦,對了,我答應別人每天只能吹#個牛,祝大家生活愉快,溜了,溜了。有問題可以加我好友,我部落格有聯絡方式,文章的程式碼都是開源專案CrazyDaily裡面的。

最後,感謝一直支援我的人!

傳送門

Github:github.com/crazysunj/

部落格:crazysunj.com/

相關文章