Transition 退场动画,惊讶了我一下的探讨

1,785 阅读3分钟

20210716, 发现一些网上的退场动画,跑一下,一脸懵

本文探讨下,怎样解决

入场动画,很简单

  • 指定一下动画时间,通过 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval

  • 如何动画, 通过 func animateTransition(using transitionContext: UIViewControllerContextTransitioning)

左手一个 from View Controller , 上一个控制器,

右手一个 to View Controller , 下一个控制器,

让上一个消失,让下一个出现

UIView.animate 调一下动画属性,

一般就是改一下视图 frame ,完了

下面的代码,效果比较简单

一般 present 一个控制器,由下而上,

这里可以做到,从上下左右,都可以推出控制器

enum PresentingDirection{
    case top, right, left, bottom
    
    var bounds: CGRect{
        UIScreen.main.bounds
    }
    
    func offsetF(withFrame viewFrame: CGRect) -> CGRect{
        let h = bounds.size.height
        let w = bounds.size.width
        switch self {
        case .top:
            return viewFrame.offsetBy(dx: 0, dy: -h)
        case .bottom:
            return viewFrame.offsetBy(dx: 0, dy: h)
        case .left:
            return viewFrame.offsetBy(dx: -w, dy: 0)
            
        case .right:
            return viewFrame.offsetBy(dx: w, dy: 0)
            
        }
        
    }
}


class CustomPresentationController: NSObject, UIViewControllerAnimatedTransitioning{
    
    
    fileprivate var presentingDirection: PresentingDirection
    
    
    init(direction orientation: PresentingDirection) {
        presentingDirection = orientation
    }
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 1
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
        guard let fromCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
              let toCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else{
            return
        }
         
        let finalCtrlFrame = transitionContext.finalFrame(for: toCtrl)
        let containerView = transitionContext.containerView
        
        toCtrl.view.frame = presentingDirection.offsetF(withFrame: finalCtrlFrame)
        containerView.addSubview(toCtrl.view)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear) {
            fromCtrl.view.alpha = 0.5
            toCtrl.view.frame = finalCtrlFrame
        } completion: { _ in
            fromCtrl.view.alpha = 1
            transitionContext.completeTransition(true)
        }
    }
}

出场动画,有更新

接着上面的例子

1,凑合着用

熟悉的配方,同样是处理掉两个方法,

一个是动画时间,一个是怎么动画的

class CustomDismissController: NSObject, UIViewControllerAnimatedTransitioning{
    
    
    fileprivate var presentingDirection: PresentingDirection
    
    
    init(direction orientation: PresentingDirection) {
        presentingDirection = orientation
    }
    
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 1
    }
    
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
        guard let fromCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
              let toCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), let toView = toCtrl.view else{
            return
        }
        
        let finalCtrlFrame = transitionContext.finalFrame(for: fromCtrl)
        let containerView = transitionContext.containerView
        fromCtrl.view.alpha = 0.5
        toView.frame = presentingDirection.offsetF(withFrame: finalCtrlFrame)
    

        containerView.bringSubviewToFront(toView)
  
        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear) {
            toView.frame = finalCtrlFrame
       
        } completion: { _ in
            let success = !transitionContext.transitionWasCancelled
            transitionContext.completeTransition(success)
        }
    }
}

两条重点

  • fromCtrl.view.alpha = 0.5

这一句保证了,退场动画时,

from.view 不会把 toCtrl.view 遮挡的严严实实

  • containerView.bringSubviewToFront(toView)

这一句保证了,退场后,不会把 toCtrl.view 丢失

比较好的解决思路

动画是快照的事情,

左手一个 from 控制器,右手一个 to 控制器,布局一下,就好了

