在转场动画期间在两个视图控制器之间共享一张图片

10

自从IOS 7发布以来,UIViewControllerAnimatedTransitioning协议可用后,我发现了在视图控制器之间的一些很酷的过渡。最近,在Intacart的IOS应用中,我发现了一个特别有趣的过渡。

这就是我所说的缓慢动画:https://www.dropbox.com/s/p2hxj45ycq18i3l/Video%20Oct%2015%2C%207%2023%2059%20PM.mov?dl=0

一开始我以为它与作者在这篇教程中介绍的类似,只是增加了一些淡入淡出的动画效果:http://www.raywenderlich.com/113845/ios-animation-tutorial-custom-view-controller-presentation-transitions

但是,如果你仔细观察,似乎产品图像在第一个视图控制器淡出时在两个视图控制器之间转换。我认为有两个视图控制器的原因是,当你向下滑动新视图时,你仍然可以看到原始视图,在没有布局更改的情况下。也许实际上有两个视图控制器拥有产品图像(未淡出),并且以完美的精度同时进行动画,其中一个淡入,另一个淡出。
你认为那里实际上发生了什么?
如何编写这样的转换动画,使其看起来像是图像在两个视图控制器之间共享?

我认为这只是一个视图,因为如果您向下滑动该视图,可以将其关闭,因此可以在将子视图带到前面(隐藏)并设置alpha = 1的同时转换图像视图。 - Tj3n
2个回答

9

以下是我们为实现动画转场时浮动截图(Swift 4)所做的内容:

背后的想法:

  1. 源视图控制器和目标视图控制器都遵循 InterViewAnimatable 协议。我们使用此协议来查找源视图和目标视图。
  2. 然后我们创建这些视图的快照并将其隐藏。相反,快照会在同一位置显示。
  3. 接下来我们开始对快照进行动画处理。
  4. 在转场结束时,我们取消隐藏目标视图。

结果看起来就像源视图正在飞向目标视图。

文件:InterViewAnimation.swift

// TODO: In case of multiple views, add another property which will return some unique string (identifier).
protocol InterViewAnimatable {
   var targetView: UIView { get }
}

class InterViewAnimation: NSObject {

   var transitionDuration: TimeInterval = 0.25
   var isPresenting: Bool = false
}

extension InterViewAnimation: UIViewControllerAnimatedTransitioning {

   func transitionDuration(using: UIViewControllerContextTransitioning?) -> TimeInterval {
      return transitionDuration
   }

   func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

      let containerView = transitionContext.containerView

      guard
         let fromVC = transitionContext.viewController(forKey: .from),
         let toVC = transitionContext.viewController(forKey: .to) else {
            transitionContext.completeTransition(false)
            return
      }

      guard let fromTargetView = targetView(in: fromVC), let toTargetView = targetView(in: toVC) else {
         transitionContext.completeTransition(false)
         return
      }

      guard let fromImage = fromTargetView.caSnapshot(), let toImage = toTargetView.caSnapshot() else {
         transitionContext.completeTransition(false)
         return
      }

      let fromImageView = ImageView(image: fromImage)
      fromImageView.clipsToBounds = true

      let toImageView = ImageView(image: toImage)
      toImageView.clipsToBounds = true

      let startFrame = fromTargetView.frameIn(containerView)
      let endFrame = toTargetView.frameIn(containerView)

      fromImageView.frame = startFrame
      toImageView.frame = startFrame

      let cleanupClosure: () -> Void = {
         fromTargetView.isHidden = false
         toTargetView.isHidden = false
         fromImageView.removeFromSuperview()
         toImageView.removeFromSuperview()
      }

      let updateFrameClosure: () -> Void = {
         // https://dev59.com/sYbca4cB1Zd3GeqPc_2k#27997678
         // In order to have proper layout. Seems mostly needed when presenting.
         // For instance during presentation, destination view does'n account navigation bar height.
         toVC.view.setNeedsLayout()
         toVC.view.layoutIfNeeded()

         // Workaround wrong origin due ongoing layout process.
         let updatedEndFrame = toTargetView.frameIn(containerView)
         let correctedEndFrame = CGRect(origin: updatedEndFrame.origin, size: endFrame.size)
         fromImageView.frame = correctedEndFrame
         toImageView.frame = correctedEndFrame
      }

      let alimationBlock: (() -> Void)
      let completionBlock: ((Bool) -> Void)

