iOS中的转场动画一共有三种:Modal转场,Navigation转场,Tabbar转场。这篇文章主要来介绍如何在iOS中自定义Modal转场的动画效果
Modal转场中的一些核心概念
关于命名:
在Modal转场的过程中,iOS对于参与转场的角色有两种命名法。第一种是Presenting-Presented,发起presentation的那个控制器在整个过程中永远命名为presenting,被弹出的那个控制器在整个过程中永远被命名为presented。第二种是from-to,在presented(发起调用,弹出控制器)的过程中,发起的那个为from,要弹出的那个为to,在dismiss(发起返回,返回父级)的过程中,发起返回的那个为from,返回的那个父级为to
转场中动画如何实施
发生Modal转场动画时,系统会在屏幕上为我们提供一个containerView用来承载涉及转场的两个控制器的view,并且将涉及跳转操作的两个控制器中的view提供给我们操作,它们分别是fromView和toView。我们无需关心该containerView在哪里创建,以及层级如何,我们只需要关心,在我们要进行转场动画的时候,UIKit总会为我们提供一个可用的containerView,并且自动的将fromView添加为containerView的子视图。比如一个Modal跳出的时候,前一个view缩小旋转并向左移动出屏幕,后一个view从右向左滑出的一个转场动画,我们可以使用UIView的animation,在动画实施的时候分别操作fromView的frame与transition属性,以及toView的frame属性来实现这个动画。可以看下面的图片来理解这个过程
Modal转场动画中的关键角色
UIViewControllerContextTransitioning:动画上下文
作用:
在一次转场开始之前,UIKit会为我们自动创建一个上下文对象,并且填充里面的属性,我们在动画执行的过程中经常需要用到上下文中设置的属性。上下文存在于整个转场的过程中,我们需要通过上下文拿到需要的属性,以及通过上下文通知系统一些时间,比如动画已经完成的事件。该协议只用来声明上下文中有哪些我们可以使用的属性以及方法,我们不能去自定义一个该协议的实现类,因为这么做没有意义,上下文是由系统自动创建并且传递给特定方法的。
定义:
这里只给出部分比较重要的方法或者属性,context中定义了许多东西,但这些东西一般都是用来传递上下文信息或者控制动画过程的,有需要可以去官方文档看
1 | protocol UIViewControllerContextTransitioning { |
使用:
- 通过
view(forKey: .from)
与view(forKey: .to)
拿到参与转场的两个控制器的rootView - 通过
viewController(forKey: .from)
与viewController(forKey: .to)
拿到参与转场的两个控制器 - 通过
finalFrame(for: someController)
拿到系统给出的控制器rootView最终应该在的位置的参考。我们在自定义动画的过程中,应当让我们的view朝着这个frame与移动,并且在动画完成时应该设置该frame为view的最终frame - 通过
transitionWasCancelled
属性获知当前的转场动画是否因为意外而取消,在自定义的动画执行过程中,如果发现次属性为true,就需要自己做出相关的处理 - 通过
completeTransition(true)
方法调用来显式声明动画转场已经结束,我们在自定义转场动画代码中,在转场正常完成后,必须要主动调用该方法。因为手动调用该方法之后,iOS系统才会感知到转场的结束从而去做一些“善后”工作,这些工作包括:调用presentViewController:animated:completion:
方法中用户传入的闭包、将fromView从containerView上清除、调用动画对象的animationEnded
方法,通知动画已经结束。
UIViewControllerAnimatedTransitioning
作用:
动画实现器,提供一个将fromView隐藏并显现出toView的动画过程
定义:
1 | func animationEnded(Bool) |
实现一个动画类的过程:
- 设置动画需要执行的时间
- 实现animateTransition方法
实现animateTransition方法的过程:
- 根据上下文参数获取到FromController,ToController,FromView,ToView这些基本信息
- 根据上下文参数获取动画结束之后toView的位置frame
- 给toView设置合适的位置以及相关属性,同时也给涉及转场的fromView设置合适的相关属性
- 将toView添加到containerView中
- 使用UIView或者CoreAnimation实现相关的动画,并且在动画的最后,使toView的位置移动到第2步中获取的结束frame处
- 在动画结束的completion中,判断转场是否正常结束,如果没有正常结束则进行自己的相关处理,如果正常结束了,那么手动调用上下文的
completeTransition(true)
方法结束转场
UIViewControllerInteractiveTransitioning:可交互的动画控制器
作用:
代表一个可以控制动画过程的对象,使用该对象可以控制转场动画的进度,来完成一些可交互的动画效果。比如右滑返回,在我们右滑的过程中需要随时控制动画的进度以及停止和结束
定义:
1 | // 开始对一个转场动画应用交互控制器,我们在该方法中可以修改动画上下文的相关属性 |
使用系统提供的实现类:
我们直接自定义类去实现UIViewControllerInteractiveTransitioning这个协议并不是很轻松的,因为我们需要从头开始,自己控制动画的完成曲线和完成速度,这需要我们做非常多的工作才能写出一个完善的动画控制器。系统给我们提供了一个已经实现了这个协议的类,这个类是UIPercentDrivenInteractiveTransition,他给我们提供了一种通过进度值来控制动画运行的便捷的方式,99%的场景下我们单独使用系统给我们提供的这个类已经够了,而不用去自己自定义类了
1 | class UIPercentDrivenInteractiveTransition { |
我们可以创建出一个UIPercentDrivenInteractiveTransition的对象,然后绑定一些交互事件,比如我们创建一个对象之后,就给当前页面的手势添加一个响应事件,在事件中根据手势滑动的进度来更新动画的进度,以及在合适的地方调用方法关闭动画(比如手势滑动距离不到一半停止)或者完成动画(比如手势滑动距离超过一半后停止)
UIViewControllerTransitionCoordinator:动画协调器
作用:
帮助我们在转场动画进行的同时并行执行其他的动画,是我们可以在转场的时候控制一些其他元素与转场动画同步,实现更加自由的转场动画效果。该类型由系统内部创建,不需要我们提供实现类,因为那并没有意义。我们可以通过controller中一个方法transitionCoordinator()来获取到控制器的这个对象。这个对象会在控制器参与转场之前被系统自动创建,在非转场的情况下调用此方法会返回nil。该方法主要记录转场动画的时间等属性,方便将其他动画与其同步
使用:
- 协调器执行与正常转场动画同步的动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/*需要执行动画的view是位于containerView中的情况*/
coordinator.animate(alongsideTransition: { context in
/*要执行的动画的代码*/
/*直接设置特定UIView的属性,协调器会帮助我们自动同步动画*/
someView.alpha = 1.0
}, completion: { context in
/*动画执行结束之后的block*/
})
/*需要执行动画的view是位于containerView之外的情况*/
coordinator.animateAlongsideTransition(in: someView, animation: { context in
/*要执行的动画的代码*/
/*直接设置特定UIView的属性,协调器会帮助我们自动同步动画*/
someView.alpha = 1.0
}, completion: { context in
/*动画执行结束之后的block*/
}) - 协调器执行与交互式动画同步的动画
1
2
3
4
5/*该方法会注册一个block,并在每次交互式动画控制器状态改变时(比如动画进度)调用*/
func notifyWhenInteractionChanges { context in
/*根据现在交互式动画执行的状态来调整自定义的view需要随之变化的属性*/
someView.alpha = someCalc(context.someProps)
}
UIPresentationController:Presentation控制器
作用:
其实在iOS系统中,所有通过present弹出的控制器都会包裹在UIPresentationController中。UIPresentationController的作用就是对presentedController提供额外的管理,其内部有一个containerView(注意这个containerView与转场中出现的那个概念是不同的),用来承载presentedController的rootView以及用户额外添加的view。UIPresentationController可以管理presentedController的rootView在containerView中的相对位置与大小,我们可以提供自定义的view作为背景(比如一个模糊的背景view),并且还可以提供额外的手势等交互逻辑(比如触摸周围的透明部分就执行dismiss)
为什么要为自定义的present转场动画中的控制器的
modalPresentationStyle
属性设置为custom
?
系统为系统内置的几种转场动画效果提供了不同的UIPresentationController子类实现,比如UIModalPresentationFullScreen
类型就对应着内部设置presentedController的frame充满整个containerView的一个UIPresentationController子类实现。再比如UIModalPresentationFormSheet
类型对应着一个内部设置presentedController的frame不充满整个containerView,并且在containerView中添加一个暗视图的一个UIPresentationController子类实现。如果我们没有修改控制器的modalPresentationStyle为Custom,系统在转场的时候为目标控制器提供UIPresentationController的时候就可能使用系统内置的其他实现,从而产生一些歧义
如何实现一个自定义的UIPresentationController:
- 创建自定义的类继承自系统的UIPresentationController
- 重写类中的
frameOfPresentedViewInContainerView
方法,返回presentedController的rootView在containerView之上的相对位置 - 重写类的初始化方法
initWithPresentedViewController:presentingController:
,在初始化方法中负责创建一些自定义的view,以及添加一些自定义的逻辑(比如手势控制点击rootView之外containerView之上的地方就主动dismiss) - 重写类的方法
presentationTransitionWillBegin
,该方法会在控制器将要被presented的时候调用,在该方法中将自定义的view添加到containerView上去,并设置一些自定义view的一些动画相关的初始属性(如果有动画效果的话)。注意你不用主动将rootView添加到containerView上,UIKit内部会自动执行这件事情。之后调用presentedController的协调器的方法执行转场时自定义view需要执行的动画效果。 - 重写类的方法
presentationTransitionDidEnd:
,转场动画关闭时调用,如果是正常结束并完成转场则会传入参数YES,如果是中途结束未完成转场(比如交互式动画手势取消)会传入参数NO,针对这种非正常取消的情况我们需要做一些额外操作,比如移除我们之前添加的自定义view - 重写类的方法
dismissalTransitionWillBegin
,该方法在dismissal将要发生时调用,方法内部的逻辑与第4步中的presentationTransitionWillBegin
方法基本一致 - 重写类的方法
dismissalTransitionDidEnd:
,该方法在dismissal将要发生时调用,方法内部的逻辑与第5步中的presentationTransitionDidEnd:
方法基本一致
UIViewControllerTransitioningDelegate:转场代理
在哪里使用:
每一个Controller中拥有一个UIViewControllerTransitioningDelegate协议类型的代理属性,用来响应本Controller中Modal转场的相关事件
1 | weak var transitionDelegate:UIViewControllerTransitioningDelegate? |
定义:
1 | protocol UIViewControllerTransitioningDelegate { |
Modal自定义转场中的调用流程
Present过程(A Present To B)
- 检查B控制器的
transitionDelegate
是否不为空,如果不为空,代表提供了自定义的转场动画,如果没有则会使用系统默认的动画效果 - 访问B控制器的
modalPresentationStyle
,如果为其他系统中自定义的style,则提供一个与之对应的UIPresentationController,如果为custom,则访问代理的presentationController
方法获取自定义的presentationController,如果该方法返回为空,则会返回一个默认实现(rootView的frame充满containerView,无任何其他多余view) - 访问B控制器代理的
animationController
方法获取一个动画控制器,如果返回为空,则执行系统默认的动画过程 - 检查B控制器代理的
interactionControllerForPresentation
方法是否返回一个非空的交互动画控制器,如果返回了则会更新上下文中相关信息,为交互动画控制做一些准备 - 系统准备好所有东西时,转场动画准备开始,调用UIPresentationController的
presentationTransitionWillBegin
方法 - 渲染动画器控制的动画以及向协调器提交的动画
- UIKit等待动画渲染完毕并主动调用上下文的
completeTransition:
方法 - 调用UIPresentationController的
presentationTransitionDidEnd
方法
dismissal过程(B Dismiss To A)
- 检查B控制器的
transitionDelegate
是否不为空,如果不为空,代表提供了自定义的转场动画,如果没有则会使用系统默认的动画效果 - 访问B控制器的
modalPresentationStyle
,如果为其他系统中自定义的style,则提供一个与之对应的UIPresentationController,如果为custom,则访问代理的presentationController
方法获取自定义的presentationController,如果该方法返回为空,则会返回一个默认实现(rootView的frame充满containerView,无任何其他多余view) - 访问B控制器代理的
animationController
方法获取一个动画控制器,如果返回为空,则执行系统默认的动画过程 - 检查B控制器代理的
interactionControllerForDismissal
方法是否返回一个非空的交互动画控制器,如果返回了则会更新上下文中相关信息,为交互动画控制做一些准备 - 系统准备好所有东西时,转场动画准备开始,调用UIPresentationController的
dismissalTransitionWillBegin
方法 - 渲染动画器控制的动画以及向协调器提交的动画
- UIKit等待动画渲染完毕并主动调用上下文的
completeTransition:
方法 - 调用UIPresentationController的
dismissalTransitionDidEnd:
方法
如何实现自定以转场动画
- 设置目标控制器的
modalPresentationStyle
属性为custom - 设置目标控制器的
transitionDelegate
指向一个实现了UIViewControllerTransitioningDelegate
协议的对象 - 创建一个实现了
UIViewControllerContextTransitioning
协议的类,该类的实现方法可以参考上面的UIViewControllerContextTransitioning
协议介绍中的如何实现一个动画类的过程
- 如果是交互式的动画(比如手势拖动控制),那么创建一个继承于
UIPercentDrivenInteractiveTransition
的子类,在子类中监听手势(或者其他交互方式)的变动,并在合适的地方调用父类的方法控制动画进度 - 是否需要使用到UIPresentationController(比如是否需要实现弹出视图并不完全覆盖屏幕,屏幕空白部分需要附加模糊试图,点击屏幕空白部分需要退出等效果),如果需要用到,则创建一个继承于UIPresentationController的子类,重写子类中的相关方法实现你的逻辑。关于实现方法可以参考上面的
如何实现一个自定义的UIPresentationController
实际应用
最简单的淡入淡出动画实现
假设A为源控制器,B为跳转的目标控制器
1 | //首先创建一个动画类,实现UIViewControllerAnimatedTransitioning协议来控制动画 |
可交互的动画应用
1 | //实现一个可交互的动画控制器,遵循UIViewControllerInteractiveTransitioning协议 |