视图控制器转场动画子视图位置。

6

我正在尝试创建两个视图控制器之间的简单转换动画,这两个视图控制器都有相同的标签。我只想将标签从第一个视图控制器中的位置动画到第二个视图控制器中的位置(请参见下面的示例)。

View Controller Illustration

我已经设置了我的视图控制器来使用自定义动画控制器,通过一个输出口,我可以访问两个视图控制器和标签。在动画块中,我只需将第一个视图控制器上的标签框架设置为第二个视图控制器上的标签框架即可。
[UIView animateWithDuration:self.duration animations:^{
    fromViewController.label.frame = toViewController.titleLabel.frame;
} completion:^(BOOL finished) {
    [transitionContext completeTransition:finished];
}];
fromViewController.label.frame: {{115.5, 313}, {144, 41}}
toViewController.titleLabel.frame: {{16, 12}, {144, 41}}

我不知道为什么我的动画没有达到预期效果,取而代之的是发生了什么。

如果您有任何建议可以改变我的动画以使其正确运行,并解释我为什么会看到这种情况,我将非常感激。


@matt 感谢您的建议,但是那个答案似乎并没有回答我的问题。再次强调,我想使用自定义视图控制器转换动画来将一个视图控制器的子视图动画到第二个视图控制器的子视图所定义的位置。这应该是一个微不足道的任务,但出于某种原因,我无法使其正常工作,也不明白为什么会这样。 - Aleksander
@matt 如果我正确理解了你对另一个问题的回答,你创建了 UIView 的快照以移动它,将其位置设为原始 UIView 的位置,然后将其动画化到目标位置。虽然我同意这种逻辑,但我更喜欢不创建快照。相反,我想将第一个视图控制器中的原始 UILabel 移动到由第二个视图控制器中相应标签的位置所决定的位置。 - Aleksander
@matt 继续跟进,是的,创建快照可以解决问题。我想知道如果我正确修改标签为什么它不起作用,以及我能做些什么来使其工作。 - Aleksander
2个回答

39

您提到了子视图的动画,但没有谈论整体动画。我倾向于使用容器视图来进行动画,以避免在同时对子视图和主视图进行动画时出现任何潜在的混淆/问题。我的做法如下:

  1. 制作“from”视图中子视图位置的快照,然后隐藏子视图;
  2. 制作“to”视图中子视图位置的快照,然后隐藏子视图;
  3. 将所有这些frame值转换为容器的坐标空间,并将所有这些快照添加到容器视图中;
  4. 将“to”快照的alpha从零开始(以便它们淡入);
  5. 将“to”快照的更改同时动画化到其最终目的地,将它们的alpha更改回1
  6. 同时将“from”快照动画到“to”视图最终目的地的位置,并将它们的alpha动画到零(这样它们就会淡出,在与第4点相结合时产生一种交叉溶解效果)。
  7. 完成后,删除快照并取消隐藏其快照被动画化的子视图。

其效果是标签从一个位置滑动到另一个位置,如果初始和最终内容不同,那么在它们移动时会产生一种交叉溶解效果。

例如:

enter image description here

通过使用容器视图来进行快照的动画,它独立于您可能正在对目标场景的主视图进行的任何动画。在这种情况下,我将其从右侧滑入,但您可以按照自己的喜好进行操作。

或者,您可以对多个子视图进行此操作:

enter image description here

(个人认为,如果情况是这样的,即几乎所有内容都在滑动,那么我会失去主视图的滑动动画,因为它现在变得令人分心,但它给了您基本的想法。同时,在我的退出动画中,我交换了要传递给另一个视图的视图,您永远不会这样做,但我只是想说明它的灵活性和淡入淡出效果。)

要呈现上述内容,我在Swift 4中使用了以下内容:

protocol CustomTransitionOriginator {
    var fromAnimatedSubviews: [UIView] { get }
}

protocol CustomTransitionDestination {
    var toAnimatedSubviews: [UIView] { get }
}

class Animator: NSObject, UIViewControllerAnimatedTransitioning {
    enum TransitionType {
        case present
        case dismiss
    }

    let type: TransitionType

    init(type: TransitionType) {
        self.type = type
        super.init()
    }

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

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let fromVC = transitionContext.viewController(forKey: .from) as! CustomTransitionOriginator  & UIViewController
        let toVC   = transitionContext.viewController(forKey: .to)   as! CustomTransitionDestination & UIViewController

        let container = transitionContext.containerView

        // add the "to" view to the hierarchy

        toVC.view.frame = fromVC.view.frame
        if type == .present {
            container.addSubview(toVC.view)
        } else {
            container.insertSubview(toVC.view, belowSubview: fromVC.view)
        }
        toVC.view.layoutIfNeeded()

        // create snapshots of label being animated

        let fromSnapshots = fromVC.fromAnimatedSubviews.map { subview -> UIView in
            // create snapshot

            let snapshot = subview.snapshotView(afterScreenUpdates: false)!

            // we're putting it in container, so convert original frame into container's coordinate space

            snapshot.frame = container.convert(subview.frame, from: subview.superview)

            return snapshot
        }

        let toSnapshots = toVC.toAnimatedSubviews.map { subview -> UIView in
            // create snapshot

            let snapshot = subview.snapshotView(afterScreenUpdates: true)!// UIImageView(image: subview.snapshot())

            // we're putting it in container, so convert original frame into container's coordinate space

            snapshot.frame = container.convert(subview.frame, from: subview.superview)

            return snapshot
        }

        // save the "to" and "from" frames

        let frames = zip(fromSnapshots, toSnapshots).map { ($0.frame, $1.frame) }

