历时5天从各种英文教程中学习到的过渡动画,是一个很难忘的探索经历
比较好的参考文章自定义UIViewController过渡入门 ,动画入门。
转场方式
首先让我们来了解iOS转场的方式:
- UINavigationController push/pop UIViewController导航栏的转场
- UITabBarController 切换Tab的转场
- present/dismiss 模态的方式转场
这是iOS提供的3种基本转场方式,默认的转场方式转场风格有限。例如模态转场中,尽管有modalPresentationStyle
和modalTransitionStyle
关于展现风格和过渡风格的设置,但是转场仍是死板从底部弹出。这并不能满足我们在软件开发的需求,侧边栏、顶部栏的动画效果都无法很好地实现。在iOS 7.0之后Apple提供一套完整的自定义过渡动画的API,为各种转场动画的实现带了无限可能。
这里主要介绍模态转场的自定义动画。
顶部栏的动画效果:
Modal 转场
模态转场分为非交互式转场和交互式转场。
非交互式转场也就是普通转场,转场的动画无法交互,不能在动画的过程中终止转场。
交互式转场能通过手指触摸屏幕通过滑动体验过渡动画的进行,并能终止动画过程。
过渡动画API
我们定义:如果视图控制器Apresent
之后展示视图控制器B。在后文中,源视图控制器为fromVC,目标视图控制器为toVC。
状态 | 视图控制器A | 视图控制器B |
---|---|---|
Present | 源视图控制器 | 目标视图控制器 |
Dissmiss | 目标视图控制器 | 源视图控制器 |
transitioningDelegate 过渡代理
每个视图控制器UIViewController都有一个transitioningDelegate属性,该代理需遵循UIViewControllerTransitioningDelegate
协议,提供相关动画控制器。
每当您显示或关闭视图控制器时,UIKit都会要求其过渡代理使用动画控制器。要将默认动画替换为您自己的自定义动画,必须实现过渡代理,并使其返回适当的动画控制器。
AnimationController 动画控制器
过渡代理在present/dismiss时返回相应的动画控制器。动画控制器是过渡动画的核心。它完成了动画过渡的“繁重工作”。
TransitioningContext 过渡语境
过渡语境在过渡过程中实现并起着至关重要的作用:它封装了有关过渡中涉及的视图和视图控制器的信息。过渡语境辅助动画控制器实现动画。
从图中可以看出,自己并没有实现此协议。UIKit会为您创建和配置过渡上下文,并在每次发生过渡时将其传递给动画控制器。
非交互式过渡动画的过程
以present过渡动画为例:
- 通过代码或
StoryBoard segue
触发模态视图present过程。 - UIKit向
toVC
(目标视图控制器)请求其过渡代理。如果没有,UIKIt将使用标准的内置过渡。 - 然后,UIKit通过来向过渡代理请求动画控制器
animationController(forPresented:presenting:source:)
。如果返回nil
,则过渡将使用默认动画。 - UIKit构造过渡语境。UIKit通过调用向动画控制器询问动画的持续时间
transitionDuration(using:)
。UIKitanimateTransition(using:)
在动画控制器上调用以执行过渡的动画。 - 最后,动画控制器调用
completeTransition(_:)
过渡上下文以指示动画已完成。
dimiss过渡动画的步骤几乎相同。
在这种情况下,UIKit向fromVC
视图控制器(正在关闭的视图控制器)请求其过渡代理,要求提供动画控制器animationController(forDismissed:)
。
非交互式过渡动画需要提供的条件
- 设置过渡动画代理。设置(目标视图控制器)的
transitioningDelegate
属性,即设置过渡动画代理对象,该代理对象遵循UIViewControllerTransitioningDelegate
协议,实现forPresented
和forDismissed
两个方法,分别提供present和dismiss的视图控制器实例。 - 创建动画控制器。创建present和dismiss的动画控制器,实现动画持续时间和构造动画方法。
这里我们完成一个简单的从左到右的过渡动画。
1. 构建present/dimiss场景
这里不再讲述构建过程,无论是stroyboard还是纯代码都是很好完成的。这里为了方便,使用storyboard完成。
左视图控制器为:ViewController
右视图控制器为:LeftViewController
2. 创建Animation Controller
Animation Controller是自定义过渡动画的核心对象。
AnimationControlle继承至NSObject
,遵循UIViewControllerAnimatedTransitioning
协议,实现两个required方法。
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
//要求提供动画师对象的动画持续时间属性
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//过渡动画的实现效果,也是自定义过渡动画的核心方法,需要构建动画的实现。
}
}
这里贴出Present状态下的AnimationController的代码,Dismiss状态与其类似(动画过程执行反过程)。
import UIKit
class PresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// 1
guard let _ = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) else {
return
}
// 2
let containerView = transitionContext.containerView
containerView.addSubview(toVC.view)
let duration = transitionDuration(using: transitionContext)
toVC.view.frame = CGRect(x: -ScreenWidth, y: 0, width: ScreenWidth, height: ScrennHeight)
// 3
UIView.animate(withDuration: duration, animations: {
toVC.view.frame = CGRect(x: 0, y: 0, width: ScreenWidth, height: ScrennHeight)
}) { (_) in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
在第一个transitionDuration(using:)
方法中设置动画的持续时间。
在第二个transitionDuration(using:)
方法中构建动画。
- 获取过渡动画所需的视图控制器及snapshot。从过渡语境中
transitionContext.viewController
我们可以获取源视图控制器fromVC和目标视图控制器toVC,过渡语境封装了设计的视图控制器的信息,极大地帮助我们处理视图控制器的动画转化。还可以获取fromVC和toVC的snapshot(屏幕快照),来构造更加复杂和优秀的动画。 - 管理过渡语境的容器视图 --- containerView和视图动画的位置初始化。UIKit将整个过渡封装在容器视图中,以简化视图层次结构和动画的管理,容器视图负责管理
fromVC.view
和toVC.view
。由UIKit创建的容器视图仅包含fromVC视图。您必须添加任何其他将参与过渡的视图。
addSubview(_:)将新视图置于视图层次结构中的所有其他视图之前,因此添加子视图的顺序很重要。
- 设置动画效果。动画有两种实现方法,一种是基础动画
animate
,另一种是关键帧动画animateKeyframes
。这里只是简单地将fromVC.view
从屏幕的左边界外移动到屏幕中。<font color = red>注意:在动画完成后需要调用completeTransition(_:)
通知UIKit动画已完成。这将确保最终状态是一致的。</font>
3.设置过渡动画代理
设置(目标视图控制器)destinationVC的modalPresentationStyle
为枚举属性custom,过渡动画的代理为self即(源视图控制器)ViewController。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destinationVC = segue.destination as? LeftViewController {
destinationVC.modalPresentationStyle = .custom
destinationVC.transitioningDelegate = self
}
}
然后在(源视图控制器)添加扩展,遵循UIViewControllerTransitioningDelegate
协议,实现forPresented
和forDismissed
方法。
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//目标VC是presented,源VC是presenting
guard let _ = presented as? LeftViewController, let _ = presenting as? ViewController else {
return nil
}
return PresentAnimationController()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let _ = dismissed as? LeftViewController else {
return nil
}
return DismissAnimationController()
}
}
这样就构建好了一个简单、基础的自定义转场过渡动画。
交互式过渡动画
交互式动画会使使用者的动画体验更自然舒适而不显得尖锐,给控制动画过程留下了余地。iOS原生APP设置中便有这样的交互式动画的例子。
过渡动画的进度跟随手指的滑动来启动/终止转场动画,这样可以带来良好的用户交互体验。
交互式控制器的工作方式
交互控制器通过加快,减慢甚至反转过渡的过程来响应触摸事件或编程输入。为了启用交互式转换,转换代理必须提供一个交互控制器。您已经制作了过渡动画。在过渡动画的基础上,Apple将交互式动画封装成交互控制器将响应手势来管理此动画,而不是让其像视频一样播放。Apple提供了现成的UIPercentDrivenInteractiveTransition
类,通过继承该类来创建自己的交互式控制器。
1. 创建交互式控制器
我们在之前过渡动画的基础上构建交互式过渡动画,我们首先需要创建交互式控制器。这里贴出显示交互式控制器PresentInteractionController
的代码,继承封装好的UIPercentDrivenInteractiveTransition
类。
class PresentInteractionController: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
private var shouldCompleteTrantision = false
private weak var viewController: UIViewController!
private weak var toViewController: UIViewController!
init(viewController: UIViewController, toViewController: UIViewController) {
super.init()
self.viewController = viewController
self.toViewController = toViewController
prepareGestureRecognizer(in: self.viewController.view)
}
private func prepareGestureRecognizer(in view: UIView) {
let gesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleGsture(_:)))
gesture.edges = .left
view.addGestureRecognizer(gesture)
}
@objc func handleGsture(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview)
var progress = translation.x / 200
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch gestureRecognizer.state {
case .began:
interactionInProgress = true
viewController.present(toViewController, animated: true, completion: nil)
case .changed:
shouldCompleteTrantision = progress > 0.5
update(progress)
case .cancelled:
interactionInProgress = false
cancel()
case .ended:
interactionInProgress = false
if shouldCompleteTrantision {
finish()
} else {
cancel()
}
default:
break
}
}
}
-
interactionInProgress
Bool属性,表示交互式场景是否在发生。 -
shouldCompleteTrantision
Bool属性,表示是否应该终止过渡动画。用于内部管理过渡。 -
viewController
和toViewController
,获取源视图控制器和目标视图控制器的引用,用于管理过渡某状态present视图控制器,达到交互式控制器与动画控制器相联系的作用。 -
prepareGestureRecognizer(in:)
为源视图添加屏幕手势的方法,这里的交互式动画为从左往右present出VC,所以为源视图添加屏幕相应在.left
的手势。 -
handleGsture(_:)
为相应手势变化从而改变过渡动画状态的方法。通过声明局部变量以跟踪滑动进度translation
,根据translation
在视图中获取并计算过渡进度progress
。手势开始时,您将设置interactionInProgress
为true
并触发prsent
视图控制器。手势移动时,调用update(_:)
更新过渡进度。这是一种根据UIPercentDrivenInteractiveTransition
您传入的百分比移动过渡的方法。如果取消手势,则更新interactionInProgress
并取消过渡。一旦动作已经结束,您使用的过渡进度来决定cancel()
或finish()
。
2. 控制器联系
(源)视图控制器与交互控制器相联
在viewController
中添加以下属性:
var presentInteractionController: PresentInteractionController?
并在viewDidLoad()
方法中初始化属性:
presentInteractionController = PresentInteractionController(viewController: self, toViewController: leftViewController)
交互控制器与动画控制器相联
在PresentAnimationController
中添加以下属性:
let interactionController: PresentInteractionController?
并添加init
方法:
init(interactionController: PresentInteractionController?) {
self.interactionController = interactionController
}
3. 实现过渡代理协议方法
动画控制器forPresented
方法中修改PresentAnimationController
对象的创建。
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//展现VC是presented,源VC是presenting
guard let _ = presented as? LeftViewController, let fromVC = presenting as? ViewController else {
return nil
}
return PresentAnimationController(interactionController: fromVC.presentInteractionController)
}
添加以下方法:
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? PresentAnimationController,
let interactionController = animator.interactionController,
interactionController.interactionInProgress else {
return nil
}
return interactionController
}
这首先检查所涉及的动画控制器是否为PresentAnimationController
。如果是这样,它将获取关联的交互控制器的引用,并验证用户交互正在进行中。如果不满足这些条件中的任何一个,它将返回nil
以便转换将继续进行而不会发生交互。否则,它将交互控制器交还给UIKit,以便它可以管理过渡。
dismiss
状态的交互式控制器的方法也是相近的,最终实现效果如下:
以上便是自定义过渡动画的全部内容,读者学习完后可以学一些进阶动画,参考一些优秀App的转场动画,进而创建自己过渡动画,从而获得良好的用户体验。