系统学习iOS动画之四:视图控制器的转场动画

本文是我学习《iOS Animations by Tutorials》 笔记中的一篇。 文中详细代码都放在个人Github上 andyRon/LearniOSAnimationshtml

以前学习了视图动画、图层动画、自动布局动画等。这个部分让视野更大一点,学习整个视图控制器的动画,视图控制器的转场动画(View Controller Transition Animations)ios

iOS中最容易识别的动画之一是将新视图控制器推入导航堆栈的动画,当咱们想让APP有本身的特点,自定义转场动画是很是好的方式。git

在本文,将学习如何使用动画建立本身的自定义视图控制器转换。github

预览:swift

17-视图控制器转场和屏幕旋转转场 了解如何经过自定义动画转场呈现视图控制器 - 做为奖励,您将建立动画转场以处理设备方向更改。安全

18-导航控制器转场bash

19-交互式导航控制器转场闭包

17-视图控制器转场和屏幕旋转转场

不管是呈现 照相机视图控制器、地址簿仍是自定义的模态屏幕,每次都调用相同的UIKit方法:present(_:animated:completion:)。 此方法将当前屏幕“放弃”,而后跳到另外一个视图控制器。ide

下图呈现一个“New Contact”视图控制器向上滑动以覆盖当前视图(联系人列表),这是默认的动画方式:函数

在本章中,学习建立本身的自定义演示控制器动画,以替换默认的动画,并使本章的项目更加生动。

开始项目

本章开始项目是一个新项目,叫BeginnerCook

这个开始项目能够简单归纳 以下,ViewController中包括一个背景图UIImageView,一个标题UILabel,一个文本视图UITextView,下面是一个能够左右移动的UIScrollView。这个UIScrollView里会用代码加入一些香草(herb)图片,点击图片会跳转到另个展现详情的视图控制器HerbDetailsViewController,这个转场是标准的从下到上的垂直覆盖转场动画。

开始项目预览一下:

自定义转场的原理

UIKit实现自定义转场动画是经过代理模式完成的。所以首先须要让ViewController遵照UIViewControllerTransitioningDelegate协议。

每次呈现新的视图控制器时,UIKit都会询问其代理是否要使用自定义转场。如下是自定义转场动画的第一步:

image-20181202172719920

须要实现animationController(forPresented:presenting:source:)方法,这个方法若是返回nil,则进行默认的转场动画,若是返回时遵照UIViewControllerAnimatedTransitioning协议的对象,则将这个对象做为自定义转场的Animator(能够翻译为动画师)。

在UIKit使用自定义Animator以前,还须要一些步骤:

image-20181202172932834

transitionDuration(using:)返回动画持续时间。

animateTransition(using:)方法时实际动画代码所在的地方。在这个方法中能够访问屏幕上的当前视图控制器以及将要显示的新视图控制器,能够本身根据须要淡化,缩放,旋转等操做现有视图和新视图。

下面开始实现自定义转场!💪

实现转场代理

新建一个NSObject子类PopAnimator(就是以前提到的Animator),并遵照协议 UIViewControllerAnimatedTransitioning 。并在这个动画类中添加两个函数的存根:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0
}
    
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
复制代码

ViewController遵照UIViewControllerTransitioningDelegate协议:

extension ViewController: UIViewControllerTransitioningDelegate {
    
}
复制代码

didTapImageView(_:)中的present(herbDetails, animated: true, completion: nil)前添加:

herbDetails.transitioningDelegate = self
复制代码

如今,每次在屏幕上显示详情页的视图控制器时,UIKit都会向ViewController询问动画对象。 可是,目前仍然没有实现任何UIViewControllerTransitioningDelegate中的相关方法,所以UIKit仍将使用默认转换。

ViewController中建立动画属性:

let transition = PopAnimator()
复制代码

实现呈现时动画的协议方法:

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {  
    return transition
}
复制代码

实现解除(dismiss)时动画的协议方法:

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return transition
}
复制代码

如今点击香草🌿图片时,没有反应,这是由于,把默认的转场动画修改为了自定义,但自定义动画目前是空的。

建立转场动画师

PopAnimator添加:

let duration = 1.0
var presenting = true
var originFrame = CGRect.zero
复制代码

duration 是动画持续时间。

presenting 是用判断当前是呈现仍是解除

