[iOS]過渡動畫之高階模仿 airbnb

貝聊科技發表於2017-07-03


注意:我為過渡動畫寫了兩篇文章:
第一篇:[iOS]過渡動畫之簡單模仿系統,主要分析系統簡單的動畫實現原理,以及講解座標系、絕對座標系、相對座標系,座標系轉換等知識,為第二篇儲備理論基礎。最後實現 Mac 上的檔案預覽動畫。
第二篇:[iOS]過渡動畫之高階模仿 airbnb ,主要基於第一篇的理論來實現複雜的介面過渡,包括進入和退出動畫的串聯。最後將這個動畫的實現部分與當前介面解耦,並封裝為一個普適(其他類似介面也適用)的工具類。


這兩篇文章將會帶你學到如何實現下圖 airbnb 首頁類似的過渡動畫,同時最重要的,你將學會怎麼分析類似的動畫,並且知道如何動手實現。GitHub 地址在這裡。

如果你沒看第一篇,那我建議你去看一下第一篇,因為如果沒有第一篇的基礎,這篇還是比較難理解的。不如,現在就去吧。

######好,準備好了嗎?現在開始第二篇。這一篇主要基於第一篇的理論來實現複雜的介面過渡,包括進入和退出動畫的串聯。最後將這個動畫的實現部分與當前介面解耦,並封裝為一個普適(其他類似介面也適用)的工具類。


01.這個介面的架構

我們首先來分析一下這個介面的架構。視窗上是一個可以上下滾動的 UITableViewController,每個 UITableViewCell 上有一個可以左右滑動的 UICollectionView,在每一個 UICollectionViewCell 上佈局一張封面圖片和其他元素。很主流的佈局,大致就是這樣,對吧?

02.基於第一篇,我們應該怎麼劃分這個動畫的結構?

上面的動畫應該怎麼分析呢?什麼❓我好像聽到有同學在說:“太快了❓根本看不清❓” 好,那我就放慢一點,你再仔細瞧瞧。

看清楚沒❓還沒看清❓什麼❓只看到它們在動❓有一種輕拿輕放的感覺❓

那我們再看張圖吧。如果我們腦洞大一點,不要管介面結構,我們把介面想象成為一個平面,那麼我們可以按照下面這張圖來劃分一下動畫結構。

如你所見,其實動畫分為三個部分,UpAnimationPart + CentreAnimationPart + DownAnimationPart,UpAnimationPart 和 DownAnimationPart 的動畫可以歸為一類,他們只是簡單的上移或者下移。重點是中間的 CentreAnimationPart,它和其他部分都有一些區別。

CentreAnimationPart,每張圖片都要作為一個單個的個體進行動畫,而不能將中間整個模組一起進行動畫。為什麼呢?因為每張圖片最後都要在下個介面的頂部填充滿一個控制元件。這麼說太難懂了?意思就是,如果你拿到的是中間區域整體進行動畫的話,那麼你拿到的將是中間圖片區域有遮蓋的部分,而右邊露出來那片橙色的圖片你將拿不到,這個時候當使用者點選的剛好又是右邊那張露出半邊的圖片,你將無法實現中間區域的動畫。

如果你理解了我所說的,那麼你將會理解這兩張圖片的微妙區別。

上面兩張圖片,第二張圖片的動畫結構是正確的。

劃分完動畫結構以後,你應該有一種庖丁解牛的感覺。你有沒有感覺到和第一篇文章所實現的系統動畫已經很像了。希望你能從這種分析和訓練中總結出問題的核心:最難的部分是我們的理解,而不是實現。

03.注意點

既然思路都有了,那趕緊寫程式碼吧?籲... 等等,你的思路真的有了嗎?能說出來是什麼嗎?

沒關係,學東西哪有那麼快。這不是安慰,這是事情的真相。

我們先來看兩個知識點和一個注意點。

3.1.Block 的迴圈引用

可能你已經隱隱意識到了,這個動畫已經難免會用到 block 了,block 有很難搞的“迴圈引用”,你可能仍然搞不懂什麼是“引用環”,說清楚這個問題可能要再寫一篇文章才夠,所以我也不打算在這裡說清楚這個問題。

所以我給你的建議是,凡是你拿不準是不是會出現迴圈引用的地方,你都這麼寫:

__weak typeof(self) weakSelf = self; 
self.aBlock = ^{ 
      __strong typeof(weakSelf) strongSelf = weakSelf;
       if (!strongSelf) return; 

        // 其它程式碼
        ... 
  }複製程式碼

為什麼這麼寫?

  • 解除迴圈引用的問題。__weak 是弱引用,不會將 self 的引用計數器 +1。_strong 將 weakSelf 引用計數器 +1,以保持對 weakSelf 的持有,但是 strongSelf 是一個區域性變數,過完這個程式碼塊,strongSelf 就會自動釋放,所以解除了迴圈引用的可能性。

  • 防止應用奔潰。if (!strongSelf) return; 我們假設一種很常見的情況,當 self 已經釋放的時候,這個 block 被調起,然後就去訪問一個為 nil 的殭屍物件,比如說將 self 的某個屬性插入字典什麼的,這個時候往字典裡插入空元素,自然會造成應用奔潰,有了這一行程式碼,就不會再出現類似的情況了。

