iOS自定义转场动画实战讲解

转场动画这事,说简单也简单,能够经过presentViewController:animated:completion:dismissViewControllerAnimated:completion:这一组函数以模态视图的方式展示、隐藏视图。若是用到了navigationController,还能够调用pushViewController:animated:popViewController这一组函数将新的视图控制器压栈、弹栈。git

下图中全部转场动画都是自定义的动画,这些效果若是不用自定义动画则很难甚至没法实现:github

demo演示

因为录屏的缘由,有些效果没法彻底展示,好比它其实还支持横屏。spring

自定义转场动画的效果实现起来比较复杂,若是仅仅是拷贝一份可以运行的代码却不懂其中原理,就有可能带来各类隐藏的bug。本文由浅入深介绍下面几个知识:swift

  1. 传统的基于闭包的实现方式及其缺点
  2. 自定义present转场动画
  3. 交互式(Interactive)转场动画
  4. 转场协调器与UIModalPresentationCustom
  5. UINavigationController转场动画

我为这篇教程制做了一个demo,您能够去在个人github上clone下来:CustomTransition,若是以为有帮助还望给个star以示支持。本文以Swift+纯代码实现,对应的OC+Storyboard版本在demo中也能够找到,那是苹果的官方示范代码,正确性更有保证。demo中用到了CocoaPods,您也许须要执行pod install命令并打开.xcworkspace文件。缓存

在开始正式的教程前,您首先须要下载demo,在代码面前文字是苍白的,demo中包含的注释足以解释本文全部的知识点。其次,您还得了解这几个背景知识。闭包

From和To

在代码和文字中,常常会出现fromViewtoView。若是错误的理解它们的含义会致使动画逻辑彻底错误。fromView表示当前视图,toView表示要跳转到的视图。若是是从A视图控制器present到B,则A是from,B是to。从B视图控制器dismiss到A时,B变成了from,A是to。用一张图表示:app

from和to

Presented和Presenting

这也是一组相对的概念,它容易与fromViewtoView混淆。简单来讲,它不受present或dismiss的影响,若是是从A视图控制器present到B,那么A老是B的presentingViewController,B老是A的presentedViewControlleride

modalPresentationStyle

这是一个枚举类型,表示present时动画的类型。其中能够自定义动画效果的只有两种:FullScreenCustom,二者的区别在于FullScreen会移除fromView,而Custom不会。好比文章开头的gif中,第三个动画效果就是Custom函数

基于block的动画

最简单的转场动画是使用transitionFromViewController方法:布局

传统的转场动画实现

这个方法虽然已通过时,可是对它的分析有助于后面知识的理解。它一共有6个参数,前两个表示从哪一个VC开始,跳转到哪一个VC,中间两个参数表示动画的时间和选项。最后两个参数表示动画的具体实现细节和回调闭包。

这六个参数其实就是一次转场动画所必备的六个元素。它们能够分为两组,前两个参数为一组,表示页面的跳转关系,后面四个为一组,表示动画的执行逻辑。

这个方法的缺点之一是可自定义程度不高(在后面您会发现能自定义的不只仅是动画方式),另外一个缺点则是重用性很差,也能够说是耦合度比较大。

在最后两个闭包参数中,能够预见的是fromViewControllertoViewController参数都会被用到,并且他们是动画的关键。假设视图控制器A能够跳转到B、C、D、E、F,并且跳转动画基本类似,您会发现transitionFromViewController方法要被复制屡次,每次只会修改少许内容。

自定义present转场动画

出于解耦和提升可自定义程度的考虑,咱们来学习转场动画的正确使用姿式。

首先要了解一个关键概念:转场动画代理,它是一个实现了UIViewControllerTransitioningDelegate协议的对象。咱们须要本身实现这个对象,它的做用是为UIKit提供如下几个对象中的一个或多个:

  1. Animator:

它是实现了UIViewControllerAnimatedTransitioning协议的对象,用于控制动画的持续时间和动画展现逻辑,代理能够为present和dismiss过程分别提供Animator,也能够提供同一个Animator。

  1. 交互式Animator:和Animator相似,不过它是交互式的,后面会有详细介绍
  2. Presentation控制器:

它能够对present过程更加完全的自定义,好比修改被展现视图的大小,新增自定义视图等,后面会有详细介绍。

