上週,我們把Captain Train app更名為了Trainline。這意味著我們必須改變顏色,圖示,空白狀態以及動畫等以匹配我們的新商標。
在建立新的載入動畫的時候我們遇到了一些問題。因此我覺得這篇部落格可能對開發者夥伴們有點用。實際上,我將嘗試解釋一下我們是如何設計與實現這個動畫的。
劇透:完全是由裝置渲染,無視訊,無gif,只有普通的View和向量動畫。
背景
在app更名之前,在載入搜尋結果時我們顯示的動畫是這樣的:
這非常簡單,就在app釋出之前不到3小時內我們就完成了。實際上我們只使用了由三個drawable組成的animation-list drawable就完成了。
對於Trainline,我們想要更好的效果。我們的設計師建立了一個漂亮的設計圖。我們的挑戰就是實現它。這裡是我們最終得到的效果:
現在來看看我們是如何建立的。
實現
在安卓上有幾種實現動畫的技術。首先要指明的是我希望這個動畫能在 Ice Cream Sandwich (4.0) 上執行,這是我們的minSdk。
讓我們來看看幾種可能的方式:
使用GIF。在web開發中這可能是一個很好的方法,但是安卓並不真正支援GIF。我也並不想為此而專門引入一個新的library。況且,我還必須處理不同的解析度,這對於我們的APK來說是個負擔。
使用video。我不想使用MediaPlay之類的東西。理由還是解析度和加重apk。
使用AnimatedVectorDrawable。考慮到最近已經向後相容,這個方法值得考慮。它很棒,但是會帶來很多XML檔案。而且這個過程也很難在設計師與開發者之間交流,因為設計師很難建立AnimatedVectorDrawable。而且我也不敢保證在低端裝置上能流暢執行。
使用檢視動畫。對於設計師和開發者來說這都比較簡單,只有標準的VectorDrawable。檢視動畫被安卓framework很好的處理了,在過度繪製和效能上都有優化。
設計
越遠越慢
第一步就是設計視察動畫,給人以速度和移動的感覺。我需要把原始影象分解成幾個layer(層)並把它們顯示在ImageView中:
然後,規則很簡單:“越遠越慢”。比如,風顯然要比地平線移動得快。
一個layer動畫的規則也很簡單,每個layer都是一樣的:讓layer從右到左移動,從視窗的一邊開始,到另一邊結束。重複模式為無限迴圈。layer越遠,動畫的duration(持續時間)越長。讓我們來看一看程式碼示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
int translationXStart = totalViewWidth - mLayerView.getLeft() + OFFSET; int translationXEnd = -mLayerView.getLeft() - mLayerView.getWidth() - OFFSET; mLayerAnimator = ObjectAnimator.ofFloat(mLayerView, View.TRANSLATION_X, translationXStart, translationXEnd); mLayerAnimator.setRepeatCount(ObjectAnimator.INFINITE); mLayerAnimator.setRepeatMode(ObjectAnimator.RESTART); mLayerAnimator.setInterpolator(LINEAR_INTERPOLATOR); mLayerAnimator.setDuration(ANIMATION_DURATION); |
正如你看到的,這非常簡單,這個例子只有X上的動畫,但是某些layer(列車和鳥)可能需要Y上的動畫。
另一個重要的細節就是為了減少記憶體的佔用,每個layer只包裹它的內容,這樣Android只會繪製最小的部分。
你可以看到動畫中鐵軌是唯一沒有移動的layer。
目前我們並不支援RTL,如果哪天支援的話,改變動畫的方向會很簡單。
Magic disappearance
既然動畫是無限迴圈的,我必須讓layer到達兩邊的時候有個漸進的淡入淡出效果。一旦鳥到達了左邊沿,它必須同時從右邊沿進入。這裡我使用了一個小技巧:用兩個半透明的drawable作為背景。原理如下圖:
火車自然的移動
對於火車的實現我最開始只是上下移動1dp。當我跟我們的設計師說的時候,他建議我試下實現比這更自然的效果。畢竟火車不是這樣震動的。
我想建立一個遵循我自定義路徑的interpolator:
最佳方式就是使用PathInterpolator,這個interpolator在開發者中間可能是最不知名的,但是我覺得它更強大,尤其是現在可以使用PathInterpolatorCompat做到向前相容了以後。
讓我們來看看程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private void prepareLocomotiveAnimation() { // First, create your path final Path path = new Path(); path.lineTo(0.25f, 0.25f); path.lineTo(0.5f, -0.25f); path.lineTo(0.7f, 0.5f); path.lineTo(0.9f, -0.75f); path.lineTo(1f, 1f); // Create the ObjectAnimatpr mLocomotiveAnimator = ObjectAnimator.ofFloat(mLocomotiveView, View.TRANSLATION_Y, -1dp, 0); mLocomotiveAnimator.setRepeatCount(ObjectAnimator.INFINITE); mLocomotiveAnimator.setRepeatMode(ObjectAnimator.REVERSE); mLocomotiveAnimator.setDuration(ANIMATION_DURATION_LOCOMOTIVE); // Use the PathInterpolatorCompat mLocomotiveAnimator.setInterpolator(PathInterpolatorCompat.create(path)); } |
其實沒有啥神祕的。要注意的是,我應該使用quadTo替代ineTo去改進這個interpolation,那樣會更流暢。還要注意的是動畫的重複模式我使用了反向,這樣可以變得連貫。
讓鳥飛起來
這屬於錦上添花的部分。讓我們來看看我是如何做到的。
我說過AnimatedVectorDrawable很棒是吧?剛好它是這個問題的最佳解決方案。第一步是匯入你的VectorDrawable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:viewportWidth="18" android:viewportHeight="9" android:width="18dp" android:height="9dp"> <path android:name="bird" android:pathData="M2,4c3,-1 6,-1.5 7,2c1,-3.5 4,-3 7,-2" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeColor="#20416b" android:strokeMiterLimit="1.41421" android:strokeLineCap="round" /> </vector> |
pathData的值是翅膀在上時的vector。
然後,你需要一個animator讓翅膀上下襬動。最關鍵的點是讓兩個path data的點具有相同數目。
1 2 3 4 5 6 7 8 9 |
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:duration="500" android:interpolator="@android:anim/accelerate_interpolator" android:propertyName="pathData" android:repeatCount="infinite" android:repeatMode="reverse" android:valueFrom="M2,4c3,-1 6,-1.5 7,2c1,-3.5 4,-3 7,-2" android:valueTo="M4,7c3,-1 4,-4.5 5,-1c1,-3.5 2,0 5,1" android:valueType="pathType" /> |
valueFrom的值是翅膀在上時的向量,valueTo則是翅膀在下時的向量。
最後我們使用AnimatedVectorDrawable把VectorDrawable和animator連繫起來:
1 2 3 4 5 6 |
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/animated_train_bird"> <target android:animation="@animator/fly" android:name="bird" /> </animated-vector> |
就如你看到的那樣,這是一個演示了AnimatedVectorDrawables強大之處的基本例子。的確對動畫產生了不小的潤色效果。
總結
作為總結,我想說使用者的反饋是很重要的!你總是需要別人來檢測你的設計是否有意義、漂亮、自然。
對於我而言,如果沒有Enrico的視覺設計,我是無法完成的,也感謝Cyril 和 Thibaut 的反饋和檢驗。