      fromTargetView.isHidden = true
      toTargetView.isHidden = true

      if isPresenting {
         guard let toView = transitionContext.view(forKey: .to) else {
            transitionContext.completeTransition(false)
            assertionFailure()
            return
         }
         containerView.addSubviews(toView, toImageView, fromImageView)
         toView.frame = CGRect(origin: .zero, size: containerView.bounds.size)
         toView.alpha = 0
         alimationBlock = {
            toView.alpha = 1
            fromImageView.alpha = 0
            updateFrameClosure()
         }
         completionBlock = { _ in
            let success = !transitionContext.transitionWasCancelled
            if !success {
               toView.removeFromSuperview()
            }
            cleanupClosure()
            transitionContext.completeTransition(success)
         }
      } else {
         guard let fromView = transitionContext.view(forKey: .from) else {
            transitionContext.completeTransition(false)
            assertionFailure()
            return
         }
         containerView.addSubviews(toImageView, fromImageView)
         alimationBlock = {
            fromView.alpha = 0
            fromImageView.alpha = 0
            updateFrameClosure()
         }
         completionBlock = { _ in
            let success = !transitionContext.transitionWasCancelled
            if success {
               fromView.removeFromSuperview()
            }
            cleanupClosure()
            transitionContext.completeTransition(success)
         }
      }

      // TODO: Add more precise animation (i.e. Keyframe)
      if isPresenting {
         UIView.animate(withDuration: transitionDuration, delay: 0, options: .curveEaseIn,
                        animations: alimationBlock, completion: completionBlock)
      } else {
         UIView.animate(withDuration: transitionDuration, delay: 0, options: .curveEaseOut,
                        animations: alimationBlock, completion: completionBlock)
      }
   }
}

extension InterViewAnimation {

   private func targetView(in viewController: UIViewController) -> UIView? {
      if let view = (viewController as? InterViewAnimatable)?.targetView {
         return view
      }
      if let nc = viewController as? UINavigationController, let vc = nc.topViewController,
         let view = (vc as? InterViewAnimatable)?.targetView {
         return view
      }
      return nil
   }
}

使用方法:

let sampleImage = UIImage(data: try! Data(contentsOf: URL(string: "https://placekitten.com/320/240")!))

class InterViewAnimationMasterViewController: StackViewController {

   private lazy var topView = View().autolayoutView()
   private lazy var middleView = View().autolayoutView()
   private lazy var bottomView = View().autolayoutView()

   private lazy var imageView = ImageView(image: sampleImage).autolayoutView()
   private lazy var actionButton = Button().autolayoutView()

   override func setupHandlers() {
      actionButton.setTouchUpInsideHandler { [weak self] in
         let vc = InterViewAnimationDetailsViewController()
         let nc = UINavigationController(rootViewController: vc)
         nc.modalPresentationStyle = .custom
         nc.transitioningDelegate = self
         vc.handleClose = { [weak self] in
            self?.dismissAnimated()
         }
         // Workaround for not up to date laout during animated transition.
         nc.view.setNeedsLayout()
         nc.view.layoutIfNeeded()
         vc.view.setNeedsLayout()
         vc.view.layoutIfNeeded()
         self?.presentAnimated(nc)
      }
   }

   override func setupUI() {
      stackView.addArrangedSubviews(topView, middleView, bottomView)
      stackView.distribution = .fillEqually

      bottomView.addSubviews(imageView, actionButton)

      topView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
      middleView.backgroundColor = UIColor.green.withAlphaComponent(0.5)

      bottomView.layoutMargins = UIEdgeInsets(horizontal: 15, vertical: 15)
      bottomView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)

      actionButton.title = "Tap to perform Transition!"
      actionButton.contentEdgeInsets = UIEdgeInsets(dimension: 8)
      actionButton.backgroundColor = .magenta

      imageView.layer.borderWidth = 2
      imageView.layer.borderColor = UIColor.magenta.withAlphaComponent(0.5).cgColor
   }

   override func setupLayout() {
      var constraints = LayoutConstraint.PinInSuperView.atCenter(imageView)
      constraints += [
         LayoutConstraint.centerX(viewA: bottomView, viewB: actionButton),
         LayoutConstraint.pinBottoms(viewA: bottomView, viewB: actionButton)
      ]
      let size = sampleImage?.size.scale(by: 0.5) ?? CGSize()
      constraints += LayoutConstraint.constrainSize(view: imageView, size: size)
      NSLayoutConstraint.activate(constraints)
   }
}