转场动画代理

在这一小节中,咱们首先介绍最简单的Animator。回顾一下转场动画必备的6个元素,它们被分为两组,彼此之间没有关联。Animator的做用等同于第二组的四个元素,也就是说对于同一个Animator,能够适用于A跳转B,也能够适用于A跳转C。它表示一种通用的页面跳转时的动画逻辑,不受限于具体的视图控制器。

若是您读懂了这段话,整个自定义的转场动画逻辑就很清楚了,以视图控制器A跳转到B为例:

  1. 建立动画代理,在事情比较简单时,A本身就能够做为代理
  2. 设置B的transitioningDelegate为步骤1中建立的代理对象
  3. 调用presentViewController:animated:completion:并把参数animated设置为true
  4. 系统会找到代理中提供的Animator,由Animator负责动画逻辑

用具体的例子解释就是:

// 这个类至关于A
class CrossDissolveFirstViewController: UIViewController, UIViewControllerTransitioningDelegate {
// 这个对象至关于B
crossDissolveSecondViewController.transitioningDelegate = self

// 点击按钮触发的函数
func animationButtonDidClicked() {
self.presentViewController(crossDissolveSecondViewController,
animated: true, completion: nil)
}

// 下面这两个函数定义在UIViewControllerTransitioningDelegate协议中
// 用于为present和dismiss提供animator
func 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?.view
var 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 in
toView!.alpha = 1.0     // 逐渐变为不透明
toView?.frame = transitionContext.finalFrameForViewController(toViewController!)    // 移动到指定位置
}) { (finished: Bool) -> Void in
let 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,唯一的区别在于:

  1. present时,要把toView加入到container的视图层级。
  2. dismiss时,要把fromView从container的视图层级中移除。

若是您被前面这一大段代码和知识弄晕了,或者暂时用不到这些具体的知识,您至少须要记住自定义动画的基本原理和流程:

  1. 设置将要跳转到的视图控制器(presentedViewController)的transitioningDelegate
  2. 充当代理的对象能够是源视图控制器(presentingViewController),也能够是本身建立的对象,它须要为转场动画提供一个animator对象。
  3. animator对象的animateTransition是整个动画的核心逻辑。

交互式(Interactive)转场动画

刚刚咱们说到,设置了toViewControllertransitioningDelegate属性而且present时,UIKit会从代理处获取animator,其实这里还有一个细节:UIKit还会调用代理的interactionControllerForPresentation:方法来获取交互式控制器,若是获得了nil则执行非交互式动画,这就回到了上一节的内容。

若是获取到了不是nil的对象,那么UIKit不会调用animator的animateTransition方法,而是调用交互式控制器(还记得前面介绍动画代理的示意图么,交互式动画控制器和animator是平级关系)的startInteractiveTransition:方法。

所谓的交互式动画,一般是基于手势驱动,产生一个动画完成的百分比来控制动画效果(文章开头的gif中第二个动画效果)。整个动画再也不是一次性、连贯的完成,而是在任什么时候候均可以改变百分比甚至取消。这须要一个实现了UIPercentDrivenInteractiveTransition协议的交互式动画控制器和animator协同工做。这看上去是一个很是复杂的任务,但UIKit已经封装了足够多细节,咱们只须要在交互式动画控制器和中定义一个时间处理函数(好比处理滑动手势),而后在接收到新的事件时,计算动画完成的百分比而且调用updateInteractiveTransition来更新动画进度便可。

用下面这段代码简单表示一下整个流程(删除了部分细节和注释,请不要以此为正确参考),完整的代码请参考demo中的Interactivity文件夹:

// 这个至关于fromViewController
class InteractivityFirstViewController: UIViewController {
// 这个至关于toViewController
lazy 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: break
case .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类型的子类,而且在动画代理中返回这个类型的实例对象。

在这个类型中,监听手势(或者下载进度等等)的时间变化,而后调用percentForGesture方法更新动画进度便可。

转场协调器与UIModalPresentationCustom

在进行转场动画的同时,您还能够进行一些同步的,额外的动画,好比文章开头gif中的第三个例子。presentedViewpresentingView能够更改自身的视图层级,添加额外的效果(阴影,圆角)。UIKit使用转成协调器来管理这些额外的动画。您能够经过须要产生动画效果的视图控制器的transitionCoordinator属性来获取转场协调器,转场协调器只在转场动画的执行过程当中存在。

转场动画协调器

想要完成gif中第三个例子的效果,咱们还须要使用UIModalPresentationStyle.Custom来代替.FullScreen。由于后者会移除fromViewController,这显然不符合需求。

当present的方式为.Custom时,咱们还可使用UIPresentationController更加完全的控制转场动画的效果。一个 presentation controller具有如下几个功能:

  1. 设置presentedViewController的视图大小
  2. 添加自定义视图来改变presentedView的外观
  3. 为任何自定义的视图提供转场动画效果
  4. 根据size class进行响应式布局

您能够认为,. FullScreen以及其余present风格都是swift为咱们实现提供好的,它们是.Custom的特例。而.Custom容许咱们更加自由的定义转场动画效果。

UIPresentationController提供了四个函数来定义present和dismiss动画开始先后的操做:

  1. presentationTransitionWillBegin: present将要执行时
  2. presentationTransitionDidEnd:present执行结束后
  3. dismissalTransitionWillBegin:dismiss将要执行时
  4. dismissalTransitionDidEnd:dismiss执行结束后

下面的代码简要描述了gif中第三个动画效果的实现原理,您能够在demo的Custom Presentation文件夹下查看完成代码:

// 这个至关于fromViewController
class CustomPresentationFirstViewController: UIViewController {
// 这个至关于toViewController
lazy var customPresentationSecondViewController: CustomPresentationSecondViewController = CustomPresentationSecondViewController()
// 建立PresentationController
lazy 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 in
self.dimmingView?.alpha = 0.5
}, completion: nil)
}

