一、什么是转场
转场:通俗讲,转场就是从当前页面跳转到下一个页面的过程
转场动画: 在当前视图消失和下一个视图出现的过程中,执行的动画,就是转场动画。
动画代理: UIViewControllerAnimatedTransitioning
二、转场的类型
按照页面切换类型分为(前两种属于容器VC转场):
1)、Modal转场: presentation、dismissal
转场代理: UIViewControllerTransitioningDelegate
2)、UINavigationController转场: push、pop
转场代理: UINavigationControllerDelegate
3)、UITabBarController转场: Tab切换
转场代理: UITabBarControllerDelegate
按照转场进度是否可控分为:
1)、非交互式转场
2)、交互式转场
三、转场结果
非交互式转场: 完成
交互式转场: 完成、取消
-------------这是一根牛逼闪闪放光彩的分割线------------------
上面扯了那么多,现在接上正文,下面我们将实现Navi、Modal、Tab、三种方式的转场动画,前两种都是模仿的系统动画,旨再阐述你如何实现转场动画。如果你要其他酷炫的动画,只要在UIViewControllerAnimatedTransitioning
协议的方法里,修改成你需要的酷炫动画就行了。
首先,为了方便调用,我们来封装一个工具类,把三种转场类型的代理、转场动画封装到一起,也就是说这个工具类需要实现UIViewControllerTransitioningDelegate
, UINavigationControllerDelegate
, UITabBarControllerDelegate
以及UIViewControllerAnimatedTransitioning
四个代理方法。
封装转场工具类
///通用转场工具类
class TransitionUtil: NSObject {
///转场类型
var transitionType: TransitionType?
//交互转场
var interactive = false
let interactionTransition = UIPercentDrivenInteractiveTransition()
override init() {
super.init()
}
}
///转场类型
enum TransitionType {
//导航栏
case navigation(_ operation: UINavigationController.Operation)
//tabBar切换
case tabBar(_ direction: TabBarOperationDirection)
//模态跳转
case modal(_ operation: ModalOperation)
}
enum TabBarOperationDirection {
case left
case right
}
enum ModalOperation {
case presentation
case dismissal
}
1)、模态转场代理: UIViewControllerTransitioningDelegate
///自定义模态转场动画时使用
extension TransitionUtil: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.transitionType = .modal(.presentation)
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.transitionType = .modal(.dismissal)
return self
}
//interactive false:非交互转场, true: 交互转场
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactive ? self.interactionTransition : nil
}
//interactive false:非交互转场, true: 交互转场
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactive ? self.interactionTransition : nil
}
}
2) 、导航转场代理: UINavigationControllerDelegate
/// 自定义navigation转场动画时使用
extension TransitionUtil: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.transitionType = .navigation(operation)
return self
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactive ? self.interactionTransition : nil
}
}
3)、TabBar转场代理: UITabBarControllerDelegate
/// 自定义tab转场动画时使用
extension TransitionUtil: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let fromIndex = tabBarController.viewControllers?.firstIndex(of: fromVC) ?? 0
let toIndex = tabBarController.viewControllers?.firstIndex(of: toVC) ?? 0
let direction: TabBarOperationDirection = fromIndex < toIndex ? .right : .left
self.transitionType = .tabBar(direction)
return self
}
func tabBarController(_ tabBarController: UITabBarController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactive ? self.interactionTransition : nil
}
}
4)、转场动画代理
尽管三大转场代理协议的方法不尽相同,但它们返回的动画控制对象遵守的是同一个协议: UIViewControllerAnimatedTransitioning
extension TransitionUtil: UIViewControllerAnimatedTransitioning {
//控制转场动画执行时间
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
//执行动画的地方,最核心的方法。
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
transitionAnimation(transitionContext: transitionContext)
}
}
//MARK: ------------ 不同类型的转场 ------------
private func transitionAnimation(transitionContext: UIViewControllerContextTransitioning) {
//获得容器视图(转场动画发生的地方)
let containerView = transitionContext.containerView
//动画执行时间
let duration = self.transitionDuration(using: transitionContext)
//fromVC (即将消失的视图)
let fromVC = transitionContext.viewController(forKey: .from)!
let fromView = fromVC.view!
//toVC (即将出现的视图)
let toVC = transitionContext.viewController(forKey: .to)!
let toView = toVC.view!
var offset = containerView.frame.width
var fromTransform = CGAffineTransform.identity
var toTransform = CGAffineTransform.identity
switch transitionType {
case .modal(let operation):
offset = containerView.frame.height
let fromY = operation == .presentation ? 0 : offset
fromTransform = CGAffineTransform(translationX: 0, y: fromY)
let toY = operation == .presentation ? offset : 0
toTransform = CGAffineTransform(translationX: 0, y: toY)
if operation == .presentation {
containerView.addSubview(toView)
}
case .navigation(let operation):
offset = operation == .push ? offset : -offset
fromTransform = CGAffineTransform(translationX: -offset, y: 0)
toTransform = CGAffineTransform(translationX: offset, y: 0)
containerView.insertSubview(toView, at: 0)
//containerView.addSubview(toView)
case .tabBar(let direction):
offset = direction == .left ? offset : -offset
fromTransform = CGAffineTransform(translationX: offset, y: 0)
toTransform = CGAffineTransform(translationX: -offset, y: 0)
containerView.addSubview(toView)
case nil:
break
}
toView.transform = toTransform
UIView.animate(withDuration: duration, animations: {
fromView.transform = fromTransform
toView.transform = .identity
}) { (finished) in
fromView.transform = .identity
toView.transform = .identity
//考虑到转场中途可能取消的情况,转场结束后,恢复视图状态。(通知是否完成转场)
let wasCancelled = transitionContext.transitionWasCancelled
transitionContext.completeTransition(!wasCancelled)
}
}
非交互转场跳转
封装完工具类,基本就大功搞成了,下面只需要在跳转的时候创建一个TransitionUtil对象,设置为代理就行了。
这里模态和导航跳转都是模仿的系统动画,如果把动画时间改长一点,你就能确实知道,这是我们自己设计的动画,而不是系统动画.
模态跳转:
@IBAction func modalTransitionClicked(_ sender: Any) {
let vc = ModalTransitionViewController()
//设置转场代理(必须在跳转前设置)
//与容器 VC 的转场的代理由容器 VC 自身的代理提供不同,Modal 转场的代理由 presentedVC 提供
vc.transitioningDelegate = transitionUtil
/*
.FullScreen 的时候,presentingView 的移除和添加由 UIKit 负责,在 presentation 转场结束后被移除,dismissal 转场结束时重新回到原来的位置;
.Custom 的时候,presentingView 依然由 UIKit 负责,但 presentation 转场结束后不会被移除。
*/
vc.modalPresentationStyle = .custom
//animated: 一定要给true,不然不会出现转场动画
self.present(vc, animated: true, completion: nil)
}
导航跳转:
@IBAction func naviTransitionClicked(_ sender: Any) {
let vc = NaviTransitionViewController()
vc.hidesBottomBarWhenPushed = true
//设置转场代理
self.navigationController?.delegate = transitionUtil
self.navigationController?.pushViewController(vc, animated: true)
}
Tab切换:
创建一个继承UITabBarController的ScrollTabBarViewController类,设置代理为创建的TransitionUtil对象就行了。然后把项目中的系统类UITabBarController换成ScrollTabBarViewController就行了。
这样点击切换tab时就会又一个左滑右滑的动画,效果如下:
交互转场跳转
交互转场无非就是用户自己控制转场进度,那么如何控制呢,一般都是通过一个滑动手势。不管是模态、导航、tab,这些转场类型要添加交互功能,都只需要在self.view上添加一个pan手势就行。
下面以tab的交互转场为例:
class ScrollTabBarViewController: UITabBarController {
//滑动手势
private var panGesture = UIPanGestureRecognizer()
//转场动画
private let transitionUtil = TransitionUtil()
private var vcCount: Int {
guard let vcs = self.viewControllers else { return 0 }
return vcs.count
}
override func viewDidLoad() {
super.viewDidLoad()
//设置转场代理
self.delegate = transitionUtil
self.tabBar.tintColor = .green
//添加交互手势
panGesture.addTarget(self, action: #selector(panHandle(_:)))
self.view.addGestureRecognizer(panGesture)
}
}
@objc func panHandle(_ pan: UIPanGestureRecognizer) {
let translationX = panGesture.translation(in: view).x
let absX = abs(translationX)
let progress = absX / view.frame.width
switch panGesture.state {
case .began:
transitionUtil.interactive = true
//速度
let velocityX = panGesture.velocity(in: view).x
if velocityX < 0 {
if selectedIndex < vcCount - 1 {
selectedIndex += 1
}
}else {
if selectedIndex > 0 {
selectedIndex -= 1
}
}
case .changed:
//更新转场进度,进度数值范围为0.0~1.0。
transitionUtil.interactionTransition.update(progress)
case .cancelled, .ended:
/*
这里有个小问题,转场结束或是取消时有很大几率出现动画不正常的问题.
解决手段是修改交互控制器的 completionSpeed 为1以下的数值,这个属性用来控制动画速度,我猜测是内部实现在边界判断上有问题。
这里其修改为0.99,既解决了 Bug 同时尽可能贴近原来的动画设定.
*/
if progress > 0.3 {
transitionUtil.interactionTransition.completionSpeed = 0.99
//.finish()方法被调用后,转场动画从当前的状态将继续进行直到动画结束,转场完成
transitionUtil.interactionTransition.finish()
}else {
//转场取消后,UITabBarController 自动恢复了 selectedIndex 的值,不需要我们手动恢复。
transitionUtil.interactionTransition.completionSpeed = 0.99
//.cancel()被调用后,转场动画从当前的状态回拨到初始状态,转场取消。
transitionUtil.interactionTransition.cancel()
}
//无论转场的结果如何,恢复为非交互状态。
transitionUtil.interactive = false
default:
break
}
}
效果如下:
番外篇: 如何实现一个圆形转场动画
这种效果有两种方式:
1)、 不使用mask: 视图+缩放
2)、 使用mask: UIBezierPath + CAShapeLayer + maskLayer
第一种实现看起比较丝滑,下面就说一下其如何实现:
其他地方根上面的实现一模一样,唯一的不同是UIViewControllerAnimatedTransitioning
协议里animateTransition(using transitionContext: UIViewControllerContextTransitioning)
的实现不同。
//执行动画的地方,最核心的方法。
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//圆形遮罩,方式一
bubble(transitionContext: transitionContext)
}
具体的动画实现:
func bubble(transitionContext: UIViewControllerContextTransitioning) {
//获得容器视图(转场动画发生的地方)
let containerView = transitionContext.containerView
//动画执行时间
let duration = self.transitionDuration(using: transitionContext)
//fromVC (即将消失的视图)
let fromVC = transitionContext.viewController(forKey: .from)!
let fromView = fromVC.view!
//toVC (即将出现的视图)
let toVC = transitionContext.viewController(forKey: .to)!
let toView = toVC.view!
let originalCenter = toView.center
let originalSize = toView.frame.size
//半径
let radius = self.getRadius(startPoint: startPoint, originalSize: originalSize)
switch transitionType {
case .modal(let operation):
if operation == .presentation {
let bubble = UIView()
bubble.frame = CGRect(x: 0, y: 0, width: radius*2, height: radius*2)
bubble.layer.cornerRadius = bubble.frame.size.height / 2
bubble.center = startPoint
bubble.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
bubble.backgroundColor = toView.backgroundColor
containerView.addSubview(bubble)
toView.center = startPoint
toView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
toView.alpha = 0
containerView.addSubview(toView)
UIView.animate(withDuration: duration) {
bubble.transform = .identity
toView.transform = .identity
toView.alpha = 1
toView.center = originalCenter
} completion: { (isFinished) in
transitionContext.completeTransition(true)
bubble.isHidden = true
if toVC.modalPresentationStyle == .custom {
toVC.endAppearanceTransition()
}
fromVC.endAppearanceTransition()
}
}else {
if fromVC.modalPresentationStyle == .custom {
fromVC.beginAppearanceTransition(false, animated: true)
}
toVC.beginAppearanceTransition(true, animated: true)
let bubble = UIView()
bubble.frame = CGRect(x: 0, y: 0, width: radius*2, height: radius*2)
bubble.layer.cornerRadius = bubble.frame.size.height / 2
bubble.backgroundColor = fromView.backgroundColor
bubble.center = startPoint
bubble.isHidden = false
containerView.insertSubview(bubble, at: 0)
UIView.animate(withDuration: duration) {
bubble.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
fromView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
fromView.center = self.startPoint
fromView.alpha = 0
} completion: { (isFinished) in
transitionContext.completeTransition(true)
fromView.removeFromSuperview()
bubble.removeFromSuperview()
if fromVC.modalPresentationStyle == .custom {
fromVC.endAppearanceTransition()
}
toVC.endAppearanceTransition()
}
}
default:
break
}
}
计算圆的半径:
///获得半径 (不明白的自己画个图,看一下哪一条应该是半径)
private func getRadius(startPoint: CGPoint, originalSize: CGSize) -> CGFloat {
let horizontal = max(startPoint.x, originalSize.width - startPoint.x)
let vertical = max(startPoint.y, originalSize.height - startPoint.y)
//勾股定理计算半径
let radius = sqrt(horizontal*horizontal + vertical*vertical)
return radius
}
效果如下(可交互):
附参考文章:
iOS 视图控制器转场详解: https://blog.devtang.com/2016/03/13/iOS-transition-guide/