Castie!

正态分布, 优劣伴生

北冥有鱼,其名为鲲(kūn)。鲲之大,不知其几千里也;化而为鸟,其名为鹏。鹏之背,不知其几千里也;怒而飞,其翼若垂天之云。是鸟也,海运则将徙于南冥。南冥者,天池也。


北海若曰:“井鼃不可以语于海者,拘于虚也;夏虫不可以语于冰者,笃于时也;曲士不可以语于道者,束于教也。今尔出于崖涘,观于大海,乃知尔丑,尔将可与语大理矣。

Animations 自定义转场现已加入豪华午餐

经过了上周的基本动画和核心动画的理解, 我们今天来讲讲自定义转场动画, 自定义转场动画是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:

点击下方链接跳转!!

🌟 源码 请点这里🌟 »> 喜欢的朋友请点喜欢 »> 下载源码的同学请送下小星星 »> 有闲钱的壕们可以进行打赏 »> 小弟会尽快推出更好的文章和大家分享 »> 你的激励就是我的动力!!

最近的文章

Animations 快速上手 iOS10 属性动画

基础动画, 核心动画到自定义转场动画其实都不是什么新东西了, 所以我也是草草看一遍就能够读个大概, 但今天要说的UIViewPropertyAnimator, 是iOS10新的API, 其他的好处我还不太清楚, 但抽象动画逻辑和监控动画的进程上真的是方便很多.代码见:github对于属性动画来说, 真的是个新知识, 想必会用的还不多, 我也是近期才有涉及, 理解不周还望大伙指点一二, 之前我们要做一些稍微高级的动画都会使用核心动画的方法, 但是核心动画有一个致命的弱点, 就是假象和被打...…

移动开发继续阅读
更早的文章

Animations 核心动画什么的要研究透!

看过我去年文章的同学们一定知道对于动画方面还是有点心得体会的, 对于核心动画的学习, 之前我看的是iOS Core Animation的翻译版, 看完感觉真的学到了不少东西, 不过这本书已经有点时日了, 我们需要与时俱进学习最前沿的技术.代码见:github和之前的View的动画一样, 这次Layer的核心动画, 我们也是先细讲API, 再进行项目实战, 这样会比较容易理解, 新入行的同学不会望而却步.其实刚学习核心动画的时候还有有点难度的, 先提两个注意点: 1) 相对于view的c...…

移动开发继续阅读