教程:创建自定义的UIViewController转场动画

本教程使用: xcode 7和swift 2
翻译自: http://www.raywenderlich.com/110536/custom-uiviewcontroller-transitions

Push、pop、cover vertically… 我们可以从iOS内置的转场动画中得到这些很棒的效果,但是自己尝试自己的动画也是个很棒的体验!自定义转场动画可以极大地增加用户体验,并且让你的app和其他的看起来不一样。如果你之前都没有自定义过转场动画,通过阅读本教程你会发现,这比你想象的要简单很多!
在本教程中,我们会在一个demo中增加自定义转场动画,当我们完成此教程时,你应该学会了以下技能:

  • 了解转场API的结构
  • 如何利用转场动画来自定义present和dismiss动画
  • 开发出互动的转场动画

开始

下载起始项目。编译运行程序,我们会看到:

《教程:创建自定义的UIViewController转场动画》

这个demo在page view controller中展示了几张卡片,每张卡片都是每种宠物的描述,点击卡片就会弹出该宠物的照片。

大部分的跳转逻辑都已经写好了,但是这个demo看起来还是有点平淡无奇,我们这就来用自定义的转场动画来增加效果。

浏览转场API

相比于具体的对象,转场API运用了大量的协议,在这个模块的最后,你会了解到每个协议的作用以及它们之间的联系。这个图表显示了API中主要的部分:

《教程:创建自定义的UIViewController转场动画》

组成部分

尽管上面图表看起来比较复杂,但当你了解到各部分是如何工作之后,它看起来就直观很多了。

Transitioning Delegate

每个view controller都可以有一个transitioningDelegate代理,它实现了UIViewControllerTransitioningDelegate的协议。
不管你是present还是dismiss一个view controller,UIKit总会向transitioning delegate询问是否有代理可用。设置view controller的transitioningDelegate为我们自定义的类来作出自定义的转场动画。

Animation Controller

自定义的类实现UIViewControllerAnimatedTransitioning协议,来完成具体的转场动画。

Transitioning Context

这个context对象实现了UIViewControllerContextTransitioning协议,在转场过程中扮演重要的角色,它包含了view controllers的重要信息。
我们实际上不用去在我们自己的代码来实现它,当转场发生时,UIKit会提供给我们这样一个context的对象的。

转场过程

下面是一个present动画的步骤:
1.不管是用代码还是通过segue,我们需要触发转场。
2.UIKit向”to” view controller(将被显示的view controller)的transitioning delegate询问,如果没有代理,就会用内置的默认转场动画。
3.UIKit之后向transigioning delegate通过代理方法animationControllerForPresentedController(_:presentingController:sourceController:)询问一个animation controller,如果返回nil,仍然会用默认的动画。
4.一旦有一个有效的animation controller后,UIKit就会构建transitioning context。
5.UIKit之后向animation controller通过transitionDuration(_:)询问动画持续时长。
6.UIKit调用animateTransition(_:)来实现animation controller的转场动画。
7.最后,animation controller调用context的completeTransition(_:)方法来表示这次动画已经结束。

创建一个自定义的presentation动画

是时候来实践了!
你的目的是实现下面的动画:

  • 当用户点击卡片,它会翻转到第二个view,这个view的尺寸和卡片尺寸一样。
  • 随着翻转,第二个view放大到全屏。

创建Animator

我们由创建animation controller开始。
首先创建一个继承自NSObject的子类,并且语言为Swift,命名为FlipPresentAnimationController.swift,接着更新其声明:

import UIKit
class FlipPresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

}

注意我们可能因为没有实现代理方法而受到编译错误的提示,我们现在来fix。

《教程:创建自定义的UIViewController转场动画》 Compiler errors…don’t panic…

在当用户点击图片时,我们会用到动画起始时的frame,在类的body中加入如下变量:

var originFrame = CGRect.zero

为了满足UIViewControllerAnimatedTransitioning的要求,我们还需要加入两个method:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimerInterval {
    return 2.0
}

根据名字也知道,这个方法指定了转场的持续时间,设置成2秒能让我们在开发过程中更容易观察动画。
现在加入另一个方法:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

}