3.2.迴圈利用池

我們天天在用的 UITableView 為什麼效能這麼好,很大一部分原因是得益於迴圈利用池這個設計思想。

迴圈利用池的設計思想可以概括為:

  • 當要用到一個物件的時候,先從 ReusePool 中取,如果 ReusePool 中有快取就把這個快取取出來,返還給使用者,然後將這個物件從 ReusePool 中移除。如果 ReusePool 中沒有,就建立一個新的物件,返還給使用者。
  • 當一個物件已經移出視野(或不需要使用)的時候,就會將它加入到 ReusePool 中等待再次迴圈。

基於高效能的這個目的,我們應該給我們的做動畫的 UIImageView 例項建立一個 ReusePool。

3.3.如何測試自己計算的 frame 是否正確?

計算 frame 和遷移 frame 是一件很糾結的事情,而且不知道自己究竟有沒有算對,如果算不對,就會導致動畫錯亂。但是如果最後調動畫的時候才回過來調 frame,就要反覆的檢查究竟哪個 frame 算錯了。這樣是很蛋疼的。

所以我給你一個建議。就是你每算完一個 frame,就在這個 frame 上新增一個佔位檢視看一下對不對。比方說,像這樣新增一個紅色的 View 到螢幕上檢查一下 frame 對不對:

UIView *redView = [[UIView alloc]init];
redView.backgroundColor = [UIColor redColor];
redView.frame = YourFrame;
[self.view.window addSubview:redView];複製程式碼

3.4.怎麼找到 UICollectionViewCell 上那個顯示封面圖片的 UIImageView?

這個需要你在使用的時候給這個 UIImageView 繫結一個 tag,這樣我就能拿到這個 UIImageView。

04.具體實現思路?

讓我們總結一下上面分析的內容,看能不能從中分析出我們的具體實現思路。

4.1.動畫素材

  • 4.1.1、當使用者點選的那一刻,我們首先應該把當前視窗(注意,是視窗 Window)進行截圖,備用。

  • 4.1.2、我們需要有一個工具,給這個工具傳入裁剪的點和裁剪的型別,就能幫我們把一張圖片裁成我們想要的樣式。比方說,我們只要截圖的上半部分,或者下半部分。

  • 4.1.3、我們需要把使用者點選的那個 UICollectionViewCell 上面的那張圖片的 Frame 遷移到視窗座標中,然後計算出需要裁剪的點的位置,最後把視窗截圖和這個點傳進去進行裁剪,得到我們做動畫需要的圖片。

  • 4.1.4、現在做動畫需要的元素中我們已經有了 UpAnimationPart 和 DownAnimationPart 需要的圖片了,現在只差 CentreAnimationPart 需要的可見 Cell 上面的圖片了,這個我們通過 UICollectionView 的 visiableCells 可以拿到。這樣一來,做動畫的素材已經齊備了。

4.2.動畫起始位置

動畫的起始位置應該是這個動畫最容易的部分。

  • 4.2.1、UpAnimationPart 的起始位置應該是點選那個 Cell 的圖片的 Y 座標以上。如果把 upTailorY 指定為點選那個 Cell 的圖片的 Y 座標的話,那麼:

    CGRect upAnimationImageViewFrame_start = CGRectMake(0, 0, JPScreenWidth, upTailorY);複製程式碼
  • 4.2.2、同理,如果把 downTailorY 指定為為點選那個 Cell 的圖片的底部(Y 座標加上圖片的高度)。那麼,DownAnimationPart 的起始位置應該是:

    CGRect downAnimationImageViewFrame_start = CGRectMake(0, downTailorY, JPScreenWidth, JPScreenHeight - downTailorY);複製程式碼
  • 4.2.3、而中間可見 Cell 的圖片的起始位置,都可以通過座標系遷移直接得到。這樣以後,各部分的動畫起點位置我們也都有了,這樣以後我們就可以在視窗上新增 UIImageView 了。

4.3.動畫終點位置