        // move the "to" snapshots to where where the "from" views were, but hide them for now

        zip(toSnapshots, frames).forEach { snapshot, frame in
            snapshot.frame = frame.0
            snapshot.alpha = 0
            container.addSubview(snapshot)
        }

        // add "from" snapshots, too, but hide the subviews that we just snapshotted
        // associated labels so we only see animated snapshots; we'll unhide these
        // original views when the animation is done.

        fromSnapshots.forEach { container.addSubview($0) }
        fromVC.fromAnimatedSubviews.forEach { $0.alpha = 0 }
        toVC.toAnimatedSubviews.forEach { $0.alpha = 0 }

        // I'm going to push the the main view from the right and dim the "from" view a bit,
        // but you'll obviously do whatever you want for the main view, if anything

        if type == .present {
            toVC.view.transform = .init(translationX: toVC.view.frame.width, y: 0)
        } else {
            toVC.view.alpha = 0.5
        }

        // do the animation

        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            // animate the snapshots of the label

            zip(toSnapshots, frames).forEach { snapshot, frame in
                snapshot.frame = frame.1
                snapshot.alpha = 1
            }

            zip(fromSnapshots, frames).forEach { snapshot, frame in
                snapshot.frame = frame.1
                snapshot.alpha = 0
            }

            // I'm now animating the "to" view into place, but you'd do whatever you want here

            if self.type == .present {
                toVC.view.transform = .identity
                fromVC.view.alpha = 0.5
            } else {
                fromVC.view.transform = .init(translationX: fromVC.view.frame.width, y: 0)
                toVC.view.alpha = 1
            }
        }, completion: { _ in
            // get rid of snapshots and re-show the original labels

            fromSnapshots.forEach { $0.removeFromSuperview() }
            toSnapshots.forEach   { $0.removeFromSuperview() }
            fromVC.fromAnimatedSubviews.forEach { $0.alpha = 1 }
            toVC.toAnimatedSubviews.forEach { $0.alpha = 1 }

            // clean up "to" and "from" views as necessary, in my case, just restore "from" view's alpha

            fromVC.view.alpha = 1
            fromVC.view.transform = .identity

            // complete the transition

            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }
}

// My `UIViewControllerTransitioningDelegate` will specify this presentation 
// controller, which will clean out the "from" view from the hierarchy when
// the animation is done.

class PresentationController: UIPresentationController {
    override var shouldRemovePresentersView: Bool { return true }
}

那么,为了使上述所有内容正常运行,如果我从ViewController转换到SecondViewController,我需要指定我正在从哪些子视图移动以及我正在移动到哪些子视图:

extension ViewController: CustomTransitionOriginator {
    var fromAnimatedSubviews: [UIView] { return [label] }
}

extension SecondViewController: CustomTransitionDestination {
    var toAnimatedSubviews: [UIView] { return [label] }
}

同时为了支持dismiss操作,我会添加相应的协议遵循:

extension ViewController: CustomTransitionDestination {
    var toAnimatedSubviews: [UIView] { return [label] }
}

extension SecondViewController: CustomTransitionOriginator {
    var fromAnimatedSubviews: [UIView] { return [label] }
}

现在,我不希望你被所有这些代码迷惑了,所以我建议你着重关注高层设计(我在顶部列举的前七个要点)。但愿这足以让你理解基本思路。


非常感谢你详细的解释,Rob!我理解你的观点了,我想我只是迷失在这样一个想法中,即我可以直接操作子视图,而不是快照它们并在“更高”的级别(容器)上操作它们。我现在意识到,我不仅需要使用容器视图来动画显示控制器视图本身,还需要对其中任何我想要动画的子视图进行操作。 - Aleksander
公平地说,有一个更简单的解决方案,不需要所有这些自定义动画师的东西,你只需在主动画旁边动画子视图,并绕过所有这些容器的东西,但它会带来很多限制(主要动画不能涉及移动主视图等)。 - Rob
虽然我相信我会坚持这种方法(似乎是“正确”的方式),但我很想了解更多关于其他方法的知识。有没有建议的资源可以学习更多/搜索术语可以得到适当的结果? - Aleksander
你可以获取视图控制器的 transitionCoordinator,然后使用 animate(alongsideTransition:completion:) 方法。这将为你提供一个与转场动画同步的动画效果(包括交互式动画手势的切换等)。 - Rob
1
这个需要有自己的教程。 - The1993

2
问题在于处理坐标系。考虑以下数字:
fromViewController.label.frame: {{115.5, 313}, {144, 41}}
toViewController.titleLabel.frame: {{16, 12}, {144, 41}}

那些数对是无关的:
  • label 的框架在其父视图的边界坐标中,可能是 fromViewController.view

  • titleLabel 的框架在其父视图的边界坐标中,可能是 toViewController.view

此外,在大多数自定义视图转换中,两个视图控制器的视图在整个过程中都在运动。这使得很难说中间视图在任何时刻应该在哪个视图控制器的坐标系中。

因此,您需要在某个公共坐标系统中表达此视图的运动,该坐标系统高于这两个坐标系之一。这就是为什么在我的答案 这里 中,我使用了一个在更高上下文视图中松散的快照视图。


好的。请更新您的答案,包括有关使用快照来解决此问题的说明(可能还要附上链接到您的其他答案,以帮助其他可能遇到此问题的人),我会将其标记为已接受! - Aleksander
@matt,你能在GitHub上提供一个示例吗?你回答了与这个问题相关的每一个问题,但我们还是无法弄清楚。 - John

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