由于录屏的原因,有些效果无法完全展现,比如它其实还支持横屏。
自定义转场动画的效果实现起来比较复杂,如果仅仅是拷贝一份能够运行的代码却不懂其中原理,就有可能带来各种隐藏的bug。本文由浅入深介绍下面几个知识:
1、传统的基于闭包的实现方式及其缺点
2、自定义present转场动画
3、交互式(Interactive)转场动画
4、转场协调器与UIModalPresentationCustom
5、UINavigationController转场动画
在开始正式的教程前,您首先需要下载demo,在代码面前文字是苍白的,demo中包含的注释足以解释本文所有的知识点。其次,您还得了解这几个背景知识。
From和To
在代码和文字中,经常会出现fromView和toView。如果错误的理解它们的含义会导致动画逻辑完全错误。fromView表示当前视图,toView表示要跳转到的视图。如果是从A视图控制器present到B,则A是from,B是to。从B视图控制器dismiss到A时,B变成了from,A是to。用一张图表示:
Presented和Presengting
这也是一组相对的概念,它容易与fromView和toView混淆。简单来说,它不受present或dismiss的影响,如果是从A视图控制器present到B,那么A总是presentedViewController,B总是presentingViewController。
modalPresentationStyle
这是一个枚举类型,表示present时动画的类型。其中可以自定义动画效果的只有两种:FullScreen和Custom,两者的区别在于FullScreen会移除fromView,而Custom不会。比如文章开头的gif中,第三个动画效果就是Custom。
基于block的动画
最简单的转场动画是使用transitionFromViewController方法:
这个方法虽然已经过时,但是对它的分析有助于后面知识的理解。它一共有6个参数,前两个表示从哪个VC开始,跳转到哪个VC,中间两个参数表示动画的时间和选项。最后两个参数表示动画的具体实现细节和回调闭包。
这六个参数其实就是一次转场动画所必备的六个元素。它们可以分为两组,前两个参数为一组,表示页面的跳转关系,后面四个为一组,表示动画的执行逻辑。
这个方法的缺点之一是可自定义程度不高(在后面您会发现能自定义的不仅仅是动画方式),另一个缺点则是重用性不好,也可以说是耦合度比较大。
在最后两个闭包参数中,可以预见的是fromViewController和toViewController参数都会被用到,而且他们是动画的关键。假设视图控制器A可以跳转到B、C、D、E、F,而且跳转动画基本相似,您会发现transitionFromViewController方法要被复制多次,每次只会修改少量内容。
自定义present转场动画
出于解耦和提高可自定义程度的考虑,我们来学习转场动画的正确使用姿势。
首先要了解一个关键概念:转场动画代理,它是一个实现了UIViewControllerTransitioningDelegate协议的对象。我们需要自己实现这个对象,它的作用是为UIKit提供以下几个对象中的一个或多个:
1、Animator:
它是实现了UIViewControllerAnimatedTransitioning协议的对象,用于控制动画的持续时间和动画展示逻辑,代理可以为present和dismiss过程分别提供Animator,也可以提供同一个Animator。
交互式Animator:和Animator类似,不过它是交互式的,后面会有详细介绍
Presentation控制器:
它可以对present过程更加彻底的自定义,比如修改被展示视图的大小,新增自定义视图等,后面会有详细介绍。
在这一小节中,我们首先介绍最简单的Animator。回顾一下转场动画必备的6个元素,它们被分为两组,彼此之间没有关联。Animator的作用等同于第二组的四个元素,也就是说对于同一个Animator,可以适用于A跳转B,也可以适用于A跳转C。它表示一种通用的页面跳转时的动画逻辑,不受限于具体的视图控制器。
如果您读懂了这段话,整个自定义的转场动画逻辑就很清楚了,以视图控制器A跳转到B为例:
// 这个类相当于Aclass CrossDissolveFirstViewController: UIViewController, UIViewControllerTransitioningDelegate {// 这个对象相当于BcrossDissolveSecondViewController.transitioningDelegate = self // 点击按钮触发的函数func animationButtonDidClicked() {self.presentViewController(crossDissolveSecondViewController,animated: true, completion: nil)}// 下面这两个函数定义在UIViewControllerTransitioningDelegate协议中// 用于为present和dismiss提供animatorfunc animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {//也可以使用CrossDissolveAnimator,动画效果各有不同//return CrossDissolveAnimator()return HalfWaySpringAnimator()}func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {return CrossDissolveAnimator()}}动画的关键在于animator如何实现,它实现了UIViewControllerAnimatedTransitioning协议,至少需要实现两个方法,我建议您仔细阅读animateTransition方法中的注释,它是整个动画逻辑的核心:
class HalfWaySpringAnimator: NSObject, UIViewControllerAnimatedTransitioning {/// 设置动画的持续时间func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {return 2}/// 设置动画的进行方式,附有详细注释,demo中其他地方的这个方法不再解释func animateTransition(transitionContext: UIViewControllerContextTransitioning) {let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)let containerView = transitionContext.containerView()// 需要关注一下from/to和presented/presenting的关系// For a Presentation:// fromView = The presenting view.// toView= The presented view.// For a Dismissal:// fromView = The presented view.// toView= The presenting view.var fromView = fromViewController?.viewvar toView = toViewController?.view// iOS8引入了viewForKey方法,尽可能使用这个方法而不是直接访问controller的view属性// 比如在form sheet样式中,我们为presentedViewController的view添加阴影或其他decoration,animator会对整个decoration view// 添加动画效果,而此时presentedViewController的view只是decoration view的一个子视图if transitionContext.respondsToSelector(Selector("viewForKey:")) {fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)toView = transitionContext.viewForKey(UITransitionContextToViewKey)}// 我们让toview的origin.y在屏幕的一半处,这样它从屏幕的中间位置弹起而不是从屏幕底部弹起,弹起过程中逐渐变为不透明toView?.frame = CGRectMake(fromView!.frame.origin.x, fromView!.frame.maxY / 2, fromView!.frame.width, fromView!.frame.height)toView?.alpha = 0.0// 在present和,dismiss时,必须将toview添加到视图层次中containerView?.addSubview(toView!)let transitionDuration = self.transitionDuration(transitionContext)// 使用spring动画,有弹簧效果,动画结束后一定要调用completeTransition方法UIView.animateWithDuration(transitionDuration, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: .CurveLinear, animations: { () -> Void intoView!.alpha = 1.0 // 逐渐变为不透明toView?.frame = transitionContext.finalFrameForViewController(toViewController!)// 移动到指定位置}) { (finished: Bool) -> Void inlet wasCancelled = transitionContext.transitionWasCancelled()transitionContext.completeTransition(!wasCancelled)}}}animateTransition方法的核心则是从转场动画上下文获取必要的信息以完成动画。上下文是一个实现了UIViewControllerContextTransitioning的对象,它的作用在于为animateTransition方法提供必备的信息。您不应该缓存任何关于动画的信息,而是应该总是从转场动画上下文中获取(比如fromView和toView),这样可以保证总是获取到最新的、正确的信息。
获取到足够信息后,我们调用UIView.animateWithDuration方法把动画交给Core Animation处理。千万不要忘记在动画调用结束后,执行completeTransition方法。
本节的知识在Demo的Cross Dissolve文件夹中有详细的代码。其中有两个animator文件,这说明我们可以为present和dismiss提供同一个animator,或者分别提供各自对应的animator。如果两者动画效果类似,您可以共用同一个animator,惟一的区别在于:
// 这个相当于fromViewControllerclass InteractivityFirstViewController: UIViewController { // 这个相当于toViewControllerlazy var interactivitySecondViewController: InteractivitySecondViewController = InteractivitySecondViewController()// 定义了一个InteractivityTransitionDelegate类作为代理lazy var customTransitionDelegate: InteractivityTransitionDelegate = InteractivityTransitionDelegate()override func viewDidLoad() {super.viewDidLoad()setupView() // 主要是一些UI控件的布局,可以无视其实现细节/// 设置动画代理,这个代理比较复杂,所以我们新建了一个代理对象而不是让self作为代理interactivitySecondViewController.transitioningDelegate = customTransitionDelegate}// 触发手势时,也会调用animationButtonDidClicked方法func interactiveTransitionRecognizerAction(sender: UIScreenEdgePanGestureRecognizer) {if sender.state == .Began {self.animationButtonDidClicked(sender)}}func animationButtonDidClicked(sender: AnyObject) {self.presentViewController(interactivitySecondViewController, animated: true, completion: nil)}}非交互式的动画代理只需要为present和dismiss提供animator即可,但是在交互式的动画代理中,还需要为present和dismiss提供交互式动画控制器:
class InteractivityTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {return InteractivityTransitionAnimator(targetEdge: targetEdge)}func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {return InteractivityTransitionAnimator(targetEdge: targetEdge)}/// 前两个函数和淡入淡出demo中的实现一致/// 后两个函数用于实现交互式动画func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {return TransitionInteractionController(gestureRecognizer: gestureRecognizer, edgeForDragging: targetEdge)}func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {return TransitionInteractionController(gestureRecognizer: gestureRecognizer, edgeForDragging: targetEdge)}}animator中的代码略去,它和非交互式动画中的animator类似。因为交互式的动画只是一种锦上添花,它必须支持非交互式的动画,比如这个例子中,点击按钮依然出发的是非交互式的动画,只是手势滑动才会触发交互式动画。
class TransitionInteractionController: UIPercentDrivenInteractiveTransition {/// 当手势有滑动时触发这个函数func gestureRecognizeDidUpdate(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {switch gestureRecognizer.state {case .Began: breakcase .Changed: self.updateInteractiveTransition(self.percentForGesture(gestureRecognizer)) //手势滑动,更新百分比case .Ended:// 滑动结束,判断是否超过一半,如果是则完成剩下的动画,否则取消动画if self.percentForGesture(gestureRecognizer) >= 0.5 {self.finishInteractiveTransition()}else {self.cancelInteractiveTransition()}default: self.cancelInteractiveTransition()}}private func percentForGesture(gesture: UIScreenEdgePanGestureRecognizer) -> CGFloat {let percent = 根据gesture计算得出return percent}}交互式动画是在非交互式动画的基础上实现的,我们需要创建一个继承自UIPercentDrivenInteractiveTransition类型的子类,并且在动画代理中返回这个类型的实例对象。
想要完成gif中第三个例子的效果,我们还需要使用UIModalPresentationStyle.Custom来代替.FullScreen。因为后者会移除fromViewController,这显然不符合需求。
当present的方式为.Custom时,我们还可以使用UIPresentationController更加彻底的控制转场动画的效果。一个 presentation controller具备以下几个功能:
// 这个相当于fromViewControllerclass CustomPresentationFirstViewController: UIViewController {// 这个相当于toViewControllerlazy var customPresentationSecondViewController: CustomPresentationSecondViewController = CustomPresentationSecondViewController()// 创建PresentationControllerlazy var customPresentationController: CustomPresentationController = CustomPresentationController(presentedViewController: self.customPresentationSecondViewController, presentingViewController: self)override func viewDidLoad() {super.viewDidLoad()setupView() // 主要是一些UI控件的布局,可以无视其实现细节// 设置转场动画代理customPresentationSecondViewController.transitioningDelegate = customPresentationController}override func didReceiveMemoryWarning() {super.didReceiveMemoryWarning()// Dispose of any resources that can be recreated.}func animationButtonDidClicked() {self.presentViewController(customPresentationSecondViewController, animated: true, completion: nil)}}重点在于如何实现CustomPresentationController这个类:
class CustomPresentationController: UIPresentationController, UIViewControllerTransitioningDelegate {var presentationWrappingView: UIView? // 这个视图封装了原视图,添加了阴影和圆角效果var dimmingView: UIView? = nil // alpha为0.5的黑色蒙版// 告诉UIKit为哪个视图添加动画效果override func presentedView() -> UIView? {return self.presentationWrappingView}}// 四个方法自定义转场动画发生前后的操作extension CustomPresentationController {override func presentationTransitionWillBegin() {// 设置presentationWrappingView和dimmingView的UI效果let transitionCoordinator = self.presentingViewController.transitionCoordinator()self.dimmingView?.alpha = 0// 通过转场协调器执行同步的动画效果transitionCoordinator?.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) -> Void inself.dimmingView?.alpha = 0.5}, completion: nil)}/// present结束时,把dimmingView和wrappingView都清空,这些临时视图用不到了override func presentationTransitionDidEnd(completed: Bool) {if !completed {self.presentationWrappingView = nilself.dimmingView = nil}}/// dismiss开始时,让dimmingView完全透明,这个动画和animator中的动画同时发生override func dismissalTransitionWillBegin() {let transitionCoordinator = self.presentingViewController.transitionCoordinator()transitionCoordinator?.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) -> Void inself.dimmingView?.alpha = 0}, completion: nil)}/// dismiss结束时,把dimmingView和wrappingView都清空,这些临时视图用不到了override func dismissalTransitionDidEnd(completed: Bool) {if completed {self.presentationWrappingView = nilself.dimmingView = nil}}}extension CustomPresentationController {}除此以外,这个类还要处理子视图布局相关的逻辑。它作为动画代理,还需要为动画提供animator对象,详细代码请在demo的Custom Presentation文件夹下阅读。
class FromViewController: UIViewController, UINavigationControllerDelegate {let toViewController: ToViewController = ToViewController()override func viewDidLoad() {super.viewDidLoad()setupView() // 主要是一些UI控件的布局,可以无视其实现细节self.navigationController.delegate = self}}与present/dismiss不同的时,现在视图控制器实现的是UINavigationControllerDelegate协议,让自己成为navigationController的代理。这个协议类似于此前的UIViewControllerTransitioningDelegate协议。
func navigationController(navigationController: UINavigationController,animationControllerForOperation operation: UINavigationControllerOperation,fromViewController fromVC: UIViewController,toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {if operation == .Push {return PushAnimator()}if operation == .Pop {return PopAnimator()}return nil;}至于animator,就和此前没有任何区别了。可见,一个封装得很好的animator,不仅能在present/dismiss时使用,甚至还可以在push/pop时使用。