草庐IT

ios - 在过渡动画期间在两个 viewController 之间共享图像

coder 2023-09-25 原文

自从 UIViewControllerAnimatedTransitioning 协议(protocol)在 IOS 7 中可用以来,我在 viewController 之间遇到了非常酷的转换。最近我在 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

但是,如果您仔细观察,就会发现随着第一个 viewController 的淡出,产品图像似乎在两个 viewController 之间转换。我认为有两个 viewController 的原因是因为当你向下滑动新 View 时,你仍然可以看到它后面的原始 View ,没有布局变化。

也许两个 viewControllers 实际上有产品图像(没有淡出)并且以某种方式同时以完美的精度设置动画并且其中一个淡入而另一个淡出。

您认为那里实际发生了什么?

如何编写这样一个看起来像在两个 viewController 之间共享图像的过渡动画?

最佳答案

为了在动画转换期间实现 View 的 float 屏幕截图(Swift 4),我们做了以下操作:

背后的想法:

  1. 源和目标 View Controller 符合 InterViewAnimatable 协议(protocol)。我们正在使用此协议(protocol)来查找源 View 和目标 View 。
  2. 然后我们创建这些 View 的快照并隐藏它们。相反,在同一位置显示快照。
  3. 然后我们制作动画快照。
  4. 在转换结束时,我们取消隐藏目标 View 。

结果看起来源 View 正在飞向目的地。

文件: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://stackoverflow.com/a/27997678/1418981
         // 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://stackoverflow.com/a/25704861/1418981
   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动画):

关于ios - 在过渡动画期间在两个 viewController 之间共享图像,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33161692/

有关ios - 在过渡动画期间在两个 viewController 之间共享图像的更多相关文章

  1. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  2. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  3. ruby - 通过 ruby​​ 进程共享变量 - 2

    我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

  4. ruby - #之间? Cooper 的 *Beginning Ruby* 中的错误或异常 - 2

    在Cooper的书BeginningRuby中,第166页有一个我无法重现的示例。classSongincludeComparableattr_accessor:lengthdef(other)@lengthother.lengthenddefinitialize(song_name,length)@song_name=song_name@length=lengthendenda=Song.new('Rockaroundtheclock',143)b=Song.new('BohemianRhapsody',544)c=Song.new('MinuteWaltz',60)a.betwee

  5. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  6. ruby-on-rails - `a ||= b` 和 `a = b if a.nil 之间的区别? - 2

    我正在检查一个Rails项目。在ERubyHTML模板页面上,我看到了这样几行:我不明白为什么不这样写:在这种情况下,||=和ifnil?有什么区别? 最佳答案 在这种特殊情况下没有区别,但可能是出于习惯。每当我看到nil?被使用时,它几乎总是使用不当。在Ruby中,很少有东西在逻辑上是假的,只有文字false和nil是。这意味着像if(!x.nil?)这样的代码几乎总是更好地表示为if(x)除非期望x可能是文字false。我会将其切换为||=false,因为它具有相同的结果,但这在很大程度上取决于偏好。唯一的缺点是赋值会在每次运行

  7. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  8. ruby - 这两个 Ruby 类初始化定义有什么区别? - 2

    我正在阅读一本关于Ruby的书,作者在编写类初始化定义时使用的形式与他在本书前几节中使用的形式略有不同。它看起来像这样:classTicketattr_accessor:venue,:datedefinitialize(venue,date)self.venue=venueself.date=dateendend在本书的前几节中,它的定义如下:classTicketattr_accessor:venue,:datedefinitialize(venue,date)@venue=venue@date=dateendend在第一个示例中使用setter方法与在第二个示例中使用实例变量之间是

  9. ruby-on-rails - 添加回形针新样式不影响旧上传的图像 - 2

    我有带有Logo图像的公司模型has_attached_file:logo我用他们的Logo创建了许多公司。现在,我需要添加新样式has_attached_file:logo,:styles=>{:small=>"30x15>",:medium=>"155x85>"}我是否应该重新上传所有旧数据以重新生成新样式?我不这么认为……或者有什么rake任务可以重新生成样式吗? 最佳答案 参见Thumbnail-Generation.如果rake任务不适合你,你应该能够在控制台中使用一个片段来调用重新处理!关于相关公司

  10. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

随机推荐