extension InterViewAnimationMasterViewController: InterViewAnimatable {
   var targetView: UIView {
      return imageView
   }
}

extension InterViewAnimationMasterViewController: UIViewControllerTransitioningDelegate {

   func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
      let animator = InterViewAnimation()
      animator.isPresenting = true
      return animator
   }

   func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
      let animator = InterViewAnimation()
      animator.isPresenting = false
      return animator
   }
}

class InterViewAnimationDetailsViewController: StackViewController {

   var handleClose: (() -> Void)?

   private lazy var imageView = ImageView(image: sampleImage).autolayoutView()
   private lazy var bottomView = View().autolayoutView()

   override func setupUI() {
      stackView.addArrangedSubviews(imageView, bottomView)
      stackView.distribution = .fillEqually

      imageView.contentMode = .scaleAspectFit
      imageView.layer.borderWidth = 2
      imageView.layer.borderColor = UIColor.purple.withAlphaComponent(0.5).cgColor

      bottomView.backgroundColor = UIColor.blue.withAlphaComponent(0.5)

      navigationItem.leftBarButtonItem = BarButtonItem(barButtonSystemItem: .done) { [weak self] in
         self?.handleClose?()
      }
   }
}

extension InterViewAnimationDetailsViewController: InterViewAnimatable {
   var targetView: UIView {
      return imageView
   }
}

可重复使用的扩展:

extension UIView {

   // https://medium.com/@joesusnick/a-uiview-extension-that-will-teach-you-an-important-lesson-about-frames-cefe1e4beb0b
   public func frameIn(_ view: UIView?) -> CGRect {
      if let superview = superview {
         return superview.convert(frame, to: view)
      }
      return frame
   }
}


extension UIView {

   /// The method drawViewHierarchyInRect:afterScreenUpdates: performs its operations on the GPU as much as possible
   /// In comparison, the method renderInContext: performs its operations inside of your app’s address space and does
   /// not use the GPU based process for performing the work.
   /// https://dev59.com/V2Ag5IYBdhLWcg3w0Noz#25704861
   public func caSnapshot(scale: CGFloat = 0, isOpaque: Bool = false) -> UIImage? {
      var isSuccess = false
      UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, scale)
      if let context = UIGraphicsGetCurrentContext() {
         layer.render(in: context)
         isSuccess = true
      }
      let image = UIGraphicsGetImageFromCurrentImageContext()
      UIGraphicsEndImageContext()
      return isSuccess ? image : nil
   }
}

结果(gif动画):

Gif动画


这段内容展示了一个gif动画的结果。

感谢您详细的回答。我现在正在尝试代码,我认为您可能缺少了一些扩展或者存在错误。ImageView 应该是 UIImageViewUIView 没有 addSubviews 这个函数。 - Bryan Bryce

8
"我猜测这可能是两个不同的视图和一个动画快照视图。事实上,这正是为什么发明了快照视图的原因。这就是我在我的应用程序中所做的。观察红色矩形的移动,当呈现的视图向上或向下滑动时:"

enter image description here

看起来红色视图正在离开第一个视图控制器并进入第二个视图控制器,但这只是一种幻觉。如果您有自定义的过渡动画,可以在过渡期间添加额外的视图。因此,我创建了一个快照视图,它看起来就像第一个视图,隐藏了真正的第一个视图,并移动了快照视图,然后删除快照视图并显示真正的第二个视图。

同样的事情也发生在这里(这是我在许多应用程序中使用的好技巧):看起来标题已经从第一个视图控制器表格视图单元格中脱落并滑动到第二个视图控制器中,但实际上这只是一个快照视图:

enter image description here


非常酷,我觉得我差不多明白发生了什么,但还有几个问题;快照视图是指红色矩形吗?你是将该新视图添加到转换容器视图中,该容器视图是你正在过渡到和过渡的视图控制器的父容器吗? - s_curry_s
“你将新的视图添加到转换容器视图中了吗?” “没错。” 你可以在其中放任何你想要的东西!并且在演示过程中它可以保持存在。这也是如何实现例如弹出窗口后面屏幕变暗的;它只是一个额外的视图。 - matt
你是如何确定动画视图的终点位置(使用自动布局)的?我在这方面遇到了一些主要问题... - BananaBuisness

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接