在 UINavigationController 中使用 UICollectionView 缩放动画

Posted

技术标签:

【中文标题】在 UINavigationController 中使用 UICollectionView 缩放动画【英文标题】:Zoom animation with a UICollectionView inside a UINavigationController 【发布时间】:2020-03-21 14:36:21 【问题描述】:

我正在尝试制作从 UICollectionViewController 到 UIViewController 的过渡动画。

TLDR:在最后一个函数中,如何获得将导航栏考虑在内的框架?

每个单元格都有一个 UIImageView,我想在转换到视图控制器时放大它。这与照片应用程序类似,只是图像不居中并且周围有其他视图(标签等)。

UIImageView 使用 AutoLayout 放置,水平居中,距离顶部安全布局指南 89pts。

它几乎可以工作,除了我无法为目标 UIImageView 获得正确的框架,它没有考虑导航,因此框架 y 原点偏离了那么多。

我的UICollectionViewControllerUINavigationControllerDelegate

class CollectionViewController: UINavigationControllerDelegate 
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) 
        guard segue.identifier == detailSegue,
            let destination = segue.destination as? CustomViewController,
            let indexPath = sender as? IndexPath else  return 
        navigationController?.delegate = self
        …
    

    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? 
        switch operation 
        case .push:
            return ZoomInAnimator()
        default:
            return nil
        
    

ZoomInAnimator 对象如下所示:

open class ZoomInAnimator: NSObject, UIViewControllerAnimatedTransitioning 
    public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval 
        return 0.5
    

public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) 
        guard let fromViewController = transitionContext.viewController(forKey: .from),
            let fromContent = (fromViewController  as? ZoomAnimatorDelegate)?.animatedContent(),
            let toViewController = transitionContext.viewController(forKey: .to),
            let toContent = (toViewController as? ZoomAnimatorDelegate)?.animatedContent() else  return 

        let finalFrame = toContent.pictureFrame!

        // Set pre-animation state
        toViewController.view.alpha = 0
        toContent.picture.isHidden = true

        // Add a new UIImageView where the "from" picture currently is
        let transitionImageView = UIImageView(frame: fromContent.pictureFrame)
        transitionImageView.image = fromContent.picture.image
        transitionImageView.contentMode = fromContent.picture.contentMode
        transitionImageView.clipsToBounds = fromContent.picture.clipsToBounds

        transitionContext.containerView.addSubview(toViewController.view)
        transitionContext.containerView.addSubview(transitionImageView)

        // Animate the transition
        UIView.animate(
            withDuration: transitionDuration(using: transitionContext),
            delay: 0,
            usingSpringWithDamping: 0.8,
            initialSpringVelocity: 0,
            options: [.transitionCrossDissolve],
            animations: 
                transitionImageView.frame = finalFrame
                toViewController.view.alpha = 1
        )  completed in
            transitionImageView.removeFromSuperview()
            toContent.picture.isHidden = false
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        
    

如您所见,我使用animatedContent() 向两个控制器询问他们的 UIImageView 和框架。这个函数在 UICollectionViewController 和 UIViewController 中都有定义,都符合这个协议:

protocol ZoomAnimatorDelegate 
    func animatedContent() -> AnimatedContent?

AnimatedContent 是一个简单的结构:

struct AnimatedContent 
    let picture: UIImageView!
    let pictureFrame: CGRect!

以下是该功能在双方的实现方式。

这一面工作正常(转换中的 UICollectionViewController / fromViewController):

extension CollectionViewController: ZoomAnimatorDelegate 

    func animatedContent() -> AnimatedContent? 
        guard let indexPath = collectionView.indexPathsForSelectedItems?.first else  return nil 
        let cell = cellOrPlaceholder(for: indexPath) // Custom method to get the cell, I'll skip the details
        let frame = cell.convert(cell.picture.frame, to: view)
        return AnimatedContent(picture: cell.picture, pictureFrame: frame)
    

这是有问题的部分。

相框原点为 89pts 并忽略添加 140pt 偏移的导航栏。我尝试了各种 convert(to:) 都没有成功。另外,我有点担心在这里打电话给view.layoutIfNeeded,这样可以吗?

class ViewController: ZoomAnimatorDelegate 
    @IBOutlet weak var picture: UIImageView!
    func animatedContent() -> AnimatedContent? 
        view.layoutIfNeeded()
        return AnimatedContent(picture: picture, profilePictureFrame: picture.frame)
    

