之前略微尝试了自定义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