经过了上周的基本动画和核心动画的理解, 我们今天来讲讲自定义转场动画, 自定义转场动画是iOS中比较高阶的动画形式了, 简单来说就是在切换控制器时发生的动画形式, 可以在Push/Pop ,Modal(present/dismiss)以及tabbar切换时进行动画. 代码见:github
对于Modal(present/dismiss)以及tabbar切换的自定义动画可以看我之前的博客: Modal 请点击 –> iOS 狂霸酷炫拽之Button动效 Tabbar 请点击 –> iOS 会跳舞的TabbarController
本期我们来着重说说Push/Pop里如何进行自定义转场动画, 和之前的核心动画一样, 我们还是一如既往的先讲API, 再进行实战的演练, 对于自定义转场我们要分三步走: 1) 设置代理, 2)遵守协议, 3)实现代理方法.我们先从协议开始看起:
导航控制器代理方法:
public protocol UINavigationControllerDelegate : NSObjectProtocol
@available(iOS 7.0, *)
optional public func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
@available(iOS 7.0, *)
optional public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
- interactionControllerFor 与用户交互时调用的代理方法
- animationControllerFor 自定义转场切换时调用的代理方法
自定义转场协议:
public protocol UIViewControllerAnimatedTransitioning : NSObjectProtocol
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
- transitionDuration 转场时长
- animateTransition 转场动画如何实现
自定义转场交互协议:
@available(iOS 7.0, *)
open class UIPercentDrivenInteractiveTransition : NSObject, UIViewControllerInteractiveTransitioning
open func update(_ percentComplete: CGFloat)
open func cancel()
open func finish()
- update 更新转场 百分比
- cancel 取消转场
- finish 完成转场
自定义转场类型枚举:
public enum UINavigationControllerOperation : Int {
case push
case pop
}
- push 压栈
- pop 出栈
自定义转场上下文:
public protocol UIViewControllerContextTransitioning : NSObjectProtocol
@available(iOS 2.0, *)
public var containerView: UIView { get }
public var transitionWasCancelled: Bool { get }
@available(iOS 2.0, *)
public func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController?
@available(iOS 2.0, *)
public func initialFrame(for vc: UIViewController) -> CGRect
@available(iOS 2.0, *)
public func finalFrame(for vc: UIViewController) -> CGRect
- containerView 包装View, 用户处理自定义转场交互的View
- transitionWasCancelled 取消转场的操作
- viewControllerforKey 拿到起始或目标控制器方法
- initialFrame 初始Frame
- finalFrame 最终Frame
讲完API, 我们就来使用这些API进行实战, 和之前一样, 我们就通过Stroyboard搭建界面:
跳过@IBOutlet 关联这步, 我们先来创建自定义转场对象:
class RevealAnimator: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning, CAAnimationDelegate { //遵循协议
let animationDuration = 2.0 //动画时长
var operation: UINavigationControllerOperation = .push //转场类型
weak var storedContext: UIViewControllerContextTransitioning? //保存转场上下文
var interactive = false //是否可交互
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return animationDuration //返回动画时长
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
storedContext = transitionContext //保存上下文
...
}
}
进行动画的自定义:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
storedContext = transitionContext
if operation == .push { //判断转场类型 为push
let fromVC = transitionContext.viewController(forKey: .from) as! MasterViewController //拿到初始控制器
let toVC = transitionContext.viewController(forKey: .to) as! DetailViewController //拿到目标控制器
transitionContext.containerView.addSubview(toVC.view) //将目标控制器的View添加到上下文的包装View中
toVC.view.frame = transitionContext.finalFrame(for: toVC) //设置目标控制器View为finalFrame
let animation = CABasicAnimation(keyPath: "transform") //进行核心动画
animation.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
animation.toValue = NSValue(caTransform3D:
CATransform3DConcat( //concat与RxSwift中相同为向后拼接, 此处为叠加3D仿射动画
CATransform3DMakeTranslation(0.0, -10.0, 0.0),
CATransform3DMakeScale(150.0, 150.0, 1.0)
)
)
animation.duration = animationDuration
animation.delegate = self
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
let maskLayer: CAShapeLayer = RWLogoLayer.logoLayer() //maskLayer为遮罩层
maskLayer.position = fromVC.logo.position
maskLayer.add(animation, forKey: nil)
toVC.view.layer.mask = maskLayer //将遮罩层Layer添加到显示层Layer的Mask属性上
fromVC.logo.add(animation, forKey: nil)
let fadeIn = CABasicAnimation(keyPath: "opacity")
fadeIn.fromValue = 0.0
fadeIn.toValue = 1.0
fadeIn.duration = animationDuration
toVC.view.layer.add(fadeIn, forKey: nil)
} 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)
},
completion: {_ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) //完成转场
}
)
}
核心动画代理:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let context = storedContext {
context.completeTransition(!context.transitionWasCancelled) //完成转场
//reset logo
let fromVC = context.viewController(forKey: .from) as! MasterViewController
fromVC.logo.removeAllAnimations() //删除所有动画
let toVC = context.viewController(forKey: .to) as! DetailViewController
toVC.view.layer.mask = nil //动画停止 清空遮罩层
}
storedContext = nil
}
pan手势转场交互:
func handlePan(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: recognizer.view!.superview!) //拿到转换坐标
var progress: CGFloat = abs(translation.x / 200.0) //拿到x轴进度
progress = min(max(progress, 0.01), 0.99) //给定进度边界
switch recognizer.state { //控制手势状态
case .changed: //改变时
update(progress) //更新进度
case .cancelled, .ended: //取消或结束时
let transitionLayer = storedContext!.containerView.layer //拿到包装Layer层
transitionLayer.beginTime = CACurrentMediaTime()
if progress < 0.5 { //控制进度
cancel() //取消转场
transitionLayer.speed = -1.0 //进行隐式动画
} else {
transitionLayer.speed = 1.0 //进行隐式动画
finish() //完成转场
}
interactive = false //交互设置为不可交互
default:
break
}
}
完成自定义转场类后, 我们进行三步走:
1) 设置代理:
class MasterViewController: UIViewController {
let logo = RWLogoLayer.logoLayer()
let transition = RevealAnimator() //自定义转场类
override func viewDidLoad() {
super.viewDidLoad()
title = "Start"
navigationController?.delegate = self //设置代理
}
}
2) 遵守协议并实现代理方法:
extension MasterViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.operation = operation
return transition
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if !transition.interactive { //当上下文不可交互时返回空
return nil
}
return transition
}
}
3) 添加手势:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let pan = UIPanGestureRecognizer(target: self, action: #selector(didPan))
view.addGestureRecognizer(pan)
// add the logo to the view
logo.position = CGPoint(x: view.layer.bounds.size.width/2,
y: view.layer.bounds.size.height/2 - 30)
logo.fillColor = UIColor.white.cgColor
view.layer.addSublayer(logo)
}
func didPan(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
transition.interactive = true //设置转场可交互
performSegue(withIdentifier: "details", sender: nil)
default:
transition.handlePan(recognizer: recognizer) //调用自定义转场类中的手势操作更新转场
}
}
演示效果:
About:
About:
🌟 源码 请点这里🌟 »> 喜欢的朋友请点喜欢 »> 下载源码的同学请送下小星星 »> 有闲钱的壕们可以进行打赏 »> 小弟会尽快推出更好的文章和大家分享 »> 你的激励就是我的动力!!