编辑:这是animateTransition 的更新版本(尚未使用快照,但我会到达那里)

public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) 
    guard let fromViewController = transitionContext.viewController(forKey: .from) as? CollectionViewController,
        let fromContent = fromViewController.animatedContent(),
        let toViewController = transitionContext.viewController(forKey: .to) as? ViewController,
        let toImageView = toViewController.picture else  return 

    transitionContext.containerView.addSubview(toViewController.view)
    toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
    toViewController.view.setNeedsLayout()
    toViewController.view.layoutIfNeeded()
    let finalFrame = toViewController.view.convert(toImageView.frame, to: transitionContext.containerView)

    let transitionImageView = UIImageView(frame: fromContent.pictureFrame)
    transitionImageView.image = fromContent.picture.image
    transitionImageView.contentMode = fromContent.picture.contentMode
    transitionImageView.clipsToBounds = fromContent.picture.clipsToBounds
    transitionContext.containerView.addSubview(transitionImageView)

    // Set pre-animation state
    toViewController.view.alpha = 0
    toImageView.isHidden = true

    UIView.animate(
        withDuration: transitionDuration(using: transitionContext),
        delay: 0,
        usingSpringWithDamping: 0.8,
        initialSpringVelocity: 0,
        options: [.transitionCrossDissolve],
        animations: 
            transitionImageView.frame = finalFrame
            toViewController.view.alpha = 1
    )  completed in
        transitionImageView.removeFromSuperview()
        toImageView.isHidden = false
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
    

【问题讨论】:

【参考方案1】:

问题是您错误地获取了最终帧:

let finalFrame = toContent.pictureFrame!

这是你应该做的:

    首先通过调用finalFrame(for:) 来询问to 视图控制器视图的最后一帧的转换上下文。您从不调用该方法这一事实是一个明显的错误。你应该总是调用它!

    现在将其用作动画的帧(如果您知道图像将完全填充它),或者将其用作计算该帧的基础。

    在后一种情况下,我所做的是执行一个虚拟布局操作以了解图像帧将在视图控制器的视图中是什么,然后将坐标系转换转换上下文的坐标系.我没有看到您执行任何坐标系转换这一事实表明您的代码中存在另一个错误。请记住,frame 是根据视图的父视图坐标系计算的。

这是我的一个应用程序中的一个示例;这是一个提交的 VC 不是推送的 VC,但这对计算没有影响:

观察我是如何知道红色样本在呈现的视图控制器视图的最后一帧中的位置,然后我根据过渡上下文的坐标系将红色样本快照移动到该帧。

另外请注意,我使用快照视图作为代理。您正在使用松散的图像视图作为代理,这也可能没问题。

【讨论】:

所以使用 1/ 我将 layoutIfNeeded 移到了 animateTransition 中,但我仍然在为 2/ 苦苦挣扎。我基本上称之为 toViewController.view.convert(toViewController.picture.frame, to: transitionContext.containerView) 但它仍然很高(不考虑导航栏)。 好吧,你已经以我不理解的方式获得了所有这些协议“委托”的东西。那里有一个真实的视图控制器试图进入图片,它有一个真实的视图,该视图进行某种布局并将在某个位置显示图像,您需要找出那个位置将是。我的意思是,我已经告诉过你 做了什么,并且我已经向你展示了它是有效的。我看不出还有什么。 它只是用来获取两边的UIImageView(当前选中单元格中的一个和“to”视图控制器中的一个)。是否有意义?我是从github.com/masamichiueta/FluidPhoto 那里得到的,但看起来这不是如何做事的好例子。 我编辑了我的问题以添加我当前版本的 animateTransition。它不再将协议用于目标视图。 我解决了。我没有在正确的视图上调用 convert,它必须是 imageview 的超级视图。

以上是关于在 UINavigationController 中使用 UICollectionView 缩放动画的主要内容,如果未能解决你的问题,请参考以下文章

在 UINavigationController 内的 UITabBarcontroller 中添加 UINavigationController?

从嵌入在 UINavigationController 中的一个视图控制器到另一个 UINavigationController

(Swift) 在嵌套在 Main UINavigationController 中的 UINavigationController 和 UITabController 之间切换

关闭 UINavigationController 并呈现另一个 UINavigationController

带有主 UINavigationController 和详细 UINavigationController 的 UISplitViewcontroller

UINavigationController 标题和按钮