本文由CocoaChina譯者@ALEX吳浩文翻譯
作者:Andrew Hershberger
原文:Custom Transitions on iOS
本文是iOS自定義視圖控制器轉場系列的第一篇。本文重點在於創建自定義動畫(非交互式)轉場。
當使用傳統的iOS應用程序時,我們經常在視圖間轉場。過去,如果你不想用標准的轉場動畫,全靠你自己,但在iOS 7中蘋果提供了一個新的API讓我們自定義這些動畫。
iOS提供了一些內置的轉場類型。Navigation controllers用push和pop來有層次地導航信息,tab bar controllers用切換tabs來在各部分之間跳轉,所有的視圖控制器可以根據特定任務模態化地present和dismiss另一個視圖控制器。
API介紹
每一個自定義轉場涉及三個主要對象:
from view controller (消失的那個)
to view controller (出現的那個)
一個動畫控制器
自定義轉場和在自定義之前一樣。對於push和pop,意味著調用UINavigationController的push-、pop-、或者set-方法來修改視圖控制器的堆棧。對於切換tabs,意味著修改UITabBarController的selectedIndex或selectedViewController屬性。對於modal,則意味著調用?[UIViewController presentViewController: animated: completion: ]或?[UIViewController dismissViewControllerAnimated: completion: ]。無論哪種情況,這個步驟都確定了“from view controller”和“to view controller”。
使用一個自定義轉場,你需要一個動畫控制器。對我來說這是自定義動畫轉場中最令人困惑的部分,因為每種轉場需要的動畫控制器不同。下表展示了如何為每種轉場提供動畫控制器。記著,委托方法總是返回動畫控制器。
動畫控制器可以是任何遵守UIViewControllerAnimatedTransitioning協議的對象。該協議聲明了兩個必須要實現的方法。一個提供了動畫的時間,另一個執行了動畫。這些方法調用時都傳遞一個上下文。上下文提供了入口來訪問信息和你創建自定義轉場需要的對象。以下是一些重點:
from view controller
to view controller
兩個視圖控制器view的第一幀和最後一幀
container view,根據這篇文檔,“作為的轉場中視圖的父視圖”
重要:上下文還實現了-completeTransition:,你必須在你自定義轉場結束時調用一次。
這是關於自定義轉場所有你需要知道的。讓我們來看一些例子!
例子
所有這些例子都可以在GitHub找到,你可以克隆這些倉庫,然後邊往下看邊試試這些例子。
這三個例子都直接或子類化地使用了TWTExampleViewController。它只是設置了視圖的背景顏色,同時使你能夠通過點擊任何地方來結束例子回到主菜單。
輕彈push和pop
在這個例子中,目標是讓push和pop使用flip動畫而不是標准的slide動畫。一開始我建立一個navigation controller並把TWTPushExampleViewController的實例當作root。TWTPushExampleViewController添加了一個叫“Push”的右按鈕到導航欄。點擊它時,一個新的TWTPushExampleViewController的實例被壓入navigation的堆棧:
- (void)pushButtonTapped { TWTPushExampleViewController *viewController = [[TWTPushExampleViewController alloc] init]; viewController.delegate = self.delegate; [self.navigationController pushViewController:viewController animated:YES]; }
navigation controller的設置發生在TWTExamplesListViewController(運行demo主菜單的視圖控制器)。注意,它把自己置為navigation controller的委托:
- (void)presentPushExample { TWTPushExampleViewController *viewController = [[TWTPushExampleViewController alloc] init]; viewController.delegate = self; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; navigationController.delegate = self; [self presentViewController:navigationController animated:YES completion:nil]; }
這意味著當navigation controller的轉場即將開始時,TWTExamplesListViewController將收到委托信息,並有機會返回一個動畫控制器。對於這種轉場,我使用一個TWTSimpleAnimationController的實例,它是一個+[UIView transitionFromView: toView: duration: options: completion:]的封裝:
- (id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { TWTSimpleAnimationController *animationController = [[TWTSimpleAnimationController alloc] init]; animationController.duration = 0.5; animationController.options = ( operation == UINavigationControllerOperationPush ? UIViewAnimationOptionTransitionFlipFromRight : UIViewAnimationOptionTransitionFlipFromLeft); return animationController; }
如果轉場是一個push,我使用一個從右側的flip,否則,我使用一個從左側的flip。
以下是TWTSimpleAnimationController的實現:
- (NSTimeInterval)transitionDuration:(id)transitionContext { return self.duration; } - (void)animateTransition:(id)transitionContext { UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController]; [toViewController.view layoutIfNeeded]; [UIView transitionFromView:fromViewController.view toView:toViewController.view duration:self.duration options:self.options completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; }
記著,這兩個方法是UIViewControllerAnimatedTransitioning協議的一部分。在動畫控制器運行自定義轉場的時候,它們被UIKit調用。
這裡有一些關於animateTransition:需要注意的事情:
from view controller,to view controller,以及to view controller的最後一幀都從轉場的上下文中提取。其中還有一些其他可提取的信息,但在當前情況下,並不需要所有信息。
+[UIView transitionFromView: toView: duration: options: completion:]負責有層次地添加和刪除視圖。在後面的例子中,我將展示一種手動完成的情況。
在轉場的completion代碼塊中,我調用[transitionContext completeTransition: YES]來告訴系統轉場結束了。如果你忘了這樣做,你將無法與app交互。如果出現這種情況,先檢查這個原因。
以上就是全部!現在有一些值得嘗試的東西:
改變動畫的持續時間來看看它如何影響navigation bar的動畫。
把動畫選項由flip改為page curls。
找一個方法讓navigation的堆棧中的每個視圖控制器能指定自己的動畫控制器。看看在本文最後的推薦模式中提出的方法。
淡入淡出切換tabs
這個例子應該很熟悉。它使用和之前相同的觀點,但使用tab bar controller而不是navigation controller。
以下是TWTExamplesListViewController的設置:
- (void)presentTabsExample { NSMutableArray *viewControllers = [[NSMutableArray alloc] init]; for (NSUInteger i=0; i<3; i++) { TWTChangingTabsExampleViewController *viewController = [[TWTChangingTabsExampleViewController alloc] init]; viewController.delegate = self; viewController.index = i; [viewControllers addObject:viewController]; } UITabBarController *tabBarController = [[UITabBarController alloc] init]; [tabBarController setViewControllers:viewControllers animated:NO]; tabBarController.delegate = self; [self presentViewController:tabBarController animated:YES completion:nil]; }
TWTChangingTabsExampleViewController的index只是一個為每個視圖控制器自定義標簽的方法。就像之前一樣,TWTExamplesListViewController是委托,它將在切換tabs的時候提供動畫控制器:
- (id)tabBarController:(UITabBarController *)tabBarController animationControllerForTransitionFromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { TWTSimpleAnimationController *animationController = [[TWTSimpleAnimationController alloc] init]; animationController.duration = 0.5; animationController.options = UIViewAnimationOptionTransitionCrossDissolve; return animationController; }
看著熟悉嗎?這就夠了。
彈出一個覆蓋視圖
我最喜歡自定義轉場的一個用途,是它可以彈出覆蓋式的視圖控制器。以前,如果你想模仿一個社會化分享菜單,或任何需要呈現視圖控制器的同時保持背後內容的可見,你不得不從許多不幸的選項中做出選擇(直接添加視圖到窗口是我見過的最通用的方法)。然而幸運的是,這不再是必要的。
能這樣用的關鍵原因,是當被彈出視圖控制器的-modalPresentationStyle被置為UIModalPresentationCustom時,彈出視圖控制器就不會自動從視圖層中刪除。為了彈出一個覆蓋視圖,它僅僅如同是離開一般,這樣被彈出的視圖就可以顯示在它上方。
以下是TWTExamplesListViewController的設置:
- (void)presentPresentExample { TWTOverlayExampleViewController *viewController = [[TWTOverlayExampleViewController alloc] init]; viewController.delegate = self; viewController.modalPresentationStyle = UIModalPresentationCustom; viewController.transitioningDelegate = self; [self presentViewController:viewController animated:YES completion:nil]; }
這裡兩個要注意的事情是:modalPresentationStyle被置為UIModalPresentationCustom,被彈出視圖控制器的-transitioningDelegate被置為TWTExamplesListViewController。當轉場開始時,TWTExamplesListViewController收到轉場的委托信息:
- (id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return [[TWTPresentAnimationController alloc] init]; }
如你所見,我創建了一個自定義動畫控制器類作為示例。我將要關注-animateTransition:的實現,因為它與之前的部分並不相同。
開始前,我用transitionContext提取toViewController(被彈出視圖控制器)和containerView(容器視圖)。
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *containerView = [transitionContext containerView];
containerView在轉場中是from view controller和to view controller的父視圖。最初,to view controller的視圖沒有被添加到containerView,它的frame也未確定。我這裡直接賦值frame,你也可以使用auto layout。
CGRect frame = containerView.bounds; frame = UIEdgeInsetsInsetRect(frame, UIEdgeInsetsMake(40.0, 40.0, 200.0, 40.0)); toViewController.view.frame = frame; [containerView addSubview:toViewController.view];
對於這種轉場,我希望這個視圖pop到屏幕上。要做到這一點,我用UIView的spring動畫把視圖的scale從0.3變化到1。
說句題外話,若動畫中scale從大於0的值開始同時alpha從0到1,則可以讓你動畫速度比簡單的scale從0到1更快。試著刪除alpha動畫,再和從0開始的scale動畫比較。
toViewController.view.alpha = 0.0; toViewController.view.transform = CGAffineTransformMakeScale(0.3, 0.3); NSTimeInterval duration = [self transitionDuration:transitionContext]; [UIView animateWithDuration:duration / 2.0 animations:^{ toViewController.view.alpha = 1.0; }]; CGFloat damping = 0.55; [UIView animateWithDuration:duration delay:0.0 usingSpringWithDamping:damping initialSpringVelocity:1.0 / damping options:0 animations:^{ toViewController.view.transform = CGAffineTransformIdentity; } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }];
在completion塊,我調用-completeTransition:,到這裡就完了!現在到了彈回...
在彈回開始時,TWTExamplesListViewController收到轉場的委托信息:
- (id)animationControllerForDismissedController:(UIViewController *)dismissed { return [[TWTDismissAnimationController alloc] init]; }
返回另一個自定義動畫控制器。讓我們看一看-animateTransition::
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; NSTimeInterval duration = [self transitionDuration:transitionContext]; [UIView animateWithDuration:3.0 * duration / 4.0 delay:duration / 4.0 options:UIViewAnimationOptionCurveEaseIn animations:^{ fromViewController.view.alpha = 0.0; } completion:^(BOOL finished) { [fromViewController.view removeFromSuperview]; [transitionContext completeTransition:YES]; }]; [UIView animateWithDuration:2.0 * duration delay:0.0 usingSpringWithDamping:1.0 initialSpringVelocity:-15.0 options:0 animations:^{ fromViewController.view.transform = CGAffineTransformMakeScale(0.3, 0.3); } completion:nil];
如你所見,這個想法是倒轉彈出動畫。需要注意的重要區別是,from view controller的視圖在轉場結束前就從視圖層中被刪除了。
在繼續之前,試著改變一下彈回的動畫。
推薦模式
到這裡,您可能已經注意到這個API在很大程度上依賴於協議和委托方法。因此很容易把自定義轉場編寫得混亂且無法重用。這裡有一些模式幫助我保持整潔:
1.用專門的對象作動畫控制器
雖然一個視圖控制器可以直接代碼量翻倍當作動畫控制器,但幾乎可以肯定這樣做會減少自定義轉場的可重用性。此外,視圖控制器已經因為做得太多而臭名昭著。你可以創建可重用的動畫控制器,只需創建NSObject子類,遵守UIViewControllerAnimatedTransitioning協議,然後在需要的時候返回它們的實例。
我們在Toast裡的TWTSimpleAnimationController就是這樣做的,它很容易重用和用CocoaPod集成。
2.不要讓UINavigationControllerDelegate需要知道轉場的細節
用navigation controller的委托返回動畫控制器是好,如果你想讓所有的push和pop轉場都以同樣的方式進行。然而在某些情況下,你可能希望只自定義一個push或pop。
一種混亂的方式是把navigation controller的委托對象暫時改變成一個知道這種轉場的對象。另一種混亂的方式是使navigation controlle的委托根據特定的from view controllers和to view controllers有邏輯地去確定。顯然,這些都不是好的選擇。
用模式可以干淨利落地解決這個問題,只要向UIViewController添加-pushAnimationController和-popAnimationController屬性。然後navigation controller的委托就可以返回由被push的動畫控制器和被pop視圖控制器指定的動畫控制器。這使得navigation controller的委托保持通用同時避免了委托對象的改變。TWTNavigationControllerDelegate實現了這種模式,它也同時包括在Toast裡。
這樣就結束了這個創建自定義動畫視圖控制器轉場的介紹。一定要看看我們的在Toast裡的視圖控制器轉場模塊,並繼續關注第二部分,該部分將包括自定義交互式轉場。