originFrame用来存储用户点击的图像的原始 frame —— 呈现动画就是须要它从原始frame到全屏图像frame,对应的解除动画正好相反。

用如下内容替换transitionDuration()中的代码:

return duration
复制代码

设置转场动画的上下文

是时候为animateTransition(using:)添加代码了。 此方法有一个类型为UIViewControllerContextTransitioning的参数,经过该参数能够访问转场的相关参数和视图控制器。

在开始写动画代码以前,了解动画上下文其实是什么很重要。

当两个视图控制器之间的转场开始时,现有视图将添加到转场容器视图(transition container view)中,而且新视图控制器的视图已建立但还没有可见,以下所示:

image-20181202105011119

所以,如今的任务是将新视图添加到animateTransition()中的转场容器中,以特定动画将其显示,若有须要也是特定动画的方式解除旧视图。

默认状况下,转场动画完成后,旧视图将从转场容器中删除。

image-20181202105026911

下面👇先实现简单的转场动画。

淡出转场

得到动画将在其中进行的容器视图,而后您将获取新视图并将其存储在toView中,在animateTransition()中添加:

let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
复制代码

view(forKey:)viewController(forKey:)两个方法很是相似,分别得到转场动画对应的视图和视图控制器。

继续在animateTransition()中添加:

containerView.addSubview(toView)
toView.alpha = 0.0
UIView.animate(withDuration: duration, animations: {
    toView.alpha = 1.0
}, completion: { _ in
    transitionContext.completeTransition(true)
})
复制代码

在动画完成闭包中调用用completeTransition(),告诉UIKit你的转场动画已经完成,UIKit能够自由地结束视图控制器转场。

目前的效果就是:

pop转场

上面的fade效果不是最终想要的,把animateTransition()中的代码替换为:

let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let herbView = presenting ? toView : transitionContext.view(forKey: .from)!
复制代码

containerView是动画将存在的地方,而toView是要呈现的新视图。 若是是呈现presentingtrue),herbViewtoView,不然将从上下文中获取。 对于呈现解除herbView将始终是表现动画的视图。 当呈详细页的控制器视图时,它将逐渐占用整个屏幕。 当被解除时,它将缩小到图像的原始帧。

在上面代码后添加:

let initialFrame = presenting ? originFrame : herbView.frame
let finalFrame = presenting ? herbView.frame : originFrame

let xScaleFactor = presenting ? initialFrame.width / finalFrame.width : finalFrame.width / initialFrame.width
let yScaleFactor = presenting ? initialFrame.height / finalFrame.height : finalFrame.height / initialFrame.height
复制代码

initialFramefinalFrame分别是初始和最终动画的framexScaleFactoryScaleFactor分别是x轴和y轴上视图变化的比例因子(scale factor)

继续在上面代码后添加:

let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)
        
if presenting {
    herbView.transform = scaleTransform
    herbView.center = CGPoint(x: initialFrame.midX, y: initialFrame.midY)
    herbView.clipsToBounds = true
}
复制代码

当须要呈现新视图时,设置transform,而且定位(设置center

继续在上面代码后添加:

containerView.addSubview(toView)
containerView.bringSubview(toFront: herbView)
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0, options: [], animations: {
    herbView.transform = self.presenting ? CGAffineTransform.identity : scaleTransform
    herbView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
}) { (_) in
    transitionContext.completeTransition(true)
}
复制代码

首先将toView添加到容器中,并确保herbView位于顶部,由于这是动画的惟一视图。

而后,实现动画,在这里使用弹簧动画。在动画表达式中,能够更改herbViewtransform和位置。在呈现时,将从底部的小尺寸变为全屏;在解除时,将全屏缩小变为原始图像大小。

最后,您调用了completeTransition()告诉UIKit转场动画已经完成。

如今的效果:

动画从左上角开始; 这是由于originFrame的默认值的原点是*(0,0)* 。

ViewController.swiftanimationController(forPresented:presenting:source:) 返回代码前添加:

transition.originFrame = selectedImage!.superview!.convert(selectedImage!.frame, to: nil)
transition.presenting = true
selectedImage!.isHidden = true
复制代码

这会将转场动画的originFrame设置为selectedImageframe,并在动画期间隐藏初始图像。

目前的效果是初始小视图转场到全屏了,没有问题,可是解除详情页时就有问题,详情页忽然就消失了:

