objc系列譯文(12.5):Collection View 動畫

發表於2014-05-28

UICollectionView 和相關類的設定非常靈活和強大。但是靈活性一旦增強,某種程度上也增加了其複雜性: UICollectionView 比老式的 UITableView 更有深度,適用性也更強。

Collection View 深入太多了,事實上,Ole BegemanAsh Furrow 之前曾在 objc.io 上發表過 自定義 Collection View 佈局UICollectionView + UIKit 力學,但是我依然有一些他們沒有提及的內容可以寫。在這篇文章中,我假設你已經非常熟悉 UICollectionView 的基本佈局,並且至少閱讀了蘋果精彩的程式設計指南以及 Ole 之前的文章

本文的第一部分將集中討論並舉例說明如何用不同的類和方法來共同幫助實現一些常見的 UICollectionView 動畫。在第二部分,我們將看一下帶有 collection views 的 view controller 轉場動畫以及在 useLayoutToLayoutNavigationTransitions 可用時使用其進行轉場,如果不可用時,我們會實現一個自定義轉場動畫。

你可以在 GitHub 中找到本文提到的兩個示例工程:

Collection View 佈局動畫

標準 UICollectionViewFlowLayout 除了動畫是非常容易自定義的,蘋果選擇了一種安全的途徑去實現一個簡單的淡入淡出動畫作為所有佈局的預設動畫。如果你想實現自定義動畫,最好的辦法是子類化 UICollectionViewFlowLayout 並且在適當的地方實現你的動畫。讓我們通過一些例子來了解 UICollectionViewFlowLayout 子類中的一些方法如何協助完成自定義動畫。

插入刪除元素

一般來說,我們對佈局屬性從初始狀態到結束狀態進行線性插值來計算 collection view 的動畫引數。然而,新插入或者刪除的元素並沒有最初或最終狀態來進行插值。要計算這樣的 cells 的動畫,collection view 將通過 initialLayoutAttributesForAppearingItemAtIndexPath: 以及 finalLayoutAttributesForAppearingItemAtIndexPath: 方法來詢問其佈局物件,以獲取最初的和最後的屬性。蘋果預設的實現中,對於特定的某個 indexPath,返回的是它的通常的位置,但 alpha 值為 0.0,這就產生了一個淡入或淡出動畫。如果你想要更漂亮的效果,比如你的新的 cells 從螢幕底部發射並且旋轉飛到對應位置,你可以如下實現這樣的佈局子類:

結果如下:

對應的 finalLayoutAttributesForAppearingItemAtIndexPath: 方法中,除了設定了不同的 transform 以外,其他都很相似。

響應裝置旋轉

裝置方向變化通常會導致 collection view 的 bounds 變化。如果通過 shouldInvalidateLayoutForBoundsChange: 判定為佈局需要被無效化並重新計算的時候,佈局物件會被詢問以提供新的佈局。UICollectionViewFlowLayout 的預設實現正確地處理了這個情況,但是如果你子類化 UICollectionViewLayout 的話,你需要在邊界變化時返回 YES

在 bounds 變化的動畫中,collection view 表現得像當前顯示的元素被移除然後又在新的 bounds 中被被重新插入,這會對每個 IndexPath 產生一系列的 finalLayoutAttributesForAppearingItemAtIndexPath:initialLayoutAttributesForAppearingItemAtIndexPath: 的呼叫。

如果你在插入和刪除的時候加入了非常炫的動畫,現在你應該看看為何蘋果明智的使用簡單的淡入淡出動畫作為預設效果:

啊哦…

為了防止這種不想要的動畫,初始化位置 -> 刪除動畫 -> 插入動畫 -> 最終位置的順序必須完全匹配 collection view 的每一項,以便最終呈現出一個平滑動畫。換句話說,finalLayoutAttributesForAppearingItemAtIndexPath: 以及 initialLayoutAttributesForAppearingItemAtIndexPath: 應該針對元素到底是真的在顯示或者消失,還是 collection view 正在經歷的邊界改變動畫的不同情況,做出不同反應,並返回不同的佈局屬性。

