如何?iOS圖書動畫-第2部分(下)
狀態 2 - 開啟書
現在,狀態1的動畫完成了,我們可以轉移到狀態2的處理中來。在這裡我們將一本合起的書轉換成一本開啟的書。在setStartPositionForPush(_:toVC:)方法下添加如下方法:
func setEndPositionForPush(fromVC: BooksViewController, toVC: BookViewController) { //1 for cell in fromVC.collectionView!.visibleCells() as! [BookCoverCell] { cell.alpha = 0 } //2 for cell in toVC.collectionView!.visibleCells() as! [BookPageCell] { cell.layer.transform = transforms[cell]! cell.updateShadowLayer(animated: true) }}
上述代碼解釋如下:
隱藏所有書的封面,因為接下來我們要顯示所選圖書的內容。 在BookViewController中遍曆書中每一頁並讀取先前儲存在transform數組中的的Transform。
在從BooksViewController導航到BookViewController後,我們還需要進行一些清理工作。
在上面的方法之後加入如下方法:
func cleanupPush(fromVC: BooksViewController, toVC: BookViewController) { // Add background back to pushed view controller toVC.collectionView?.backgroundColor = toViewBackgroundColor}
在Push完成時,我們將BookViewController的Collection View的背景色設回原來儲存的顏色,隱藏位於它下面的內容。
實現開啟書的動畫
現在我們已經實現了助手方法,接下來要實現Push動畫了!
在空的animateTransition(_:)方法中加入以下代碼:
//1let container = transitionContext.containerView()//2if isPush { //3 let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BooksViewController let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BookViewController //4 container.addSubview(toVC.view) // Perform transition //5 self.setStartPositionForPush(fromVC, toVC: toVC) UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.7, options: nil, animations: { //6 self.setEndPositionForPush(fromVC, toVC: toVC) }, completion: { finished in //7 self.cleanupPush(fromVC, toVC: toVC) //8 transitionContext.completeTransition(finished) })} else { //POP}
以上代碼解釋如下:
擷取Container View,Container View在兩個View Controller發生轉場時充當父視圖的角色。 判斷當前的轉場動作是否是一個Push動作。 如果是,分別擷取fromVC(BooksViewController)和toVC(BookViewController)。 將toVC(BookViewController)加到Container View。 設定Push動作的起止點,即toVC和fromVC。 開始動畫。從起始點(書合起的狀態)轉變到終點(書開啟狀態)。 執行清理動作。 告訴系統,轉換完成。在Navigation Controller中應用Push動畫
現在我們已經建立好Push動畫,接下來就是將它應用到自訂的Navigation Controller中了。
開啟BooksViewController.swift在類聲明中增加屬性:
var transition: BookOpeningTransition?
transition屬性用於儲存Transition對象,通過它我們可以知道當前動畫是Push動畫還是Pop動畫。
然後在檔案末尾的大括弧之後加入一個擴充:
extension BooksViewController {func animationControllerForPresentController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? { // 1 var transition = BookOpeningTransition() // 2 transition.isPush = true // 3 self.transition = transition // 4 return transition }}
通過擴充,我們將一部分代碼分離出來。這裡,我們將和轉換動畫有關的方法放到了一起。上面的這個方法建立並返回了一個Transition對象。
以上代碼解釋如下:
建立一個新的Transition。 因為我們是要彈出或者Push一個Controller,所以將isPush設定為true。 儲存當前Transition對象。 返回Transition對象。
現在開啟CustomNavigationController.swift並將Push的if語句替換為:
if operation == .Push { if let vc = fromVC as? BooksViewController { return vc.animationControllerForPresentController(toVC) }}
上述語句判斷當前Push的View Controller是不是一個BooksViewController,如果是,用我們建立的BookOpeningTransition呈現BookViewController。
編譯運行,選擇某本書,你將看到書緩緩由合起狀態開啟:
呃…我們的動畫效果呢?
書直接從合起狀態跳到了開啟狀態,原因在於我們沒有載入cell(書頁)!
導航控制器從BooksViewController切換到BookViewController,這二者都是UICollecitonViewController。UICollectionViewCell沒有在主線程中載入,因此代碼一開始的時候以為cell的個數為0——這樣當然不會有動畫產生。
我們需要讓Collection View有足夠的時間去載入所有的Cell。
開啟BooksViewController.swift將openBook(_:)方法替換為:
func openBook(book: Book?) { let vc = storyboard?.instantiateViewControllerWithIdentifier(BookViewController) as! BookViewController vc.book = selectedCell()?.book //1 vc.view.snapshotViewAfterScreenUpdates(true) //2 dispatch_async(dispatch_get_main_queue(), { () -> Void in self.navigationController?.pushViewController(vc, animated: true) return })}
以上代碼解釋如下:
告訴BookViewController在動畫一開始之前截屏。 將Push BookViewController的動作放到主線程中進行,這樣就有時間去載入cell了。
編譯、運行,這次你將看到正確的Push動畫了:
這樣看起來是不是好多啦?
現在,關於Push動畫的內容就到此結束,接下來,我們開始實現Pop動畫。
實現Pop動畫的助手方法
一個View Controller的Pop動作剛好和Push相反。狀態1是圖書開啟的狀態,而狀態2則變成了書合起的狀態:
Open up BookOpeningTransition.swift and add the following code:
開啟BookOpeningTransition.swift,加入以下方法:
// MARK: Pop methodsfunc setStartPositionForPop(fromVC: BookViewController, toVC: BooksViewController) { // Remove background from the pushed view controller toViewBackgroundColor = fromVC.collectionView?.backgroundColor fromVC.collectionView?.backgroundColor = nil}
setStartPositionForPop(_:toVC)方法僅僅是儲存BookViewController的背景色並將BooksViewController的Collection View的背景色刪除。注意,你不需要建立任何cell動畫,因為書在這個時候是開啟狀態。
接著,在上面的方法後面加入這個方法:
func setEndPositionForPop(fromVC: BookViewController, toVC: BooksViewController) { //1 let coverCell = toVC.selectedCell() //2 for cell in toVC.collectionView!.visibleCells() as! [BookCoverCell] { if cell != coverCell { cell.alpha = 1 } } //3 for cell in fromVC.collectionView!.visibleCells() as! [BookPageCell] { closePageCell(cell) }}
這個方法建立Pop動畫的起止點,即從開啟變成合起:
擷取選擇的書的封面。 在合起狀態,在BooksViewController中遍曆私人書的封面,然後對所有對象進行一個漸入效果。 在BookViewController中遍曆當前圖書的所有頁,將所有cell轉變成合起狀態。
現在建立如下方法:
func cleanupPop(fromVC: BookViewController, toVC: BooksViewController) { // Add background back to pushed view controller fromVC.collectionView?.backgroundColor = self.toViewBackgroundColor // Unhide the original book cover toVC.selectedCell()?.alpha = 1}
這個方法在Pop動畫完成時執行清理動作:將BooksViewController的Collection View的背景色設回它開始的值並顯示封面。
在animateTransition(_:)方法裡面,找到注釋有“//POP”的else語句塊,添加如下代碼:
//1let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BookViewControllerlet toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BooksViewController//2container.insertSubview(toVC.view, belowSubview: fromVC.view)//3setStartPositionForPop(fromVC, toVC: toVC)UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: { //4 self.setEndPositionForPop(fromVC, toVC: toVC)}, completion: { finished in //5 self.cleanupPop(fromVC, toVC: toVC) //6 transitionContext.completeTransition(finished)})
以上代碼解釋如下:
擷取動畫中涉及的兩個ViewController。現在,fromVC 是BookViewController (開啟狀態),toVC是BooksViewController(合起狀態)。 向Container View中加入BooksViewController(在BookViewContorller的下方)。 setStartPositionForPop(_:toVC) 方法先儲存背景色,再將背景色設為nil。 執行動畫,即從開啟狀態切換到合起狀態。 動畫完成,執行清理動作。將背景色設回原來值,顯示封面。 通知動畫完成。在Navigation Controller中應用Pop動畫
現在需要建立Pop動畫,就如同我們在Push動畫所做一樣。
開啟BooksViewController.swift,在animationControllerForPresentController(_:)方法後增加如下方法:
func animationControllerForDismissController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? { var transition = BookOpeningTransition() transition.isPush = false self.transition = transition return transition}
這裡,我們建立了一個新的BookOpeningTransition對象,但不同的是isPush設定為false。
開啟CustomNavigationController.swift,然後替換Pop部分的if語句為:
if operation == .Pop { if let vc = toVC as? BooksViewController { return vc.animationControllerForDismissController(vc) }}
上述代碼返回一個Transition對象,並執行Pop動畫,合起書本。
編譯,運行程式,選擇一本書,查看它的開啟和合起。如所示:
建立互動式的Navigation Controller
開啟和合起動畫搞定了——但我們還能更進一步!我們為什麼不用一個更直觀的捏放手勢來開啟和合起書本呢?
開啟BookOpeningTransition.swift,增加如下屬性定義:
// MARK: Interaction Controllervar interactionController: UIPercentDrivenInteractiveTransition?
然後開啟CustomNavigationController.swift,加入下列代碼:
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { if let animationController = animationController as? BookOpeningTransition { return animationController.interactionController } return nil}
在這個方法中,我們從BookOpeningTransition對象獲得了一個interactionController。這樣導航控制器能夠跟蹤動畫進程以便使用者可以用捏放手勢開啟和合起書。
開啟BooksViewController.swift,在trnasitoin變數下增加如下屬性:
//1var interactionController: UIPercentDrivenInteractiveTransition?//2var recognizer: UIGestureRecognizer? { didSet { if let recognizer = recognizer { collectionView?.addGestureRecognizer(recognizer) } }}
這兩個屬性的作用分別是:
interactionController 是一個UIPercentDrivenInteractiveTransition類,它負責管理View Contorller之間轉場的自訂動畫。interactionController由一個Transition Animator產生,後者是一個實現了UIViewControllerAnimatorTransitioning協議的對象。而我們已經擁有了BookOpeningTransition——這就是一個實現了UIViewControllerAnimatorTransitioning的對象。interactionController能夠控制Push動畫和Pop動畫之間的進度。關於這個類的更多內容,請參考Apple官方文檔。 recognizer 是一個UIGestureRecognizer。我們用這個手勢辨識器實現以捏放手勢開合書本。
在BooksViewController擴充的animationControllerForPresentController(_:)方法中,transition.isPush=true一行下面,加入代碼:
transition.interactionController = interactionController
這句代碼讓CustomNavigationController知道要用哪個interaction controller。
在animationControllerForDismissController(_:)方法中transition.isPush=false一行下面加入同樣的代碼:
transition.interactionController = interactionController
在viewDidLoad()方法中增加代碼:
recognizer = UIPinchGestureRecognizer(target: self, action: handlePinch:)
這裡我們初始化了一個UIPinchGestureRecognizer,允許使用者在做出捏放手勢時調用handlePinch(_:)方法。
在viewDidLoad()方法下面實現這個方法:
// MARK: Gesture recognizer actionfunc handlePinch(recognizer: UIPinchGestureRecognizer) { switch recognizer.state { case .Began: //1 interactionController = UIPercentDrivenInteractiveTransition() //2 if recognizer.scale >= 1 { //3 if recognizer.view == collectionView { //4 var book = self.selectedCell()?.book //5 self.openBook(book) } //6 } else { //7 navigationController?.popViewControllerAnimated(true) } case .Changed: //8 if transition!.isPush { //9 var progress = min(max(abs((recognizer.scale - 1)) / 5, 0), 1) //10 interactionController?.updateInteractiveTransition(progress) //11 } else { //12 var progress = min(max(abs((1 - recognizer.scale)), 0), 1) //13 interactionController?.updateInteractiveTransition(progress) } case .Ended: //14 interactionController?.finishInteractiveTransition() //15 interactionController = nil default: break }}
對於UIPinchGestureRecognizer,我們要關注這3個狀態:開始狀態,這讓你知道捏放手勢何時開始;改變狀態,檢測捏放手勢的變化;結束狀態,讓你知道捏放手勢何時結束。
handlePinch(_:)方法代碼解釋如下:
開始狀態
1. 建立一個UIPercentDrivenInteractiveTransition 對象。
2. scale取決於捏合點之間的距離,判斷scale值是否大於或者等於1。
3. 如果是,判斷相關的View是否是一個Collection View。
4. 擷取正在被捏合的書。
5. 執行Push BookViewController的動畫,顯示書本中的書頁。
6. 如果 scale 小於 1…
7. …執行Pop BookViewController的動畫,顯示封面
改變狀態 – 捏合過程中
8. 判斷當前是否是Push動畫。
9. 如果正在Push一個BookViewConroller,計算捏放手勢的進度。該進度必然是0-1之間的數字。我們將原始值除以5以讓使用者擁有更好的控制感。否則用雙指開啟的手勢開啟一本書時,會突然跳到開啟狀態。
10. 基於我們計算的進度,更新動畫進度。
11. 如果當前不是Push動畫,則它應該是Pop動畫。
12. 當雙指捏合合起一本書時,scale值必然是從1慢慢變到0。
13. 最後, 更新動畫進度。
結束狀態 – 手勢終止
14. 告訴系統,使用者互動式動畫完成。
15.將interaction controller 設定為 nil。
最後,我們需要實現“捏合以合起書本”的狀態。當然,我們必須將手勢辨識器傳遞給BookViewController以便它會Pop。
開啟BookViewController.swift,在book變數聲明下增加一個屬性:
var recognizer: UIGestureRecognizer? { didSet { if let recognizer = recognizer { collectionView?.addGestureRecognizer(recognizer) } }}
當我們將手勢辨識器傳遞給BookViewController時,它會被添加到Collection View,因此我們可以跟蹤到使用者的“關書”手勢。
然後需要在BooksViewController和BookViewController之間傳遞手勢辨識器。
開啟BookOpeningTransition.swift。在cleanUpPush(_:toVC)方法中,在設定背景色之後添加如下代碼:
// Pass the gesture recognizertoVC.recognizer = fromVC.recognizer
當我們從BooksViewController Push到BookViewController時,將捏放手勢傳遞給BookViewController。這會導致捏放手勢自動添加到Collection View中。
當我們從BookViewController Pop回BooksViewController時,我們必須將捏放手勢又傳遞迴去。
在cleanUpPop(_:toVC)方法中,在我設定背景色之後添加如下代碼:
// Pass the gesture recognizertoVC.recognizer = fromVC.recognizer
編譯、運行程式,選擇一本書,用捏放手勢開啟和合起書:
捏放手勢是一種天然就適合用於對書本進行“開關”的手勢;它讓我們的介面顯得更加簡單。我們不再需要導覽列的Back按鈕——因此我們決定去掉它。
開啟Main.storyboard,選擇Custom Navigation View Controller,開啟屬性面板,在Navigation Controller一欄下面,取消Bar Visibility選項,如下所示:
再次編譯運行程式:
接下來做什麼
你可以在這裡下載到上面所有步驟完成後的最終項目。
在本教程中,我們學習如何對Collection View進行自訂布局,讓App的使用者體驗更加自然、也更加有趣。我們還建立了自訂動畫,使用智能互動讓使用者以捏放手勢開合一本書。這個App在實現了所有準系統的同時,讓程式顯得更加的人性化和與眾不同。
相比較之下,是預設的“淡入/淡出”動畫更簡單一些。它能節省你一部分開發時間。但是傑出的應用程式都應當有一些自己特有的地方,從而使它們能夠脫穎而出。
要知道,每個人都喜歡記住那些用起來非常有趣的App,在UI上能讓人感到興奮而同時又沒有犧牲功能的App。
希望你能喜歡本教程,再次感謝Attila Hegedüs提供了這個教程的樣本項目。