这个协议方法就是用来实现转场动画本身的,从这开始我们加入下面的代码:

// 1
guard let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey), 
    let containerView = transitionContext.containerView(),
    let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
    return
}

// 2
let initialFrame = originFrame
let finalFrame = transitionContext.finalFrameForViewController(toVC)

// 3
let snapshot = toVC.view.snapshotViewAfterScreenUpdates(true)
snapshot.frame = initailFrame
snapshot.layer.cornerRadius = 25
snapshot.layer.masksToBounds = true

上面代码做了这些:

  1. transitioning context提供了参与转场动画的view controllers以及views,我们可以通过合适的key来获取它们。
  2. 接着我们指定”to” view的起始和最终的frame,在这个例子中,转场从卡片的frame开始,然后放大到全屏。
  3. UIView的snapshotting捕获了”to” view的截屏,这样我们利用截屏来做动画,截屏也是从卡片的frame开始,圆角也设成和卡片一样的。

继续加入如下代码:

containerView.addSubview(toVC.view)
containerView.addSubview(snapshot)
toVC.view.hidden = true

AnimationHelper.perspectiveTransformForContainerView(containerView)
snapshot.layer.transform = AnimationHelper.yRotation(M_PI_2)

一个新的成员出现了:container view。把它想象成一个舞台而转场在上面跳舞。container view已经包含了”from” view,但是我们需要负责向其中加入”to” view。
我们并且加入了snapshot view并且隐藏了真的”to” view,完整的动画会旋转snapshot然后把它隐藏。

注意:不用太在意AnimationHelper,这只是个小的工具类,负责给view增加perspective和rotation的transforms的。

是时候来具体实现我们的动画了,继续增加这些代码:

// 1
let duration = transitionDuration(transitionContext) 

UIView.animateKeyframesWithDuration( 
    duration, 
    delay: 0, 
    options: .CalculationModeCubic, 
    animations: { 
        // 2 
        UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 1/3, animations: { 
            fromVC.view.layer.transform = AnimationHelper.yRotation(-M_PI_2) 
        })  

        // 3 
        UIView.addKeyframeWithRelativeStartTime(1/3, relativeDuration: 1/3, animations: { 
            snapshot.layer.transform = AnimationHelper.yRotation(0.0) 
        })  
        
        // 4 
        UIView.addKeyframeWithRelativeStartTime(2/3, relativeDuration: 1/3, animations: { 
            snapshot.frame = finalFrame })
    }, 
    completion: { _ in 
        // 5 
        toVC.view.hidden = false 
        fromVC.view.layer.transform = AnimationHelper.yRotation(0.0) 
        snapshot.removeFromSuperview() 
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled()) 
    })

我们用注释来把代码分成各区域:

  1. 首先我们指定了动画的持续时间,注意使用transitionDuration(_:)方法,我们需要让我们的动画和这个持续时间保持一致,这样UIKit才能同步。
  2. 我们根据y轴旋转”from” view 90度来隐藏from view。
  3. 接着我们用同样的方法来恢复snapshot的transform。
  4. 设置shapshot的frame来充满屏幕。
  5. 最后,我们remove掉snapshot,显示”to” view,还原”from” view,否则,它会一直隐藏着。 调用completeTransition来通知transition context动画已经结束,UIKit会保证最后remove掉”from” view。

现在我们可以使用我们写的animtion controller了!

使用animator

打开CardViewController.swift并且加入如下声明:

private let filpPresentAnimationController = FlipPresentAnimationController()

UIKit希望用代理来返回animation controller,所以我们必须提供一个实现了UIViewControllerTransitioningDelegate的对象。
在这个demo中,CardViewController会扮演这个transitioning delegate,加入如下的extension来让该类实现UIViewControllerTransitioningDelegate

extension CardViewController: UIViewControllerTransitioningDelegate {

}

接下来,在上述extension中加入:

func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {  
    flipPresentAnimationController.originFrame = cardView.frame 
    return flipPresentAnimationController
}