解除转场

剩下要作的就是解除详细页视图的动画。

ViewController.swiftanimationController(forDismissed:)中添加:

transition.presenting = false
return transition
复制代码

上面的表明 transition对象也做为解除转场动画使用。

转场动画看起来很棒,但解除详细页面后,原始的小尺寸的图片消失了。下面就解决这个问题。

在类PopAnimator中添加一个闭包属性,做为解除动画完成后处理:

var dismissCompletion: (()->Void)?
复制代码

animateTransition(using:)transitionContext.completeTransition(true)以前添加(也就是通知UIKit转场动画结束以前,若是是解除动画,就进行一些处理):

if !self.presenting {
    self.dismissCompletion?()
}
复制代码

ViewController实现具体闭包内容,在viewDidLoad()中添加:

transition.dismissCompletion = {
    self.selectedImage!.isHidden = false
}
复制代码

那么,目前效果:

屏幕旋转转场

设备方向更改视为从视图控制器到其自身的转场过程。

iOS 8中引入的viewWillTransition(to:with:)方法,用来提供了一种简单直接的方法来处理设备方向的变化。 不须要构建单独的纵向或横向布局,而只须要对视图控制器视图的大小进行更改。

ViewController中添加:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    coordinator.animate(alongsideTransition: { context in
                                              self.bgImage.alpha = (size.width > size.height) ? 0.25 : 0.55
                                             }, completion: nil)
}
复制代码

第一个参数(size)指视图控制器变换后的大小。 第二个参数(coordinator)是转场协调对象,它能够访问许多转场的属性。

animate(alongsideTransition:completion:)容许指定本身的自定义动画,与UIKit在更改方向时默认执行的旋转动画一块儿执行。当设备横向时,减小背景图像的透明度,让文本看上去更清晰,更容易阅读。

运行,旋转设备(模拟器中按Cmd +向左箭头):

将屏幕旋转为横向模式时,能够清楚地看到背景变深。

如今上面的动画看上去已经很不错,但若是仔细观看,会发现还有两个问题,解除动画时,全屏视图到小视图完成以前看到详细视图的文本;全屏视图是直角,直到动画要完成的最后一个才从直角忽然变到圆角。

平滑转场动画

纠正了细节视图的文本在被解除时消失的问题。

animateTransition(using:)中的动画(UIView.animate(...))开始前添加:

let herbController = transitionContext.viewController(forKey: presenting ? .to : .from) as! HerbDetailsViewController

if presenting {
    herbController.containerView.alpha = 0.0
}
复制代码

animateTransition(using:)中的动画闭包中添加:

herbController.containerView.alpha = self.presenting ? 1.0 : 0.0
复制代码

圆角动画

最后,为详情页视图的图层角半径设置动画,使其与主视图控制器中草本图像的圆角相匹配。

animateTransition(using:)中的动画闭包中添加:

herbView.layer.cornerRadius = self.presenting ? 0.0 : 20.0/xScaleFactor
复制代码

为了更方便的查看动画,能够把持续时间增大或用模拟器中满动画(Command + T)。

上面两个修改后的效果:

18-导航控制器转场

UINavigationController是iOS中为数很少的内置应用导航解决方案之一。 将一个新的视图控制器推入或弹出导航堆栈,这个过程自带一个时尚的动画。

上图显示了iOS如何将新视图控制器推送到设置应用中的导航堆栈:新视图从右侧滑入以覆盖旧视图,新标题淡入,而旧标题淡出。

本章的自定义导航控制器转场与前一章中构建自定义视图控制器转场的方式相似。

开始项目

本章开始项目是一个新项目,叫LogoReveal

点击默认屏幕任意地方(MasterViewController),跳转展现vacation packing list页面(DetailViewController),RW Logo是经过UIBezierPath绘制的CAShapeLayer图层。

自定义导航控制器转场的原理

自定义导航控制器转场的原理相似上一章节的自定义转场的原理,一样也能够用两个图归纳:

image-20181203214052969

image-20181203214104467

导航控制器代理

首先须要新建一个Animator,新建一个NSObject子类RevealAnimator的类文件,并让它遵照UIViewControllerAnimatedTransitioning协议:

class RevealAnimator: NSObject, UIViewControllerAnimatedTransitioning {

}
复制代码

