关于转场动画之模态转场

视图控制器转换(View Controller Transition) 俗称 关于转场动画

视图控制器中的视图显示在屏幕上有两种方式:最主要的方式是内嵌在容器控制器中,比如 UINavigationController,UITabBarController, UISplitController;由另外一个视图控制器显示它,这种方式通常被称为模态(Modal)显示。View Controller Transition 是什么?在 NavigationController 里 push 或 pop 一个 View Controller,在 TabBarController 中切换到其他 View Controller,以 Modal 方式显示另外一个 View Controller,这些都是 View Controller Transition。
UINavigationController 和 UITabBarController 这两个容器 VC 的根视图在屏幕上是不可见的(或者说是透明的),可见的只是内嵌在这两者中的子 VC 中的视图,转场是从子 VC 的视图转换到另外一个子 VC 的视图,其根视图并未参与转场;而 Modal 转场,以 presentation 为例,是从 presentingView 转换到 presentedView,根视图 presentingView 也就是 fromView 参与了转场。而且 NavigationController 和 TabBarController 转场中的 containerView 也并非这两者的根视图。

划重点

  1. UIModalPresentationFullScreen 模式:presentation 后,presentingView 被主动移出视图结构,在 dismissal 中 presentingView 是 toView 的角色,其将会重新加入 containerView 中,实际上,我们不主动将其加入,UIKit 也会这么做,前面的两种容器控制器的转场里不是这样处理的,不过这个差异基本没什么影响。
  2. UIModalPresentationCustom 模式:转场时 containerView 并不担任 presentingView 的父视图,后者由 UIKit 另行管理。在 presentation 后,fromView(presentingView) 未被移出视图结构,在 dismissal 中,注意不要像其他转场中那样将 toView(presentingView) 加入 containerView 中,否则本来可见的 presentingView 将会被移除出自身所处的视图结构消失不见。如果你在使用 Custom 模式时没有注意到这点,就很容易掉进这个陷阱而很难察觉问题所在。

小结:经过上面的尝试,建议是,不要干涉官方对 Modal 转场的处理,我们去适应它。在 Custom 模式下,由于 presentingView 不受 containerView 管理,在 dismissal 转场中不要像其他的转场那样将 toView(presentingView) 加入 containerView,否则 presentingView 将消失不见,而应用则也很可能假死;而在 presentation 转场中,切记不要手动将 fromView(presentingView) 移出其父视图。


模态的转场代理

1
@property (nullable, nonatomic, weak) id <UIViewControllerTransitioningDelegate> transitioningDelegate NS_AVAILABLE_IOS(7_0);

模态转场动画及手势代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
和Nav的不同 模态的present和dismiss是分开的
presented 要跳转的vc
presenting 当前的 如果当前的是放在Nav上的vc 那么这个就是Nav
source 当前的vc
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
模态的手势present和dismiss也是分开的
不需要的返回nil
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
8.0以后的api
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);

手势处理是和Nav的处理是一样的

iOS 8 api:UIPresentationController

iOS 8 针对分辨率日益分裂的 iOS 设备带来了新的适应性布局方案,以往有些专为在 iPad 上设计的控制器也能在 iPhone 上使用了,一个大变化是在视图控制器的(模态)显示过程,包括转场过程,引入了UIPresentationController类,该类接管了 UIViewController 的显示过程,为其提供转场和视图管理支持。当 UIViewController 的modalPresentationStyle属性为.Custom时(不支持.FullScreen),我们有机会通过控制器的转场代理提供UIPresentationController的子类对 Modal 转场进行进一步的定制。官方对该类参与转场的流程和使用方法有非常详细的说明:Creating Custom Presentations

UIPresentationController类主要给 Modal 转场带来了以下几点变化:

  1. 定制 presentedView 的外观:设定 presentedView 的尺寸以及在 containerView 中添加自定义视图并为这些视图添加动画;
  2. 可以选择是否移除 presentingView;
  3. 可以在不需要动画控制器的情况下单独工作;
  4. iOS 8 中的适应性布局。

