受到iOS版Facebook Paper的啟發,在最近的專案中,我們決定在開啟列表元素時實現一個類似風格的動畫效果。起初,我們試圖使用一個現有的實現——android-flip,通過OpenGL渲染動畫——在最新的Android版本中,它只做到了在螢幕上顯示比較明顯的工件(圖片閃爍)。另外,需要對這個類庫進行改動,因為它是為了滑動列表的元素設計的,但我們的工程需要在開啟列表元素的時候有動畫效果。在下面的演示視訊中可以看出區別:android-flip實現的是摺疊列表,而我們實際需要的是展開詳情。
考慮到這種場景,我們決定自己實現這種效果。因為應用程式支援的最小Android版本為4.0,所以沒使用OpenGL,而是用了標準Android SDK中的方法:View.setRotationX(),View.setScaleX()等。當硬體加速啟用時(如果目標API級別>=14,預設啟用硬體加速),這些方法可以非常高效地利用GPU。
結果看起來很完美,所以決定分享我們的做法。由於本文只是介紹了基本的實現要點,您可以從GitHub上下載所有的實際程式碼:FoldableLayout。
佈局的實現
設計的第一個元素是可以對摺的佈局。我們的做法相當大膽:主佈局(FoldableItemLayout)只包含一個特定的佈局(在baselayout)。在動畫中,BaseLayout將它的內容寫入到快取中,這是一個根據原始佈局的尺寸專門建立的Bitmap物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
; html-script: false ] class FoldableItemLayout extends FrameLayout { @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { Bitmap cacheBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); mBaseLayout.setCacheCanvas(new Canvas(cacheBitmap)); } } class BaseLayout extends FrameLayout { private Canvas mCacheCanvas; private void setCacheCanvas(Canvas cacheCanvas) { mCacheCanvas = cacheCanvas; } @Override public void draw(Canvas canvas) { mCacheCanvas.drawColor(0, PorterDuff.Mode.CLEAR); super.draw(mCacheCanvas); } } |
此外,還需要兩個額外的檢視(PartView)——用於影像的上、下兩半部分。它們將在快取中顯示對應的資料,這些資料代表了該影像(Bitmap)的上半部和下半部。兩個檢視填充了主佈局的整個區域,但只顯示所需的部分。為了達到這種效果,我們計算了點陣圖的界限——在onDraw()方法中,我們讓畫布通過[drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint)](http://developer.android.com/reference/android/graphics/Canvas.html#drawBitmap(android.graphics.Bitmap, android.graphics.Rect, android.graphics.RectF, android.graphics.Paint))方法來繪製所需的部分。
然後通過setRotationX()
方法設定相應的角度,設法旋轉這些額外的檢視,從而實現影像上半部和下半部的獨立旋轉。為了實現這個功能,我們為FoldableItemLayout新增了一個名為FoldRotation的新引數。
FoldRotation引數範圍是(-180,180]:
- FoldRotation=0:兩個部分都不旋轉。在這種情況下,我們可以跳過點陣圖快取,實時的顯示原始的佈局。
- 0 <FoldRotation<90:下層的部分旋轉到 FoldRotation角度;上層部分不旋轉。
- -90<FoldRotation<0:只有上層部分旋轉。
- 90≤FoldRotation<180:下層部分不再顯示。在這種情況下,包含下一佈局的FoldableItemLayout應該覆蓋當前的FoldableItemLayout。
- -180<FoldRotation≤-90:上層部分不再顯示。在這種情況下,包含先前的佈局FoldableItemLayout應該覆蓋當前的FoldableItemLayout。
- FoldRotation=180:兩個部分都隱藏。
現在有了一個二層佈局,能夠“摺疊”它包含的元素,這樣就可以做出一個FoldableListLayout——一個類似列表檢視的佈局,它建立列表元素,並通過使用BaseAdapter將其封裝成FoldableItemLayout。在這種情況下,我們還使用了FoldRotation引數用來確定元素在列表中的位置。
例如,FoldRotation= 30,列表第一個元素(FoldableItemLayout)的FoldRotation值為30,而第二個元素——FoldRotation= 150,最多可以同時顯示不超過2個元素。FoldRotation引數值的範圍依賴於元素的數量:如果列表包含一個元素,那麼取值範圍就會是[0,0],2——[0,180],3——[0,360]等。
開啟動畫
在學會了在幾個元素之間使用摺疊動畫滾動之後,我們解決了這個重大的挑戰:從任意的起點生成元素的開啟動畫。利用已經實現的 FoldableListLayout,並使其在兩個元素之間進行切換:封面佈局和詳情佈局。這兩種元素都應該顯示在螢幕上,但詳情元素應該是隱藏的。當使用者點選一個封面元素時,應用程式會記住當時的位置,並使用相同大小的空佔位符檢視取代它(以免破壞螢幕上的其他元素),並移動蓋元素的下半專門建立的佈局。以後這種佈局將被用作 FoldableListLayout的第一個元素。第二個元素將被替換為空佔位檢視,與封面元素的方式相同。
對齊
眼見封面元素已經從螢幕上的最初位置展開,因此在動畫過程中我們的FoldableListLayout
需要在封面元素和詳情元素的位置之間移動。這就是為什麼要記住在動畫初始化過程中每個元素的初始位置和大小的原因。由於封面和詳情的大小有可能不同,我們需要在動畫期間同時擴充套件它們,以便使它們的寬度一致。
我們基本已經完工,只差一件事:縮放後,封面的高度可能看起來比下半部分的詳情小。這意味著你需要隱藏影像的剩餘部分。
下面的螢幕截圖顯示了在動畫的開始部分有一個灰色區域,下層的詳情部分與封面的大小不符。
我們為FoldableItemLayoutm
引入一個額外的RollingDistance引數解決了這個問題。這個引數負責從折線垂直移動影像。使用這個引數可以在動畫的第一部分中的不知不覺中轉換詳情的一部分,然後在第二部分中全尺寸展開。
動畫現在就可以執行了,餘下的工作就是為逼真的效果新增一些暗化,或為平滑的樣子新增些陰影。本類庫在GitHub上可以隨意使用,在那裡你還可以找到一個使用例子。一如既往的歡迎pull
請求。