幸運的是,collection view 會告知佈局物件哪一種動畫將被執行。它分別通過呼叫 prepareForAnimatedBoundsChange:prepareForCollectionViewUpdates: 來對應 bounds 變化以及元素更新。出於本例項的說明目的,我們可以使用 prepareForCollectionViewUpdates: 來跟蹤更新物件:

以及修改我們元素的插入動畫,讓元素只在其正在被插入 collection view 時進行發射:

如果這個元素沒有正在被插入,那麼將通過 layoutAttributesForItemAtIndexPath 來返回一個普通的屬性,以此取消特殊的外觀動畫。結合 finalLayoutAttributesForAppearingItemAtIndexPath: 中相應的邏輯,最終將會使元素能夠在 bounds 變化時,從初始位置到最終位置以很流暢的動畫形式實現,從而建立一個簡單但很酷的動畫效果:

互動式佈局動畫

Collection views 讓使用者通過手勢實現與佈局互動這件事變得很容易。如蘋果建議的那樣,為 collection view 佈局新增互動的途徑一般會遵循以下步驟:

  1. 建立手勢識別
  2. 將手勢識別新增給 collection view
  3. 通過手勢來驅動佈局動畫

讓我們來看看我們如何可以建立一些使用者可縮放捏合的元素,以及一旦使用者釋放他們的捏合手勢元素返回到原始大小。

我們的處理方式可能會是這樣:

這個捏合操作需要計算捏合距離並找出被捏合的元素,並且在使用者捏合的時候通知佈局以實現自身更新。當捏合手勢結束的時候,佈局會做一個批量更新動畫返回原始尺寸。

另一方面,我們的佈局始終在跟蹤捏合的元素以及期望尺寸,並在需要的時候提供正確的屬性: 

 

小結

我們通過一些例子來說明了如何在 collection view 佈局中建立自定義動畫。雖然 UICollectionViewFlowLayout 並不直接允許定製動畫,但是蘋果工程師提供了清晰的架構讓你可以子類化並實現各種自定義行為。從本質來說,在你的 UICollectionViewLayout 子類中正確地響應以下訊號,並對那些要求返回 UICollectionViewLayoutAttributes 的方法返回合適的屬性,那麼實現自定義佈局和動畫的唯一約束就是你的想象力:

  • prepareLayout
  • prepareForCollectionViewUpdates:
  • finalizeCollectionViewUpdates
  • prepareForAnimatedBoundsChange:
  • finalizeAnimatedBoundsChange
  • shouldInvalidateLayoutForBoundsChange:

更引人入勝的動畫可以結合像在 objc.io 話題 #5 中 UIKit 力學這樣的技術來實現。

帶有 Collection views 的 View controller 轉場

就如 Chris 之前在 objc.io 的文章中所說的那樣,iOS 7 中的一個重大更新是自定義 view controller 轉場動畫。與自定義轉場動畫相呼應,蘋果也在 UICollectionViewController 新增了 useLayoutToLayoutNavigationTransitions 標記來在可複用的單個 collection view 間啟用導航轉場。蘋果自己的照片和日曆應用就是這類轉場動畫的非常好的代表作。

UICollectionViewController 例項之間的轉場動畫

讓我們來看看我們如何能夠利用上一節相同的示例專案達到類似的效果:

為了使佈局到佈局的轉場動畫工作,navigation controller 的 root view controller 必須是一個 useLayoutToLayoutNavigationTransitions 設定為 NO 的 collection view controller。當另一個 useLayoutToLayoutNavigationTransitions 設定為 YESUICollectionViewController 例項被 push 到根檢視控制器之上時,navigation controller 會用佈局轉場動畫來代替標準的 push 轉場動畫。這裡要注意一個重要的細節,根檢視控制器的 collection view 例項被回收用於在導航棧上 push 進來的 collection 控制器中,如果你試圖在 viewDidLoad 之類的方法中中設定 collection view 屬性, 它們將不會有任何反應,你也不會收到任何警告。

這個行為可能最常見的陷阱是期望回收的 collection view 根據頂層的 collection 檢視控制器來更新資料來源和委託。它當然不會這樣:根 collection 檢視控制器會保持資料來源和委託,除非我們做點什麼。

解決此問題的方法是實現 navigation controller 的委託方法,並根據導航堆疊頂部的當前檢視控制器的需要正確設定 collection view 的資料來源和委託。在我們簡單的例子中,這可以通過以下方式實現:

當詳細頁面的 collection view 被推入導航棧時,我們重新設定 collection view 的資料來源到詳細檢視控制器,確保只有被選擇的 cell 顏色顯示在詳細頁面的 collection view 中。如果我們不打算這樣做,佈局依然可以正確過渡,但是collection 將顯示所有的 cells。在實際應用中,detail 的資料來源通常負責在轉場動畫過程中顯示更詳細的資料。

用於常規轉換的 Collection View 佈局動畫

使用了 useLayoutToLayoutNavigationTransitions 的佈局和佈局間導航轉換是很有用的,但卻侷限於僅在 兩個 view controller 都是 UICollectionViewController 的例項,並且轉場的必須發生在頂級 collection views 之間。為了達到在任意檢視控制器的任意 collection view 之間都能實現相似的過渡,我們需要自定義一個 view collection 的轉場動畫。

針對此類自定義過渡的動畫控制器,需要遵循以下步驟進行設計:

  1. 對初始的 collection view 中的所有可見元素製作截圖
  2. 將截圖新增到轉場上下文的 container view 中
  3. 運用目標 collection view 的佈局計算最終位置
  4. 製作動畫使快照到正確的位置
  5. 當目標 collection view 可見時刪除截圖

一個這樣的動畫設計有兩重缺陷:它只能對初始的 collection view 的可見元素製作動畫,因為快照 APIs 只能工作於螢幕上可見的 view,另外,依賴於可見的元素數量,可能會有很多的 views 需要進行正確的跟蹤併為其製作動畫。但另一方面,這種設計又具有一個明顯的優勢,那就是它可以為所有型別的 UICollectionViewLayout 組合所使用。這樣一個系統的實現就留給讀者們去進行練習吧。

在附帶的演示專案中我們用另一種途徑進行了實現,它依賴於一些 UICollectionViewFlowLayout 的巧合。

基本的想法是,因為源 collection view 和目標 collection view 都擁有有效的 flow layouts,因此源 layout 的佈局屬性正好可以用作目標 collection view 的佈局中的初始佈局屬性,以此驅動轉場動畫。一旦正確建立,就算對於那些一開始在螢幕上不可見的元素,collection view 的機制都將為我們追蹤它們並進行動畫。下面是我們的動畫控制器中的 animateTransition: 的核心程式碼:

首先,動畫控制器確保目標 collection view 以與原來的 collection view 完全相同的框架和佈局作為開始。接著,它將源 collection view 的佈局設定給目標 collection view,以確保其不會失效。與此同時,該佈局已經複製到另一個新的佈局物件中,而這個佈局物件則是為防止在導航回原始檢視控制器時出現奇怪的佈局 bug。我們還會強制在目標 collection view 的底部設定一個很大的 content inset,來確保佈局在動畫的初始位置時保持在一行上。觀察日誌的話,你會發現由於元素的尺寸加上 inset 的尺寸會比 collection view 的非滾動維度要大,因此 collection view 會在控制檯警告。在這樣的情況下,collection view 的行為是沒有定義的,我們也只是使用這樣一個不穩定的狀態來作為我們轉換動畫的初始狀態。最後,複雜的動畫 block 將展現它的魅力,首先將目標 collection view 的框架設定到最終位置,然後在 performBatchUpdates:completion: 的 update block 中執行一個無動畫的佈局來改變至最終佈局,緊隨其後便是在 completion block 中將 content insets 重置為原始值。

小結

我們討論了兩種可以在 collection view 之間實現佈局轉場的途徑。一種使用了內建的 useLayoutToLayoutNavigationTransitions,看起來令人印象深刻並且極其容易實現,缺點就是可以使用的範圍較為侷限。由於 useLayoutToLayoutNavigationTransitions 在一些案例中不能使用,想驅動自定義的過渡動畫的話,就需要一個自定義的 animator。這篇文章中,我們看到了如何實現這樣一個 animator,然而,由於你的應用程式大概肯定會需要在兩個和本例完全不同的 view 結構中實現完全不同的動畫,所以正如此例中做的那樣,不要吝於嘗試不同的方法來探究其是否能夠工作。

相關文章