这里我们返回了自定义的animation controller,并且这里我们设置了起始frame来保证动画位置的正确。
最后一步就是把CardViewController当作transitioning delegate,每个view controller都有transitioningDelegate属性,UIKit会根据这个属性来判断是否执行自定义的转场。
prepareForSeque(_:sender:)中,在card assignment后面加入:

destinationViewController.transitioningDelegate = self;

值得注意的是,是被presented的view controller设置代理,而不是主动去present的view controller来设置代理!
编译运行:

《教程:创建自定义的UIViewController转场动画》

我们已经有了我们第一个自定义的转场动画,但是presenting只是一半,我们还需要dismiss的动画。

创建 Dismissing 动画

同样的,新建一个FlipDismissAnimationController,替换类的内容如下:

import UIKit 

class FlipDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {  
    
    var destinationFrame = CGRectZero  

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { 
        return 0.6 
    }  

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {  

    }
}

这个类的目的是反向实现presenting的动画:

  • 缩小显示的view到卡片的大小,destinationFrame就是卡片的frame。
  • 翻转view,并且恢复卡片内容

加入如下代码到animateTransition(_:)

guard let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey), 
    let containerView = transitionContext.containerView(), 
    let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else { 
    return
} 

// 1
let initialFrame = transitionContext.initialFrameForViewController(fromVC)
let finalFrame = destinationFrame 

// 2
let snapshot = fromVC.view.snapshotViewAfterScreenUpdates(false)
snapshot.layer.cornerRadius = 25
snapshot.layer.masksToBounds = true 

// 3
containerView.addSubview(toVC.view)
containerView.addSubview(snapshot)
fromVC.view.hidden = true 

AnimationHelper.perspectiveTransformForContainerView(containerView) 

//4
toVC.view.layer.transform = AnimationHelper.yRotation(-M_PI_2)

来解释一下:

  1. 因为动画会缩小view,我们需要设置好起始和最终的frame
  2. 这次我们截屏”from” view。
  3. 和之前一样,加入”to” view和snapshot到container view中,隐藏”from” view,这样不会和snapshot冲突。
  4. 最后通过旋转技巧隐藏”to” view

剩下的就是加入动画本身了。
直接在animateTransition(_:)直接加入如下代码:

let duration = transitionDuration(transitionContext) 

UIView.animateKeyframesWithDuration( 
    duration, 
    delay: 0, 
    options: .CalculationModeCubic, 
    animations: { 
        // 1 
        UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 1/3, animations: { 
            snapshot.frame = finalFrame 
        })  

        UIView.addKeyframeWithRelativeStartTime(1/3, relativeDuration: 1/3, animations: { 
            snapshot.layer.transform = AnimationHelper.yRotation(M_PI_2) 
        })  
        
        UIView.addKeyframeWithRelativeStartTime(2/3, relativeDuration: 1/3, animations: { 
            toVC.view.layer.transform = AnimationHelper.yRotation(0.0) 
        }) 
    }, completion: { _ in 
        // 2 
        fromVC.view.hidden = false 
        snapshot.removeFromSuperview() 
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
    })

只就是在反向执行present的动画而已:

  1. 首先缩小view,用旋转90度来隐藏snapshot,接着也用旋转�恢复”to” view。
  2. 最后,remove掉snapshot和通知context转场已经完成,这会让UIKit更新view controller状态。

打开CardViewController.swift,声明如下属性:

private let flipDismissAnimationController = FlipDismissAnimationController()

接着加入如下extension:

func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 
    flipDismissAnimationController.destinationFrame = cardView.frame 
    return flipDismissAnimationController
}

这样传递了正确的frame给dismiss animation controller。
最后一步,修改FlipPresentAnimationControllertransitionDuration,让动画更快一点:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { 
    return 0.6
}

这时候编译运行:

《教程:创建自定义的UIViewController转场动画》

我们的自定义转场动画看起来很酷,但是我们还能通过加入交互特性来优化它。

让转场具有交互性

iOS内置的设置应用,很好展示了交互式的转场动画:

《教程:创建自定义的UIViewController转场动画》