RevealAnimator中添加两个属性,而且实现UIViewControllerAnimatedTransitioning协议的两个方法:

let animationDuration = 2.0
    var operation: UINavigationControllerOperation = .push
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return animationDuration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) 
    {
        
    }
复制代码

operationUINavigationControllerOperation类型的属性,用于表示是在推送仍是弹出控制器。

用扩展的方式让MasterViewController遵照UINavigationControllerDelegate协议:

extension MasterViewController: UINavigationControllerDelegate {
    
}
复制代码

在调用任何segues或将某些内容推送到堆栈以前,须要在视图控制器生命周期的早期设置导航控制器的代理。在MasterViewControllerviewDidLoad()中添加:

navigationController?.delegate = self
复制代码

MasterViewController中建立Animator属性:

let transition = RevealAnimator()
复制代码

实现协议UINavigationControllerDelegate的方法navigationController():

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    transition.operation = operation
    return transition
}
复制代码

这是一个方法名称很是长,参数有:

navigationController:当对象是多个导航控制器的委托时,这用来区分导航控制器,这不是太常见。

operation:这是一个枚举UINavigationControllerOperation,能够是.push.pop

fromVC:这是当前在屏幕上可见的视图控制器,它一般是导航堆栈中的最后一个视图控制器。

toVC:这是将转场到的视图控制器。

若是须要不一样视图控制器有不一样转场动画,则能够选择返回不一样的Animator。为了简化此项目,在推送或弹出转场时,都返回RevealAnimator对象。

运行,点击,导航栏有一个两秒转场,但其余就没有反应了,这是由于animateTransition()中尚未编写任何代码。

添加自定义显示动画

自定义转场动画的计划相对简单。 您只需在DetailViewController上为蒙版设置动画,使其看起来像RW徽标的透明部分,显示底层视图控制器的内容。 你将不得不处理图层和一些动画任务,可是到目前为止你尚未完成任务。 对于像你这样的动画专业人士来讲,建立转场动画将是一件轻松的事!

RevealAnimator中建立一个存储动画上下文的属性:

weak var storedContext: UIViewControllerContextTransitioning?
复制代码

再在animateTransition()中添加:

storedContext = transitionContext

let fromVC = transitionContext.viewController(forKey: .from) as! MasterViewController
let toVC = transitionContext.viewController(forKey: .to) as! DetailViewController

transitionContext.containerView.addSubview(toVC.view)
toVC.view.frame = transitionContext.finalFrame(for: toVC)
复制代码

先获取fromVC并将其转换为MasterViewController;而后,获取toVC并转换为DetailViewController。 最后,只需将toVC.view添加到转场容器视图中,并将其frame设置为transitionContext中的最终frame,这是详情页面在主屏幕上的最终位置。

将如下内容添加到animateTransition()中:

let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
animation.toValue = NSValue(caTransform3D:      CATransform3DConcat(CATransform3DMakeTranslation(0.0, -10.0, 0.0), CATransform3DMakeScale(150.0, 150.0, 1.0)))
复制代码

这个动画将logo的大小增长了150倍,并同时向上移动了一点。 为何? logo的形状不均匀,我但愿后面的视图控制器经过RW形状的“孔”显示。 将其向上移动意味着缩放图像的底部将更快地覆盖屏幕。

若是使用像圆形或椭圆形这种对称的logo,就不会有这种问题。

如今将如下面代码添加到animateTransition()以稍微优化动画:

animation.duration = animationDuration
animation.delegate = self
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
复制代码

这些都是前面章节的知识。

RevealAnimator目前还不是动画代理,记得要让RevealAnimator遵照CAAnimationDelegate 协议。

animateTransition()中添加图层:

let maskLayer: CAShapeLayer = RWLogoLayer.logoLayer()
maskLayer.position = fromVC.logo.position
toVC.view.layer.mask = maskLayer
maskLayer.add(animation, forKey: nil)
复制代码

效果:

优化细节

细看上面的效果,会发现动画运行时,原来的logo还在那里,下面解决这个问题。

animateTransition()中添加:

fromVC.logo.add(animation, forKey: nil)
复制代码

运行后,没有有原始的logo了:

还有一个稍微复杂一点的问题:在第一次推送转场后,导航再也不工做了?