class CustomDismissController: NSObject, UIViewControllerAnimatedTransitioning{
   
   
   fileprivate var presentingDirection: PresentingDirection
   
   
   init(direction orientation: PresentingDirection) {
       presentingDirection = orientation
   }
   
   
   func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
       return 1
   }
   
   
   
   func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
       
       guard let fromCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
             let toCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), let toView = toCtrl.view, let snapshot = toView.snapshotView(afterScreenUpdates: true) else{
           return
       }
       
       let finalCtrlFrame = transitionContext.finalFrame(for: fromCtrl)
       let containerView = transitionContext.containerView

       snapshot.frame =
           presentingDirection.offsetF(withFrame: finalCtrlFrame)
       
       containerView.addSubview(snapshot)
       containerView.bringSubviewToFront(toView)
       toView.frame = finalCtrlFrame
       UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear) {
           snapshot.frame = finalCtrlFrame
       } completion: { _ in
           snapshot.removeFromSuperview()
           
           let success = !transitionContext.transitionWasCancelled
           transitionContext.completeTransition(success)
       }
   }
}

细节

上面代码中,如果调整下,

拿 from 控制器的快照,放在底部,

拿 to 控制器的视图,放在 from 控制器的快照的上面,去动画

发现, 放置不上去。 设置无效

而且,to 控制器的视图,从父视图 remove 掉,就找不回了

另一种思路

from 控制器的视图,就是在 to 控制器的视图的上面,

可以把 from 控制器的背景色改为透明,内容都给隐藏掉

例子

下面的退场动画,稍微复杂,

使用了关键帧动画,多加了几步


class FlipDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
 
 private let destinationFrame: CGRect
 let interactionController: SwipeInteractionController?
 
 init(destinationFrame: CGRect, interactionController: SwipeInteractionController?) {
   self.destinationFrame = destinationFrame
   self.interactionController = interactionController
 }
 
 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
   return 3
 }
 
 func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
   guard let fromVC = transitionContext.viewController(forKey: .from),
     let toVC = transitionContext.viewController(forKey: .to),
     let toView = toVC.view,
     let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false)
     else {
       return
   }
   
   snapshot.layer.cornerRadius = CardViewController.cardCornerRadius
   snapshot.layer.masksToBounds = true
   
   let containerView = transitionContext.containerView

   containerView.addSubview(snapshot)

   if let fromC = fromVC as? RevealViewController{
     fromC.imageView.isHidden = true
     fromC.view.backgroundColor = UIColor.clear
   }
   
   
   AnimationHelper.perspectiveTransform(for: containerView)
   toView.layer.transform = AnimationHelper.yRotation(-.pi / 2)
   let duration = transitionDuration(using: transitionContext)
   containerView.bringSubviewToFront(toView)
   UIView.animateKeyframes(
     withDuration: duration,
     delay: 0,
     options: .calculationModeCubic,
     animations: {
       UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1/3) {
         snapshot.frame = self.destinationFrame
       }
       
       UIView.addKeyframe(withRelativeStartTime: 1/3, relativeDuration: 1/3) {
         snapshot.layer.transform = AnimationHelper.yRotation(.pi / 2)
       }
       
       UIView.addKeyframe(withRelativeStartTime: 2/3, relativeDuration: 1/3) {
         toView.layer.transform = AnimationHelper.yRotation(0.0)
       }
   },
     completion: { _ in
      
       snapshot.removeFromSuperview()
       if transitionContext.transitionWasCancelled {
         containerView.sendSubviewToBack(toView)
       }
       transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
   })
 }
}

最后,补一下基础

转场动画结构图:

parts.001.jpg

  • 每一个控制器,都有一个属性,transitioningDelegate

通过 UIViewControllerTransitioningDelegate 协议的方法,提供对应的专场动画

  • 转场上下文 Transitioning Context

上下文,就是啥都有

可以从里面拿到左手的 from 控制器,和右手的 to 控制器,

和控制器对应的 frame

还可以直接从上下文拿 from 和 to 的视图 view,

个人感觉,用处不大

  • 动画完成了,

调用方法 completeTransition(_:) ,反馈给系统框架,动画的完成情况

github repo