iOS自定义转场动画

5
我开始学习使用UIViewControllerAnimatedTransitioning协议进行自定义转场动画。我在youtube上找到的大多数视频都是基于当我们使用圆形动画或类似动画呈现新的ViewController时的流程。
我在实现转换方式方面遇到了问题。大多数情况下,我需要的是类似于Facebook应用程序并且如何打开全屏图像查看器。
所以,假设我们有VC1VC2。在VC1上,我们调用操作以呈现VC2。在两个VC上,我们都有相同的UI元素。在我的情况下,那就是UIImageView。就像您在VC1上单击imageView一样,它会打开某个对象的详细页面,并在顶部显示其图像。我希望有一个动画,看起来像是从VC1的图像正在更改为VC2的图像的最终框架,然后其他内容(如标签,按钮等)将出现在详细页面上。
但是在培训期间,我遇到了一些问题。 1.首先,我不理解transitionContextcontainerView的想法。但是我看到,它就像是在转换之间的中间状态视图。这正确吗?但是对我来说,这很奇怪,因为即使backgroundColor属性也不适用于containerView。 2.我不明白我需要在过渡期间动画化什么,以及containerView子视图的结构应该是什么样的。在我的示例中,当呈现VC2时,我需要,据我所知,隐藏所有其子视图。然后将来自VC1的imageView动画到VC2imageView的框架中,然后再次显示所有子视图。那么,在这种情况下,imageView应该添加到containerView中吗?如果是这样,那么它应该是来自VC1的实际imageView,还是完全新的imageView,具有相同的框架/图像,只是在过渡期间临时使用...
以下是类似动画的示例/教程/代码的链接: 这里是Facebook中如何工作的链接
2个回答

11

了解自定义转场动画

如果您从 VCA 导航到 VCB,则需要执行以下操作:

  1. 首先,您需要使用 UIViewControllerTransitioningDelegate

转场代理负责提供要用于自定义转换的动画控制器。指定的委托对象必须符合 UIViewControllerTransitioningDelegate 协议。

  1. 现在,您需要使用 UIViewControllerAnimatedTransitioning

它负责过渡,包括视图动画的持续时间实际逻辑

这些代理就像您处于两个 VC 之间并与它们交互一样。

为了使完整的转换成功,必须执行以下步骤:

  1. 首先,您需要使用以下方法:

    • 设置 modalPresentationStyle = .custom
    • 分配 transitionDelegate 属性。
  2. func animateTransition(_ : ) 中,您需要使用上下文 containerView,因为您处于两个 VC 之间,所以您需要任何容器来进行任何动画,因此上下文为您提供了可以执行动画的容器。

  3. 现在,您需要 fromViewtoView ,即 VCA.viewVCB.view。现在将这两个视图添加到 containerView 中并编写动画的核心逻辑。

  4. 最后一个重要事项是在转场上下文对象上调用 completeTransition(_ :) 方法。一旦您的动画完成,必须调用此方法以让系统知道您的视图控制器已完成转换。

这是转换动画的核心基础。

我不知道 FB 动画,所以我只解释了您问题的其余部分。

参考资料

如果需要进一步的信息,请问。

代码添加

在图像选择中添加到 VC_A

var selectedImage: UIImageView?
 let transition = PopAnimator()

  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        coordinator.animate(
          alongsideTransition: {context in
            self.bgImage.alpha = (size.width>size.height) ? 0.25 : 0.55
            self.positionListItems()
          },
          completion: nil
        )
      }
//position all images inside the list
  func positionListItems() {
    let listHeight = listView.frame.height
    let itemHeight: CGFloat = listHeight * 1.33
    let aspectRatio = UIScreen.main.bounds.height / UIScreen.main.bounds.width
    let itemWidth: CGFloat = itemHeight / aspectRatio

    let horizontalPadding: CGFloat = 10.0

    for i in herbs.indices {
      let imageView = listView.viewWithTag(i) as! UIImageView
      imageView.frame = CGRect(
        x: CGFloat(i) * itemWidth + CGFloat(i+1) * horizontalPadding, y: 0.0,
        width: itemWidth, height: itemHeight)
    }

    listView.contentSize = CGSize(
      width: CGFloat(herbs.count) * (itemWidth + horizontalPadding) + horizontalPadding,
      height:  0)
  }

// On image selection
VC_B.transitioningDelegate = self
    present(VC_B, animated: true, completion: nil)



   // add extension
extension VC_A: UIViewControllerTransitioningDelegate {

  func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    transition.originFrame = selectedImage!.superview!.convert(selectedImage!.frame, to: nil)

    transition.presenting = true
    selectedImage!.isHidden = true

    return transition
  }

  func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    transition.presenting = false
    return transition
  }
}

动画类

class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {

  let duration = 1.0
  var presenting = true
  var originFrame = CGRect.zero

  var dismissCompletion: (()->Void)?

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

  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    let containerView = transitionContext.containerView

    let toView = transitionContext.view(forKey: .to)!

    let herbView = presenting ? toView : transitionContext.view(forKey: .from)!

    let initialFrame = presenting ? originFrame : herbView.frame
    let finalFrame = presenting ? herbView.frame : originFrame