RevealAnimator中实现CAAnimationDelegateanimationDidStop(_:finished:)方法:

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if let context = storedContext {
        context.completeTransition(!context.transitionWasCancelled)
        // reset logo
    }
    storedContext = nil
}
复制代码

在方法结束时,只需将转场上下文设置为nil。

因为显示动画在完成后不会自动删除,所以须要本身处理。

使用如下内容替换位于animationDidStop()中的// reset logo

let fromVC = context.viewController(forKey: .from) as! MasterViewController

fromVC.logo.removeAllAnimations()
复制代码

只须要在推送转场期间屏蔽视图控制器的内容,一旦视图控制器完成转场,就能够安全地移除屏蔽。

接着上面的代码t添加:

let toVC = context.viewController(forKey: .to) as! DetailViewController
toVC.view.layer.mask = nil
复制代码

运行报错:

image-20181203170902426

这是由于,上面的代码只适用于推送,但不适用于弹出。

animateTransition()中除了第一行storedContext = transitionContext的代码,都包含在if语句中:

if operation == .push {
    ...
}
复制代码

淡入新视图控制器

转场时,给详情页面添加淡入的动画。

animateTransition(using:)if operation == .push {语句中添加:

let fadeIn = CABasicAnimation(keyPath: "opacity")
fadeIn.fromValue = 0.0
fadeIn.toValue = 1.0
fadeIn.duration = animationDuration
toVC.view.layer.add(fadeIn, forKey: nil)
复制代码

弹出转场

前面都是推送转场,如今添加是弹出转场。

给在animateTransition(using:)if语句添加一个else

else {
    let fromView = transitionContext.view(forKey: .from)!
    let toView = transitionContext.view(forKey: .to)!

    transitionContext.containerView.insertSubview(toView, belowSubview: fromView)

    UIView.animate(withDuration: animationDuration, delay: 0.0, options: .curveEaseIn, animations: {
        fromView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
    }) { (_) in
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
       }
}
复制代码

最终效果会是:

19-交互式导航控制器转场

您不只能够为转换建立自定义动画 - 还可使其交互并响应用户的操做。一般,您经过平移手势驱动此操做,这是您将在本章中采用的方法。 当您完成后,您的用户将可以经过在屏幕上滑动手指来来回穿过显示转场。那会有多酷? 是的,我觉得你会感兴趣!继续阅读,了解它是如何完成的!

关于手势处理,可看个人一篇简单的小结 iOS tutorial 13:手势处理

本章开始项目使用上一章节完成的项目。

建立交互式转场

当导航控制器向其代理询问动画控制器(就是以前提到Animator)时,可能会发生两件事。返回nil,在这种状况下,导航控制器会运行标准转场动画; 若是返回一个动画控制器,那么导航控制器除了会向其代理询问转场动画控制器,也会询问交互控制器,以下所示:

image-20181216165544709

交互控制器根据用户的操做移动转场,而不是简单地从开始到结束动画更改。 交互控制器不必定须要是与动画控制器分开的类;实际上,当两个控制器在同一个类中时,执行某些任务会更容易一些。 您只须要确保所述类遵照UIViewControllerAnimatedTransitioningUIViewControllerInteractiveTransitioning两个协议。

UIViewControllerInteractiveTransitioning只有一个必需实现的方法 startInteractiveTransition(_:) ,它将转换上下文做为参数。 而后,交互控制器会按期调用updateInteractiveTransition(_ :)来移动转换。 首先,您须要更改处理用户输入的方式。

处理平移手势

把点击手势修改为平移手势。平移手势可观察到转场的开始、过程和结束的状态。

先把底部的标签的文本修改为 Slide to start

接下来,在MasterViewController.swiftviewDidAppear(_:)中删除如下代码:

let tap = UITapGestureRecognizer(target: self, action: #selector(didTap))
view.addGestureRecognizer(tap)
复制代码

替代为平移手势识别代码:

let pan = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
view.addGestureRecognizer(pan)
复制代码

当用户在屏幕上滑动是,会被识别而后调用didPan(_:)方法。

MasterViewController中添加空didPan(_:)

使用交互式动画师类

为了处理上面的转场,须要使用内置的交互式动画师类:UIPercentDrivenInteractiveTransition。 此类遵照UIViewControllerInteractiveTransitioning协议,并能够将转场的进度表示为完成百分比。

打开RevealAnimator.swift,并更新文件顶部的类定义,以下所示:

class RevealAnimator: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning, CAAnimationDelegate {
    
复制代码

请注意,UIPercentDrivenInteractiveTransition是一个类,而不是其余协议,因此须要处于第一位置。

添加一个属性,来表示是否已交互方式驱动转场动画:

var interactive = false
复制代码

添加方法到RevealAnimator中:

func handlePan(_ recognizer: UIPanGestureRecognizer) {

}
复制代码

当用户在屏幕上平移时,识别器将被传递给RevealAnimator中的handlePan(_:)处理,来更新当前的转场进度。

MasterViewController.swift中添加委托方法:

func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    if !transition.interactive {
        return nil
    }
    return transition
}
复制代码

当但愿转场为交互式时,只需返回交互式控制器,不然返回nil

如今,须要将平移手势识别器链接到交互控制器。 在didPan(_:)中添加:

switch recognizer.state {
    case .began:
        transition.interactive = true
        performSegue(withIdentifier: "details", sender: nil)
    default:
        transition.handlePan(recognizer)
}
复制代码

当平移手势开始时,确保交互设置为true,而后经过 segue 链接到下一个视图控制器。 执行segue将启动转场,这时动画控制器和交互控制器的委托方法将返回转场动画。

若是手势已经开始,只需将操做交给交互控制器,以下图所示:

image-20181203233104113

计算转场动画的进度

平移手势处理程序中最重要的一点是要弄清楚转场的进度。

打开RevealAnimator.swift,并将如下代码添加到handlePan中:

let translation = recognizer.translation(in: recognizer.view!.superview!)
var progress: CGFloat = abs(translation.x / 200.0)
progress = min(max(progress, 0.01), 0.99)
复制代码

经过平移手势识别器计算转场的经度。从逻辑上讲,用户离开初始位置越远,转场的进度就越大。

200.0是一个合理的任意数字,来表示转场完成所须要的距离。

下面更新转场动画的进度,将如下代码添加到handlePan()中:

switch recognizer.state {
    case .changed:
        update(progress)
    default:
        break
}
复制代码

update() 是来自UIPercentDrivenInteractiveTransition的方法,它设置转场动画的当前进度。

当用户在屏幕上平移时,手势识别器会重复调用MasterViewControllerdidPan(),从而不停的调用RevealAnimator中 的handlePan()来更新转场进度。

RevealAnimator中添加属性:

private var pausedTime: CFTimeInterval = 0
复制代码

如今,经过将如下代码添加到animateTransition(using:)来控制图层:

if interactive {
    let transitionLayer = transitionContext.containerView.layer
    pausedTime = transitionLayer.convertTime(CACurrentMediaTime(), from: nil)
    transitionLayer.speed = 0
    transitionLayer.timeOffset = pausedTime
}
复制代码

这里作的是阻止图层运行本身的动画。 这将冻结全部子图层动画。

重写update(_:),以将图层与动画一块儿移动:

override func update(_ percentComplete: CGFloat) {
    super.update(percentComplete)

    let animationProgress = TimeInterval(animationDuration) * TimeInterval(percentComplete)
    storedContext?.containerView.layer.timeOffset = pausedTime + animationProgress
}
复制代码

运行效果:

这边出现问题,就是手指离开屏幕后,动画当即中止,再次滑动时也没有反应。

处理提早终止

处理上面的问题。

handlePan()的switch语句中添加case

case .cancelled, .ended:
    if progress < 0.5 {
        cancel()
    } else {
        finish()
    }
复制代码

在用户手指离开屏幕以前,若是平移得足够远,就表示转场完成,呈现新的视图控制器;相反,就滚回原来的视图控制器。

重写cancel()finish()方法:

override func cancel() {
    restart(forFinishing: false)
    super.cancel()
}

override func finish() {
    restart(forFinishing: true)
    super.finish()
}

private func restart(forFinishing: Bool) {
    let transitionLayer = storedContext?.containerView.layer
    transitionLayer?.beginTime = CACurrentMediaTime()
    transitionLayer?.speed = forFinishing ? 1 : -1
}
复制代码

.cancelled,.endedcase中添加:

interactive = false
复制代码

本章最后的效果:

本文在个人我的博客中地址:系统学习iOS动画之四:视图控制器的转场动画

相关文章
相关标签/搜索