之前略微嘗試了自定義view controller間的轉場動畫,然后發現,其實UINavigationController
也可以自定義push和pop的轉場動畫,便也寫了個demo實驗了一下。
代碼放在這里->github
自定義push和pop動畫
還是以最老土的zoom效果來舉例好了(⊙ω⊙)
首先我們定義了XSQMasterViewController
和XSQDetailViewController
這兩個視圖控制器,它們在同一個導航棧中,當點擊XSQMasterViewController
中的一個按鈕時,XSQDetailViewController
就會被push到導航棧中展現出來。
為了自定義這一轉場動畫,需要給XSQNavigationController
對象設定一個delegate。這個delegate對象需要實現UINavigationControllerDelegate
接口,其中有兩個方法和轉場動畫有關,分別是:
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
和
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController
第一個方法可以用來自定義一個不帶用戶交互的轉場動畫,而第二個方法可以為這個動畫添加用戶交互。
然后,我們需要創建一個轉場動畫對象,來作為第一個方法的返回值。如果給第一個方法返回nil
,則UIKit會使用默認的轉場動畫效果。
創建一個類,我把它命名為XSQExpandAnimatorObject
,它需要實現UIViewControllerAnimatedTransitioning
協議。這個類中,定義了XSQDetailViewController
從XSQMasterViewController
上展開的動畫。
我這樣實現了在XSQExpandAnimatorObject
中這樣實現了UIViewControllerAnimatedTransitioning
中的兩個方法:
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
return 1.0;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
CGRect thumbFrame = [[transitionContext containerView] convertRect:self.thumbView.bounds fromView:self.thumbView];
[toView setFrame:thumbFrame];
[[transitionContext containerView] addSubview:toView];
CGRect toViewFinalFrame = [transitionContext finalFrameForViewController:toVC];
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
[toView setFrame:toViewFinalFrame];
}
completion:^(BOOL finished) {
if (![transitionContext transitionWasCancelled]) {
[fromView removeFromSuperview];
[transitionContext completeTransition:YES];
}
else {
[toView removeFromSuperview];
[transitionContext completeTransition:NO];
}
}];
}
然后將一個XSQExpandAnimatorObject
的對象作為UINavigationControllerDelegate
第一個方法的返回值返回:
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC {
if (operation == UINavigationControllerOperationPush && [fromVC isKindOfClass:[XSQMasterViewController class]] && [toVC isKindOfClass:[XSQDetailViewController class]]) {
XSQMasterViewController *masterViewController = (XSQMasterViewController *)fromVC;
return [[XSQExpandAnimatorObject alloc] initWithThumbView:masterViewController.thumbView];
}
return nil;
}
這樣,當一個XSQDetailViewController
被push到XSQMasterViewController
之上時,便會使用我們自定義的zoom效果。
反向的pop動畫的實現方式也類似。
用UIPercentDrivenInteractiveTransition為轉場動畫添加交互
在完成了沒有交互的自定義轉場動畫后,我嘗試了為轉場動畫添加簡單的交互。最簡單的方式應該就是利用UIKit提供的UIPercentDrivenInteractiveTransition
類了,這個類已經實現了UIViewControllerInteractiveTransitioning
協議,第三方程序員可以通過這個類的對象指定轉場動畫的完成百分比。
比如我們可以在XSQDetailViewController
中添加一個手勢,當用戶下拉時執行pop操作,并且轉場動畫隨著用戶下拉的幅度運動:
- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer {
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
static CGFloat beginY;
CGFloat currentY = [gestureRecognizer translationInView:window].y;
CGFloat percent = (currentY - beginY) / CGRectGetHeight(window.bounds);
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:
beginY = [gestureRecognizer translationInView:window].y;
[self.navigationController popViewControllerAnimated:YES];
break;
case UIGestureRecognizerStateChanged:
[self.interactiveTransition updateInteractiveTransition:percent];
break;
case UIGestureRecognizerStateEnded:
if (percent > 0.5) {
[self.interactiveTransition finishInteractiveTransition];
}
else {
[self.interactiveTransition cancelInteractiveTransition];
}
break;
default:
break;
}
}
調用UIPercentDrivenInteractiveTransition
的updateInteractiveTransition:
方法可以控制轉場動畫進行到哪了,當用戶的下拉手勢完成時,調用finishInteractiveTransition
或者cancelInteractiveTransition
,UIKit會自動執行剩下的一半動畫,或者讓動畫回到最開始的狀態。
對比UINavigationController的默認轉場動畫
在寫這個demo的時候,我還想到了一些問題。嗯,其實更多的時間是花在想這些問題上(⊙ω⊙)。
一. 在push的過程中,這個XSQDetailViewController
對象是什么時候進入導航棧的呢?而在pop的過程中,它又是什么時候被移出導航棧的呢?
我曾以為addChildViewController:
和removeFromParentViewController
的操作也是需要第三方程序員在animateTransition:
方法中完成,后來發現UIKit已經為我們做好了。
在push的過程中,UINavigationController
的pushViewController:animated:
方法引起了對XSQDetailViewController
中willMoveToParentViewController:
方法的調用,而自定義動畫完成時的[transitionContext completeTransition:YES];
則引起了對XSQDetailViewController
中didMoveToParentViewController:
方法的調用。
比較神奇的是,XSQNavigationController
中的addChildViewController:
方法卻沒有被調用,估計是UIKit直接通過私有方法完成了這個操作。
類似的,在pop的過程中,popViewControllerAnimated:
方法引起了對XSQDetailViewController
中willMoveToParentViewController:
方法的調用,自定義動畫完成時的[transitionContext completeTransition:YES];
則引起了對XSQDetailViewController
中didMoveToParentViewController:
方法的調用。
以及對稱的,XSQDetailViewController
中的removeFromParentViewController
也沒有被調用到。
以上也說明了,在自定義轉場動畫時,對transitionContext
調用completeTransition:
是非常重要的。如果沒有調用這個方法,UIKit會認為轉場動畫仍然在進行,導致之后XSQDetailViewController
的種種狀態都是錯誤的。
二. 應該在什么時候將XSQMasterViewController
的視圖從視圖層次中移除?
如果不自定義轉場動畫,而是使用UINavigationController
默認的轉場動畫,會發現當push動畫完成后,XSQDetailViewController
的視圖完全遮蓋住了XSQMasterViewController
的視圖,此時XSQMasterViewController
的視圖已經不在視圖層次結構中了。
XSQMasterViewController
的視圖是如何從視圖層次結構中被移除的呢?重寫XSQMasterViewController
的loadView
方法,讓XSQMasterViewController
的根視圖使用自定義的XSQView
類的對象,然后重寫XSQView
的removeFromSuperview
方法,會發現,當默認的轉場動畫結束時,removeFromSuperview
方法被調用了:
可能蘋果是出于性能的考慮,只顯示導航棧中棧頂視圖控制器的視圖。所以在實現自定義轉場動畫的時候,我也在動畫結束時將XSQMasterViewController
的視圖從視圖層次中移除了。
三. viewWillAppear
等方法真的和視圖什么時候被顯示有關么
如果自定義轉場動畫中,animateTransition:
中什么也不做,XSQDetailViewController
的viewWillAppear
方法也會被調用。
說明viewWillAppear
方法的調用,和視圖到底有沒有顯示出來似乎并沒有什么關系。
四. navigationController
屬性是什么
蘋果的注釋是這樣寫的:
The nearest ancestor in the view controller hierarchy that is a navigation controller. (read-only)
If the view controller or one of its ancestors is a child of a navigation controller, this property contains the owning navigation controller. This property is nil if the view controller is not embedded inside a navigation controller.
說明navigationController
會返回距離當前視圖控制器最近的、類型為UINavigationController
的祖先視圖控制器。
五. 自定義一個容器類
已經可以為UINavigationController
自定義轉場動畫,是不是再進一步,我們可以自定義一個容器類呢?
然而稍微查了查,原來自定義一個容器類還有許多工作要做。發現了這篇文章Custom Container View Controller,覺得很厲害(☆_☆)
參考
Custom transitions on iOS 7 & a little bit about UX
UINavigationController Class Reference