    let xScaleFactor = presenting ?

      initialFrame.width / finalFrame.width :
      finalFrame.width / initialFrame.width

    let yScaleFactor = presenting ?

      initialFrame.height / finalFrame.height :
      finalFrame.height / initialFrame.height

    let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)

    if presenting {
      herbView.transform = scaleTransform
      herbView.center = CGPoint(
        x: initialFrame.midX,
        y: initialFrame.midY)
      herbView.clipsToBounds = true
    }

    containerView.addSubview(toView)
    containerView.bringSubview(toFront: herbView)

    UIView.animate(withDuration: duration, delay:0.0, usingSpringWithDamping: 0.4,
      initialSpringVelocity: 0.0,
      animations: {
        herbView.transform = self.presenting ?
          CGAffineTransform.identity : scaleTransform
        herbView.center = CGPoint(x: finalFrame.midX,
                                  y: finalFrame.midY)
      },
      completion:{_ in
        if !self.presenting {
          self.dismissCompletion?()
        }
        transitionContext.completeTransition(true)
      }
    )
  }

}

输出:

输入图像描述

Github代码库:https://github.com/thedahiyaboy/TDCustomTransitions

  • xcode: 9.2

  • swift: 4


你好。感谢回复。我已经添加了一个链接,展示在Facebook上的效果。或许这会帮助你理解我想要实现的目标。我不关心“磁铁”效应之类的东西。我只是想要相同的动画效果,这样看起来全屏画廊中的imageView就像是列表中的同一UIImageView。 - Stas Ivanov
谢谢。那么,我理解的意思是,在呈现动作的情况下,立即将“toView”添加到“containerView”中并进行一些UI更改,然后将其动画到最终状态,并完成转换。对吗? - Stas Ivanov
如果是这样,我有一个关于最佳实践的问题...访问、将视图更改为其初始状态并将其动画化到最终状态的最佳方法是什么?例如,我有一个VC_B,它有许多字段/子视图,不仅仅是全屏imageView,但我想要一个动画,它将只显示图像(缩放)初始时,然后显示所有其他字段。这需要在实际动画之前隐藏所有对象,将图像动画到其最终框架,然后再次迭代所有对象以显示它们。对吗? - Stas Ivanov
据我理解,首先您想在 VC_B 上显示图像。当图像显示后,您希望通过动画显示其他一些组件(如果我理解有误,请发送您的描述链接)。为此,首先使用 animateTransition 来动画化 VC。然后在 VC_BViewDidAppear 中根据您的逻辑动画化其他组件。注意: transitionAnimate 用于动画化 VC,而不是组件。 - dahiya_boy
如果我们不使用模态呈现,而是像添加addChildViewController和addSubview一样添加视图控制器,转换动画会应用吗? - Iraniya Naynesh
@IraniyaNaynesh 转场动画仅在您呈现任何VC并推送您的VC时使用。但在您的情况下,您将子VC视图添加为子视图。在这里,没有发生转换,因此它不起作用。要进行转换,您必须在将childView添加为subView后使用简单的UIview动画。希望现在您已经清楚了。 - dahiya_boy

2
UIViewControllerAnimatedTransitioning 的核心方法是 animateTransition。我在这里添加了注释,试图解释基本思想。
let duration = 0.5
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    // Works like an empty scratchpad/slate.
    // This is the view that will be shown on screen when animation starts and upto 
    // it ends.
    // Any animations done here are visible to user.
    // Nothing right now, in this container(*).
    let containerView = transitionContext.containerView

    // Grab the controller to animate from and to.
    let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!.view
    let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!.view

    // We might need some other view's that need to be animated, like the UIImageView
    // In  your case this image view must exists on fromView and also in toView
    let fromImageView = UIImageView() // We should get image from fromController
    let toImageView = UIImageView() // We should get image from toController

    // Since the containerView has no views as of now(*), we need to add our fromView first
    containerView.addSubview(fromView!)

    // We will also add, the to view but with alpha 0 so that is not visible initially
    toView?.alpha = 0.0
    // Add this to view to container
    containerView.addSubview(toView!)


    UIView.animate(withDuration: duration, animations: { 
        // We do animations here, something like,
        fromImageView.frame = (toView?.frame)! // With some checking around the view relative frames
        toView?.alpha = 1.0
    }) { (completed) in
        // Do clean up here, after this completeTransition(true) method,
        // the comtainer will be removed from the screen and toView will be shown automatically
        transitionContext.completeTransition(true)
    }
}

你好。感谢回复。方法中间的“ImageView”部分,其实就是我想知道的。但问题在于,当使用解散动画时,访问viewController(forKey:)时会得到nil。因此,在解散时我将无法创建相反的动画。 - Stas Ivanov
@StasIvanov 对于 fromviewcontroller 键是 nil 吗? - BangOperator
是的,曾经我在尝试为dismiss创建自定义转场时,出现了viewController(forKey: .from)为空的情况,而viewForKey...正常工作。但是我会再次检查... - Stas Ivanov
糟糕,我的错。对不起 :) 功能正常。 - Stas Ivanov

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