在过渡动画期间在两个视图控制器之间共享图像
Posted
技术标签:
【中文标题】在过渡动画期间在两个视图控制器之间共享图像【英文标题】:Sharing an Image between two viewControllers during a transition animation 【发布时间】:2015-10-16 02:40:51 【问题描述】:自从 UIViewControllerAnimatedTransitioning 协议在 ios 7 中可用以来,我在 viewControllers 之间遇到了非常酷的转换。最近我注意到 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是因为当你向下滑动新视图时,你仍然可以看到它后面的原始视图,没有任何布局变化。
也许两个视图控制器实际上有产品图像(没有淡出),并且以某种方式同时以完美的精度制作动画,其中一个淡入而另一个淡出。
您认为那里实际发生了什么?
如何编写这样一个过渡动画,让它看起来像是在两个 viewController 之间共享的图像?
【问题讨论】:
我认为它只是一个视图,因为如果你向下滑动它可以关闭它,所以可以在将子视图置于前面(隐藏)并设置 alpha = 1 的同时转换图像视图 【参考方案1】:这可能是两个不同的视图和一个动画快照视图。事实上,这正是发明快照视图的原因。
这就是我在我的应用程序中的做法。当呈现的视图上下滑动时,观察红色矩形的移动:
看起来红色视图正在离开第一个视图控制器并进入第二个视图控制器,但这只是一种错觉。如果您有自定义过渡动画,则可以在过渡期间添加额外的视图。因此,我创建了一个与第一个视图相似的快照视图,隐藏了真正的第一个视图,并移动了快照视图,然后移除了快照视图并显示了真正的第二个视图。
同样的事情(这是一个很好的技巧,我在很多应用程序中都使用它):看起来标题从第一个视图控制器表视图单元格中松动并滑到第二个视图控制器中,但它是只是一个快照视图:
【讨论】:
非常酷,我想我几乎明白发生了什么,但有几个问题;快照视图是指红色矩形吗?您是否将该新视图添加到过渡 containerView 中,它是您要过渡到和从中过渡的 viewController 的父容器? “您是否正在将新视图添加到过渡 containerView”中。你可以放任何你想放的东西!它可以在演示继续时保留在那里。这也是例如实现了弹出框后面的屏幕变暗;这只是一个额外的视图。 你是如何确定动画视图的终点位置的(自动布局)?有一些大问题......【参考方案2】:为了在动画过渡期间实现视图的浮动屏幕截图(Swift 4),我们做了以下操作:
背后的想法:
-
源和目标视图控制器符合
InterViewAnimatable
协议。我们正在使用此协议来查找源视图和目标视图。
然后我们创建这些视图的快照并隐藏它们。而是在同一位置显示快照。
然后我们为快照制作动画。
在过渡结束时,我们取消隐藏目标视图。
结果看起来源视图正在飞往目的地。
文件: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://***.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://***.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动画):
【讨论】:
感谢您的详细回答。现在尝试代码,我认为您缺少更多扩展或存在错误。ImageView
应该是 UIImageView
。 UIView
没有函数 addSubviews
。以上是关于在过渡动画期间在两个视图控制器之间共享图像的主要内容,如果未能解决你的问题,请参考以下文章
如何在 UITableView 重新加载期间控制过渡动画? [复制]