第一篇裡說的考驗數學功底的部分終於來,還是有點小激動。其實也很簡單,你看一張圖就知道了:

  • 4.3.1、UpAnimationPart 的終點位置很 easy,簡單到你可以直接寫出來:

    CGRect upAnimationImageViewFrame_end = CGRectMake(0, -upTailorY, JPScreenWidth, upTailorY);複製程式碼
  • 4.3.2、DownAnimationPart 的終點位置是:

    CGRect downAnimationImageViewFrame_end = CGRectMake(0, JPScreenHeight, JPScreenWidth, JPScreenHeight - downTailorY);複製程式碼
  • 4.3.3、CentreAnimationPart 會稍微有點複雜,其實應該分為三種情況的。具體見下圖:

    • 4.3.3.1、TapImage 這張被點選的圖片,它的終點位置應該很容易確定,就是填充螢幕頂部:
      這個沒有異議吧?而其他兩類需要參考它的位置來定位。

      CGRect tapAnimationImageViewFrame_end = CGRectMake(0, 0, JPScreenWidth, JPScreenWidth*2.0/3.0);複製程式碼
    • 4.3.3.2、TapImage_Left。請看下面這張圖,你肯定明白了,對吧?TapImage 的初始寬度我們知道,左側圖片和 TapImage 的左側間距我們也可以算出來,螢幕寬度我們也知道,現在利用十字相乘法,我們就可以很快拿到下圖紅色方框裡的值,也就是我們要的終點位置的 X 座標。

    • 4.3.3.3、TapImage_Right。這個就不用我再贅述了吧?和上面的情況類似。

05.程式碼實現

我不打算在文章裡貼上程式碼了,一個,篇幅已經很長了,再貼上程式碼,就會讓有些“太長不看”的同學感覺壓力很大。二來,程式碼已經放在 GitHub 上了,看程式碼還是在 Xcode 中更舒服一點。而且我把 Keynote 也一併放上去了。

06.解耦和抽成工具類

如果你按照這個思路去寫的話,你會發現所有的程式碼都會集中在一個方法裡,導致這個方法的程式碼量有三四百行,非常臃腫。而且進入和退出動畫居然耦合在一起,要解耦,要抽工具類又感覺無從下手。這個時候就應該要有一種壯士斷腕的勇氣:“老子一定要把你抽成工具”的決心。有了這個決心,剩下的就是想辦法了。

這個動畫有很多引數,所以對於哪些是必須的,要有所取捨。也就是要嘗試為工具類設計 API。

/*!
 * \~chinese
 * @prama indexPath                使用者選中的那個UICollectionViewCell的 indexPath.
 * @prama collectionView           使用者選中的那個UICollectionViewCell的 UICollectionView.
 * @prama viewController           動畫之前視窗上顯示的 viewController.
 * @prama presentViewController    動畫完成之後要在視窗上顯示的 viewController.
 * @prama afterPresentedBlock      動畫完成之後要在 presentViewController 做的事情.
 *
 * @return JPContainIDBlock        關閉動畫的 block.
 */複製程式碼

對於解耦,我的理解就是,首先寫程式碼之前就要有“儘量不要耦合”的意識。如果專案特別趕時間,你可以暫時不用太理會耦合,這些很深的東西可能需要長期的積累和對於專案的全域性觀,這些可以等週末有空或者專案之間有空檔期的時候再去細細琢磨。

還有就是要有堅韌不拔的意志,有些時候給某個類解耦的時間可能比你重新寫一遍花的時間,還多。但是,總結一下,多出來的時間我們都在思考什麼?是不是這些時間都用在比實現功能更高一個層次的事務上了?

07.關於JPNavigationController

這個動畫得以最後呈現,和我之前的一個框架是分不開的。也就說,如果沒有我之前的那個框架做基礎,那麼這個動畫的關閉部分就無法實現。

具體體現在對於 pop 手勢的攔截。

#pragma mark --------------------------------------------------
#pragma mark JPNavigationControllerDelegate

-(BOOL)jp_navigationControllerShouldPushRight{
    [self backBtnClick:nil];
    return NO;
}複製程式碼

大致可以概述為,當使用者開始 pop 的時候,我們當前控制器會收到代理方法的詢問,詢問是否需要繼續 pop 行為。在我們這個動畫中迫切需要收到這個詢問,但是不需要繼續 pop 行為,所以我們 return NO。


如果你想了解這個框架的實現,你可以看下面這三篇文章:
第一篇:[iOS]UINavigationController全屏pop之為每個控制器自定義UINavigationBar。這篇文章主要是講述如何實現自定義導航欄的,所有的思路和實現都是 JNTian的。
第二篇:[iOS]UINavigationController全屏pop之為每個控制器新增底部聯動檢視。這篇文章講述,如何在已有的自定義導航欄基礎上新增自定義的“底部聯動檢視”。所有的思路和實現都是我自己的。
第三篇:[iOS]UINavigationController全屏pop之為控制器新增左滑push。這次將講述如何實現左滑push到繫結的控制器中,並且帶有push動畫。
或者訪問我的 GitHub 的JPNavigationController


08.GitHub 地址

GitHub 地址在這裡。

我的文章集合

下面這個連結是我所有文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有 Github 地址,Github 上都有原始碼。如果某篇文章剛好在你的實際開發中幫到你,又或者提供一種不同的實現思路,讓你覺得有用,那就看看這句話 “堅持每天點讚的人,99%都是帥哥美女,再也不用單身了”

我的文章集合索引

你還可以關注我自己維護的簡書專題iOS開發心得。這個專題的文章都是實打實的乾貨。

######如果你有問題,除了在文章最後留言,還可以在微博@盼盼_HKbuy上給我留言,以及訪問我的 Github

相關文章