本文是我学习《iOS Animations by Tutorials》 笔记中的一篇。 文中详细代码都放在我的Github上 andyRon/LearniOSAnimations。
之前学习了视图动画、图层动画、自动布局动画等。这个部分让视野更大一点,学习整个视图控制器的动画,视图控制器的转场动画(View Controller Transition Animations)。
iOS中最容易识别的动画之一是将新视图控制器推入导航堆栈的动画,当我们想让APP有自己的特色,自定义转场动画是非常好的方式。
在本文,将学习如何使用动画创建自己的自定义视图控制器转换。
预览:
17-视图控制器转场和屏幕旋转转场 了解如何通过自定义动画转场呈现视图控制器 - 作为奖励,您将创建动画转场以处理设备方向更改。
18-导航控制器转场
19-交互式导航控制器转场
17-视图控制器转场和屏幕旋转转场
无论是呈现 照相机视图控制器、地址簿还是自定义的模态屏幕,每次都调用相同的UIKit方法:present(_:animated:completion:)
。 此方法将当前屏幕“放弃”,然后跳到另一个视图控制器。
下图呈现一个“New Contact”视图控制器向上滑动以覆盖当前视图(联系人列表),这是默认的动画方式:
在本章中,学习创建自己的自定义演示控制器动画,以替换默认的动画,并使本章的项目更加生动。
开始项目
本章开始项目是一个新项目,叫BeginnerCook。
这个开始项目可以简单概括 如下,ViewController
中包括一个背景图UIImageView
,一个标题UILabel
,一个文本视图UITextView
,下面是一个可以左右移动的UIScrollView
。这个UIScrollView
里会用代码加入一些香草(herb)图片,点击图片会跳转到另个展示详情的视图控制器HerbDetailsViewController
,这个转场是标准的从下到上的垂直覆盖转场动画。
开始项目预览一下:
自定义转场的原理
UIKit实现自定义转场动画是通过代理模式完成的。因此首先需要让ViewController
遵守UIViewControllerTransitioningDelegate
协议。
每次呈现新的视图控制器时,UIKit都会询问其代理是否要使用自定义转场。以下是自定义转场动画的第一步:
需要实现animationController(forPresented:presenting:source:)
方法,这个方法如果返回nil
,则进行默认的转场动画,如果返回时遵守UIViewControllerAnimatedTransitioning
协议的对象,则将这个对象作为自定义转场的Animator(可以翻译为动画师)。
在UIKit使用自定义Animator之前,还需要一些步骤:
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)中,并且新视图控制器的视图已创建但尚未可见,如下所示:
因此,现在的任务是将新视图添加到animateTransition()
中的转场容器中,以特定动画将其显示,如有需要也是特定动画的方式解除旧视图。
默认情况下,转场动画完成后,旧视图将从转场容器中删除。
下面?先实现简单的转场动画。
淡出转场
获得动画将在其中进行的容器视图,然后您将获取新视图并将其存储在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
是要呈现的新视图。 如果是呈现(presenting
为true
),herbView
是toView
,否则将从上下文中获取。 对于呈现和解除,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
复制代码
initialFrame
和finalFrame
分别是初始和最终动画的frame
,xScaleFactor
和yScaleFactor
分别是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
位于顶部,因为这是动画的唯一视图。
然后,实现动画,在这里使用弹簧动画。在动画表达式中,可以更改herbView
的transform
和位置。在呈现时,将从底部的小尺寸变为全屏;在解除时,将全屏缩小变为原始图像大小。
最后,您调用了completeTransition()
告诉UIKit转场动画已经完成。
现在的效果:
动画从左上角开始; 这是因为originFrame的默认值的原点是*(0,0)* 。
在ViewController.swift的animationController(forPresented:presenting:source:)
返回代码前添加:
transition.originFrame = selectedImage!.superview!.convert(selectedImage!.frame, to: nil)
transition.presenting = true
selectedImage!.isHidden = true
复制代码
这会将转场动画的originFrame
设置为selectedImage
的frame
,并在动画期间隐藏初始图像。
目前的效果是初始小视图转场到全屏了,没有问题,但是解除详情页时就有问题,详情页突然就消失了:
解除转场
剩下要做的就是解除详细页视图的动画。
在ViewController.swift的animationController(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
图层。
自定义导航控制器转场的原理
自定义导航控制器转场的原理类似上一章节的自定义转场的原理,同样也可以用两个图概括:
导航控制器代理
首先需要新建一个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)
{
}
复制代码
operation
是UINavigationControllerOperation
类型的属性,用于表示是在推送还是弹出控制器。
用扩展的方式让MasterViewController
遵守UINavigationControllerDelegate
协议:
extension MasterViewController: UINavigationControllerDelegate {
}
复制代码
在调用任何segues或将某些内容推送到堆栈之前,需要在视图控制器生命周期的早期设置导航控制器的代理。在MasterViewController
的viewDidLoad()
中添加:
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
中实现CAAnimationDelegate
的animationDidStop(_:finished:)
方法:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let context = storedContext {
context.completeTransition(!context.transitionWasCancelled)
// reset logo
}
storedContext = nil
}
复制代码
在方法结束时,只需将转场上下文设置为ni
l。
由于显示动画在完成后不会自动删除,因此需要自己处理。
使用以下内容替换位于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
复制代码
运行报错:
这是因为,上面的代码只适用于推送,但不适用于弹出。
把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,在这种情况下,导航控制器会运行标准转场动画; 如果返回一个动画控制器,那么导航控制器除了会向其代理询问转场动画控制器,也会询问交互控制器,如下所示:
交互控制器根据用户的操作移动转场,而不是简单地从开始到结束动画更改。 交互控制器不一定需要是与动画控制器分开的类;实际上,当两个控制器在同一个类中时,执行某些任务会更容易一些。 您只需要确保所述类遵守UIViewControllerAnimatedTransitioning
和UIViewControllerInteractiveTransitioning
两个协议。
UIViewControllerInteractiveTransitioning
只有一个必需实现的方法 startInteractiveTransition(_:)
,它将转换上下文作为参数。 然后,交互控制器会定期调用updateInteractiveTransition(_ :)来移动转换。 首先,您需要更改处理用户输入的方式。
处理平移手势
把点击手势修改成平移手势。平移手势可观察到转场的开始、过程和结束的状态。
先把底部的标签的文本修改成 Slide to start。
接下来,在MasterViewController.swift
的viewDidAppear(_:)
中删除以下代码:
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将启动转场,这时动画控制器和交互控制器的委托方法将返回转场动画。
如果手势已经开始,只需将操作交给交互控制器,如下图所示:
计算转场动画的进度
平移手势处理程序中最重要的一点是要弄清楚转场的进度。
打开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
的方法,它设置转场动画的当前进度。
当用户在屏幕上平移时,手势识别器会重复调用MasterViewController
中didPan()
,从而不停的调用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,.ended
的case
中添加:
interactive = false
复制代码
本章最后的效果:
本文在我的个人博客中地址:系统学习iOS动画之四:视图控制器的转场动画