/// present结束时,把dimmingView和wrappingView都清空,这些临时视图用不到了
override func presentationTransitionDidEnd(completed: Bool) {
if !completed {
self.presentationWrappingView = nil
self.dimmingView = nil
}
}

/// dismiss开始时,让dimmingView彻底透明,这个动画和animator中的动画同时发生
override func dismissalTransitionWillBegin() {
let transitionCoordinator = self.presentingViewController.transitionCoordinator()
transitionCoordinator?.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) -> Void in
self.dimmingView?.alpha = 0
}, completion: nil)
}

/// dismiss结束时,把dimmingView和wrappingView都清空,这些临时视图用不到了
override func dismissalTransitionDidEnd(completed: Bool) {
if completed {
self.presentationWrappingView = nil
self.dimmingView = nil
}
}
}

extension CustomPresentationController {
}
复制代码

除此之外,这个类还要处理子视图布局相关的逻辑。它做为动画代理,还须要为动画提供animator对象,详细代码请在demo的Custom Presentation文件夹下阅读。

UINavigationController转场动画

到目前为止,全部转场动画都是适用于present和dismiss的,其实UINavigationController也能够自定义转场动画。二者是平行关系,不少均可以类比过来:

class FromViewController: UIViewController, UINavigationControllerDelegate {
let toViewController: ToViewController = ToViewController()

override func viewDidLoad() {
super.viewDidLoad()
setupView() // 主要是一些UI控件的布局,能够无视其实现细节

self.navigationController.delegate = self
}
}
复制代码

与present/dismiss不一样的时,如今视图控制器实现的是UINavigationControllerDelegate协议,让本身成为navigationController的代理。这个协议相似于此前的UIViewControllerTransitioningDelegate协议。

FromViewController实现UINavigationControllerDelegate协议的具体操做以下:

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时使用。

UINavigationController也能够添加交互式转场动画,原理也和此前相似。

总结

对于非交互式动画,须要设置presentedViewControllertransitioningDelegate属性,这个代理须要为present和dismiss提供animator。在animator中规定了动画的持续时间和表现逻辑。

对于交互式动画,须要在此前的基础上,由transitioningDelegate属性提供交互式动画控制器。在控制器中进行事件处理,而后更新动画完成进度。

对于自定义动画,能够经过UIPresentationController中的四个函数自定义动画执行先后的效果,能够修改presentedViewController的大小、外观并同步执行其余的动画。

自定义动画的水仍是比较深,本文仅适合作入门学习用,欢迎互相交流。

相关文章
相关标签/搜索