以上变化中第1点 iOS 7 中也能做到,3和4是 iOS 8 带来的新特性,只有第2点才真正解决了 iOS 7 中的痛点。在 iOS 7 中定制外观时,动画控制器需要负责管理额外添加的的视图,UIPresentationController类将该功能剥离了出来独立负责,其提供了如下的方法参与转场,对转场过程实现了更加细致的控制,从命名便可以看出与动画控制器里的animateTransition:的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//presentationTransitionWillBegin 是在呈现过渡即将开始的时候被调用的。
- (void)presentationTransitionWillBegin;
//presentationTransitionDidEnd: 是在呈现过渡结束时被调用的,并且该方法提供一个布尔变量来判断过渡效果是否完成。
- (void)presentationTransitionDidEnd:(BOOL)completed;
- (void)dismissalTransitionWillBegin;
- (void)dismissalTransitionDidEnd:(BOOL)completed;
//处理被呈现的 view 并没有完全填充整个屏幕,而是很小的一个矩形。
- (CGRect)frameOfPresentedViewInContainerView{
CGFloat windowH = [UIScreen mainScreen].bounds.size.height;
CGFloat windowW = [UIScreen mainScreen].bounds.size.width;
self.presentedView.frame = CGRectMake(0, windowH - 300, windowW, 300);
return self.presentedView.frame;
}
没有 presentingView 是因为 Custom 模式下 presentingView 不受 containerView 管理,UIPresentationController类并没有改变这一点。iOS 8 扩充了转场环境协议,可以通过viewForKey:方便获取转场的视图,而该方法在 Modal 转场中获取的是presentedView()返回的视图。因此我们可以在子类中将 presentedView 包装在其他视图后重写该方法返回包装后的视图当做 presentedView 在动画控制器中使用。
参与角色都准备好了,但有个问题,无法直接访问动画控制器,不知道转场的持续时间,怎么与转场过程同步?这时候前面提到的用处甚少的转场协调器(Transition Coordinator)将在这里派上用场。该对象可通过 UIViewController 的transitionCoordinator()方法获取,这是 iOS 7 为自定义转场新增的 API,该方法只在控制器处于转场过程中才返回一个与当前转场有关的有效对象,其他时候返回 nil。
1
2
3
4
//与动画控制器中的转场动画同步,执行其他动画
animateAlongsideTransition:completion:
//与动画控制器中的转场动画同步,在指定的视图内执行动画
animateAlongsideTransitionInView:animation:completion:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 使用Coordinator可以在整个viewcontroller转场动画中增加一些你自己的额外的视图元素的动画
*/
[self.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
UIView *containerView = [context containerView];
UIView *fromeView = [context viewForKey:UITransitionContextFromViewKey];
UIView *toView = [context viewForKey:UITransitionContextToViewKey];
UIViewController *fromeVC = [context viewControllerForKey: UITransitionContextFromViewControllerKey];
UIViewController *toVC = [context viewControllerForKey: UITransitionContextToViewControllerKey];
NSLog(@"%@-%@-%@-%@-%@",containerView,fromeView,toView,fromeVC,toVC);
/*
<UITransitionView: 0x7f97c5b009e0; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x604000226360>>-(null)-<UIView: 0x7f97c5901140; frame = (0 436; 414 300); autoresize = W+H; gestureRecognizers = <NSArray: 0x608000257ee0>; layer = <CALayer: 0x6080002281e0>>-<UITabBarController: 0x7f97c501ec00>-<FViewController: 0x7f97c2e0fdd0>
*/
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
NSLog(@"completion");
}];
//当手势动结束后会回调该接口
// [self.transitionCoordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
// NSLog(@"notifyWhenInteractionEndsUsingBlock");
// }];
OverlayPresentationController类接手了 dimmingView 的工作后,需要回到上一节OverlayAnimationController里把涉及 dimmingView 的部分删除,然后在 presentedVC 的转场代理属性transitioningDelegate中提供该类实例就可以实现和上一节同样的效果。

介绍 UIViewControllerTransitionCoordinator


最后说下坑

写动画的时候要注意style是FullSceen还是Custom,差别见上面划的重点
iOS 8 为< UIViewControllerContextTransitioning >协议添加了viewForKey:方法以方便获取 fromView 和 toView,但是在 Modal 转场里要注意,从上面可以知道,Custom 模式下,presentingView 并不受 containerView 管理,这时通过viewForKey:方法来获取 presentingView 得到的是 nil,必须通过viewControllerForKey:得到 presentingVC 后来获取。因此在 Modal 转场中,较稳妥的方法是从 fromVC 和 toVC 中获取 fromView 和 toView。

github-demo