在前篇重組/分解動畫完成後,我想到了一個最佳使用場景:CollectionView 添加和刪除項目時的動畫。效果如圖:
Github 地址:CollectionViewAnimation
順便推薦這個 mac 小軟件 : Drop to Gif,可以將視頻轉換為 gif 文件,非常節省時間。這次幾篇文章裡面的 gif 都是用這個小軟件轉換的,雖然很不穩定經常卡死,但是非常地節省時間,以往我都是用 PS 搞定的,慢而且效果也不怎麼好,調整參數特別的麻煩。這個小軟件的設置非常方便
本文是在 Collection View 動畫的基礎上進行的改造,這篇文章探索了布局動畫,我實現上面的效果時發現使用布局動畫的手段無法完成這個目的。UICollectionView 的內容由其數據源提供,但所有內容的布局都是由其collectionViewLayout屬性來決定,這也是 UICollectionView 靈活和強大的地方,按這篇文章的說法,『實現自定義布局和動畫的唯一約束就是你的想象力』。但上面的效果和布局關系不大,而是針對 cell 視圖內容本身進行處理,不過,從這篇文章可以了解到 UICollectionView 是怎樣處理插入/刪除項目時的布局流程,這對本文的動畫也至關重要。
這裡吐槽一下,Xcode 提供的UICollectionViewController子類模板文件裡添加了下面的代碼:
self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
這行代碼很過時,它會造成 cell 都不可見,記得在 Xcode 6 的時候這行代碼還會報錯,切記刪掉這行代碼,不然死活看不到 cell 也找不到問題所在。話說 Xcode 提供的模板代碼都很難用。
不必擔心重組/分解動畫是怎麼實現的,一行代碼幫你搞定。另外,UIView 重組/分解動畫擴展是針對在屏幕上可見的視圖的,為了在這裡使用,做了一些改動,但你不需要關心這個,只要使用就好了。
布局更新流程
先來理清更新布局時的處理流程,當你添加了新的 cell 後,需要更新布局,交給collectionViewLayout來處理,用來處理布局更新的方法就是下面這些:
首先調用prepareForCollectionViewUpdates:,這裡包含了所有的布局(位置)變化信息:添加的,刪除的,更新的,一般在這裡收集布局變化信息。
最後調用finalizeCollectionViewUpdates收尾,這裡一般用來重置收集的數據。
中間都是
initialLayoutAttributesForAppearingItemAtIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
方法名字很好地表明了作用。如果你細心觀察調用這兩個方法時處理的索引位置會發現,不止是新添加的 cell 會調用initialLayoutAttributesForAppearingItemAtIndexPath:方法,由於新添加的 cell 會造成其他 cells 的相對位置發生變化,這兩個方法會被重復調用。比如原來的 Section 0 的布局是這樣:ABCDE,在 D 的前面添加X,會這樣調用:
initialLayoutAttributesForAppearingItemAtIndexPath: //Section: 0 Item: 3, Insert X finalLayoutAttributesForDisappearingItemAtIndexPath: //Section: 0 Item: 3, Move D to E initialLayoutAttributesForAppearingItemAtIndexPath: //Section: 0 Item: 4 finalLayoutAttributesForDisappearingItemAtIndexPath: //Section: 0 Item: 4, Move E to next initialLayoutAttributesForAppearingItemAtIndexPath: //Section: 0 Item: 5
//然而這裡依然還會有其他位置未受到影響的 cell 的布局更新調用,且順序並不是按照自身的位置。
除了那個真正添加的X的布局更新調用是在最前面,其他的順序都不能保證。刪除 cell 時和這個過程類似,只不過最初調用的是finalLayoutAttributesForDisappearingItemAtIndexPath:方法。
通過這initialLayoutXXX和finalLayoutXXX這兩個方法,提供了 cell 剛出現在屏幕上和從屏幕上消失時的布局信息,比如大小,旋轉角度,透明度等等,對於新插入的 cell,系統根據initialLayoutXXX提供的初始布局與提供的最終布局信息進行差值計算從而生成動畫;對於消失的 cell,根據finalLayoutXXX提供的最終布局和原來的布局信息進行差值計算生成動畫。
插入動畫
對新添加的 cell 進行動畫,首先要獲取到該 cell。但需要注意的是,在finalizeCollectionViewUpdates完成前,由於 cell 的位置還在變化中,在initialLayoutAttributesForAppearingItemAtIndexPath:裡直接通過位置來獲取的 cell 得到的並不是你想要的 cell,特別是反映真正被添加的 cell 的該方法裡,但此時 cell 還未被顯示在屏幕上,根本無從獲取。
collectionView:willDisplayCell:forItemAtIndexPath:是UICollectionView的可選代理方法,用於跟蹤屏幕上所有即將出現的 cell 的情況,可以從這裡得到即將顯示在屏幕上的 cell。研究該方法的調用時機你會發現,就在那個真正添加的X的布局更新調用後,該方法被立即調用,隨後才是其他相對位置發生的 cells 的布局更新。所以,在這裡可以獲得我們需要的 cell,並在這裡對這個即將出現的 cell 進行動畫。問題來了,視圖還沒有出現在屏幕上,怎麼獲取它的內容呢?
let cellSnapshot = cell.snapshotViewAfterScreenUpdates(true)//必須指定參數為 true,才能獲取到真正的內容,不然會提示無法截取空的內容。
不過,截取內容不需要你來做,我把所有的准備工作都打包好了放入上面的 UIView 重組/分解動畫擴展裡,你只需要調用就可以了,一行代碼搞定。
cell.refactor()
但實際上沒有這麼簡單,還有一些工作需要做。collectionView:willDisplayCell:forItemAtIndexPath:跟蹤屏幕上所有即將出現的 cell,但我們僅僅只需要對新添加的 cell 進行動畫,這還是需要回到布局流程去獲取新添加的 cell 的索引信息。前面說過,prepareForCollectionViewUpdates:裡包含了布局更新過程中所有的變化信息。
新建UICollectionViewLayout子類SDEFlowLayoutWithAnimation,添加以下屬性來收集添加的 cell 的索引位置信息:
var insertedItemsToAnimate: Set= [] override func prepareForCollectionViewUpdates(updateItems: [UICollectionViewUpdateItem]) { super.prepareForCollectionViewUpdates(updateItems) for updateItem in updateItems{ switch updateItem.updateAction{ case .Insert: insertedItemsToAnimate.insert(updateItem.indexPathAfterUpdate) default: break } } }
在UICollectionView 的 delegate 對象裡,重寫以下方法:
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) { if animationLayout != nil{ //如果 indexPath 是新添加的 cell 的索引,那麼對該 cell 使用重組動畫。 let filteredItems = animationLayout!.insertedItemsToAnimate.filter({ element in return element.section == indexPath.section && element.item == indexPath.item }) if filteredItems.count > 0{ cell.refactor()//一行代碼搞定重組動畫 } } }
這樣就完成了插入動畫。
還有點事情要做,重寫下面的方法提供默認的布局,不然沒有動畫發生。為啥,我的重組動畫擴展需要截取 cell 的內容,這裡的方法提供了 cell 出現時最初始的布局信息,如果沒有提供,大概無法獲取內容吧,我只能這樣猜了。
override func initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { let attr = self.layoutAttributesForItemAtIndexPath(itemIndexPath) return attr }
但是,這裡有一點小問題,如果你夠敏感,或者你也許會疑惑這個動畫怎麼和前面的重組動畫不一樣,那是因為這個動畫在中途中斷了。這才是我們想要的樣子。
為什麼會這樣?其實我也不知道,我猜測是由於布局動畫過程還沒有結束,中斷了重組動畫的執行。由於布局動畫過程是個黑盒子,這裡只能靠猜了。上面是怎麼解決這個問題的呢,將cell.refactor()放到主線程裡執行。仔細看上面的動畫,在重組動畫開始之前,後面的 cell 相繼向右邊移動的過程被中斷了幾次,像卡殼一樣。之前的過程是犧牲重組動畫完成布局動畫,現在是犧牲布局動畫來完成重組動畫。現在還沒有找到完美的解決方法,如果有誰找到了辦法,請指教。
刪除動畫
對一個已經存在的視圖進行動畫比一個尚未出現的視圖進行動畫簡單多了,之前那個轉場動畫是如此,今天這個動畫如此。
沒有了實現插入動畫時的各種黑盒行為,實現刪除動畫簡單多了。而且由於不需要對布局做任何修改,分解動畫不再會被布局過程中斷,能夠按照預期順利執行,而且我們甚至不需要去布局過程收集刪除項目的布局信息。思路是這樣的,直接對要刪除的 cell 進行分解動畫,分解動畫的實現裡第一步就是獲取視圖的截圖然後分割成碎片覆蓋在源視圖上,同時隱藏源視圖,此時你看到的內容是偽造的,早已被我偷天換日;第二步等待分解動畫完成,偽造的內容被移除,而我們讓源視圖依然保持隱藏,這與原來的分解動畫在最後移除源視圖稍微有些不同,這樣做就不會引起布局的變化導致露餡;最後,執行真正的刪除 cell 操作,由於源視圖 cell 是隱藏的,是看不到原來的刪除動畫的,只能看到後面的 cells 填充空缺的位置的動畫。這樣一來,原來的刪除動畫就被我的分解動畫替代了。但要注意分解動畫完成後立即執行刪除操作,不然兩者之間間隔太久看起來有問題。
核心代碼:
let deletedIndexPath = ...... //檢查要刪除的 cell 是否在當前屏幕中,如果在,就執行分解動畫;不在,就沒必要執行分解動畫了。 let visibleIndexPaths = self.collectionView?.indexPathsForVisibleItems() let filteredIndexPaths = visibleIndexPaths?.filter({ indexPath in return indexPath.section == deletedIndexPath.section && indexPath.item == deletedIndexPath.item }) ......//更新數據源 if filteredIndexPaths?.count > 0{ let deletedCell = self.collectionView?.cellForItemAtIndexPath(deletedIndexPath) let animationTime: NSTimeInterval = 0.5 //一行代碼執行分解動畫 deletedCell?.destructWithTime(animationTime) //使用 performSelector 方法能精確執行的時間,這很重要,使用 dispatch_after 無法保證,只能保守地多加0.1~0.2秒,這可能會造成肉眼可見的延遲。 self.collectionView?.performSelector("deleteItemsAtIndexPaths:", withObject: [deletedIndexPath], afterDelay: animationTime) }else{ self.collectionView?.deleteItemsAtIndexPaths([deletedIndexPath]) }
移動動畫
最後,關於移動 cell 時的動畫,我原本准備實現的,但發現默認的動畫效果太優雅了,不想替換,好吧,是懶。這些操作裡,移動並不會改變 cells 的數量,而插入和刪除則會,這就使得在插入和刪除時都會空出一個 cell 的位置供你秀動畫,而在移動操作裡要移動的 cell 與其他相對位置發生變化的 cells 同時進行了移動,從視覺上這些 cells 的移動更吸引眼球,除非你用一個更吸引眼球的優雅的動畫,不然效果不會好。如果你對這個效果有興趣,可以自己嘗試下。
好吧,這麼有意思有挑戰的事情,當然要試一下,點擊這裡查看效果,介紹文章看這裡。
Github 地址:CollectionViewAnimation