这次我们的任务是通过慢慢在屏幕最左边滑动来返回界面,并且动画过程跟随着我们手指的滑动过程。

交互式转场如何工作的

一个交互式的controller能响应包括了加速、减速或者反转转场过程。为了能开启交互式转场功能,transitioning delegate必须能提供一个interaction controller。这个controller可以是任何对象,只要它实现了UIViewControllerInteractiveTransitioning的协议。我们已经实现了转场的动画,而interaction controller就负责把这个动画和我们的手势连在一起,而不是简单地像放电影一样的闪过。
Apple已经提供了一个UIPercentDrivenInteractiveTransition的类,它是一个具体的interaction controller的实现,我们会用这个类来实现我们的交互式转场。

创建一个交互式转场

我们第一个工作是要创建一个interaction controller,创建一个SwipeInteractionController,继承自UIPercentDrivenInteractiveTransition,选中语言为Swift,打开SwipeInteractionController.swift,加入如下属性定义:

var interactionInProgress = falseprivate 
var shouldCompleteTransition = false
private weak var viewController: UIViewController!

上述代码很直观:

  • “`interactionInProgress“表示一个交互转场是否正在进行中。
  • 我们用shouldCompleteTransition来控制转场,后面会看到。
  • 这个interaction controller会直接present和dismiss view controllers,所以我们需要持有viewController。
    然后加入到class的body中:
func wireToViewController(viewController: UIViewController!) { 
    self.viewController = viewController 
    prepareGestureRecognizerInView(viewController.view)
}

实现prepareGestureRecognizerInView(_:)

private func prepareGestureRecognizerInView(view: UIView) { 
    let gesture = UIScreenEdgePanGestureRecognizer(target: self, action: "handleGesture:") 
    gesture.edges = UIRectEdge.Left 
    view.addGestureRecognizer(gesture)
}

这里我们声明了一个gesture recognizer,它会触发屏幕左边缘的手势。
最后是加入handleGesture(_:)

func handleGesture(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {  
    // 1 
    let translation = gestureRecognizer.translationInView(gestureRecognizer.view!.superview!) 
    var progress = (translation.x / 200) progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))  

    switch gestureRecognizer.state {  

    case .Began: 
        // 2 
        interactionInProgress = true 
        viewController.dismissViewControllerAnimated(true, completion: nil)  

    case .Changed: 
        // 3 
        shouldCompleteTransition = progress > 0.5 
        updateInteractiveTransition(progress)  

    case .Cancelled: 
        // 4 
        interactionInProgress = false 
        cancelInteractiveTransition()  

    case .Ended: 
        // 5 
        interactionInProgress = false  
        if !shouldCompleteTransition { 
            cancelInteractiveTransition() 
        } else { 
            finishInteractiveTransition() 
        }  

    default: 
        println("Unsupported") }
}

我们来解释下:

  1. 定义了一些局部的变量来控制整个过程,我们记录view的位移来计算progress,200的位移再松手就视为继续完成转场。
  2. 手势开始时,我们把interactionInProgress设为true,并且开始执行dismiss。
  3. 当手势进行中,我们不断地用progress更新updateInteractiveTransition
  4. 当手势被中断时,我们把interactionInProgress设回false,并且中断转场。
  5. 当手势结束时,根据之前逻辑判断的shouldCompleteTransition来完成或者中断转场。

打开CardViewController.swift,声明如下属性:

private let swipeInteractionController = SwipeInteractionController()

加入如下的extension:

func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {  
    return swipeInteractionController.interactionInProgress ? swipeInteractionController : nil
}

这个实现先检查是否有手势,如果有的话则返回interaction controller,否则返回nil。
现在在prepareForSegue(_:sender:),在transitioningDelegate赋值代码后加入:

swipeInteractionController.wireToViewController(destinationViewController)

这样就绑定了interaction controller和view controller。
编译运行:

《教程:创建自定义的UIViewController转场动画》

译者注:完整的demo可以从https://github.com/Mercy-Li/GuessThePet 获取。

    原文作者:Xiao_Li
    原文地址: https://www.jianshu.com/p/4e4303815862
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