之前略微尝试了自定义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:方法的调用。
willMoveToParentViewController:方法被调用
didMoveToParentViewController:方法被调用
比较神奇的是,XSQNavigationController中的addChildViewController:方法却没有被调用,估计是UIKit直接通过私有方法完成了这个操作。
类似的,在pop的过程中,popViewControllerAnimated:方法引起了对XSQDetailViewController中willMoveToParentViewController:方法的调用,自定义动画完成时的[transitionContext completeTransition:YES];则引起了对XSQDetailViewController中didMoveToParentViewController:方法的调用。
willMoveToParentViewController:方法被调用
didMoveToParentViewController:方法被调用
以及对称的,XSQDetailViewController中的removeFromParentViewController也没有被调用到。
以上也说明了,在自定义转场动画时,对transitionContext调用completeTransition:是非常重要的。如果没有调用这个方法,UIKit会认为转场动画仍然在进行,导致之后XSQDetailViewController的种种状态都是错误的。
二. 应该在什么时候将XSQMasterViewController的视图从视图层次中移除?
如果不自定义转场动画,而是使用UINavigationController默认的转场动画,会发现当push动画完成后,XSQDetailViewController的视图完全遮盖住了XSQMasterViewController的视图,此时XSQMasterViewController的视图已经不在视图层次结构中了。
XSQMasterViewController的视图是如何从视图层次结构中被移除的呢?重写XSQMasterViewController的loadView方法,让XSQMasterViewController的根视图使用自定义的XSQView类的对象,然后重写XSQView的removeFromSuperview方法,会发现,当默认的转场动画结束时,removeFromSuperview方法被调用了:
removeFromSuperview方法被调用
可能苹果是出于性能的考虑,只显示导航栈中栈顶视图控制器的视图。所以在实现自定义转场动画的时候,我也在动画结束时将XSQMasterViewController的视图从视图层次中移除了。
三. viewWillAppear等方法真的和视图什么时候被显示有关么
如果自定义转场动画中,animateTransition:中什么也不做,XSQDetailViewController的viewWillAppear方法也会被调用。
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









网友评论
[self.interactiveTransition updateInteractiveTransition:percent]; 这一句代码 我始终不能出现效果。