视图控制器转换动画子视图位置

Posted

技术标签:

【中文标题】视图控制器转换动画子视图位置【英文标题】:View Controller Transition animate subview position 【发布时间】:2017-10-06 00:07:26 【问题描述】:

我正在尝试在两个具有相同标签的视图控制器之间创建一个简单的过渡动画。我只是想将标签从它在第一个视图控制器中的位置动画到它在第二个视图控制器中的位置(见下图)。

我已将我的视图控制器设置为使用自定义动画控制器,我可以通过插座访问视图控制器和标签。

在动画块中,我只是将第一个视图控制器上标签的框架设置为第二个视图控制器上标签的框架。

[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 谢谢你的建议,但这个答案似乎没有回答我的问题。重申一下,我想使用自定义视图控制器转换动画将一个视图控制器的子视图设置为由第二个视图控制器的子视图定义的另一个位置。这应该是一项微不足道的任务,但由于某种原因,我无法让它工作,我不明白为什么。 @matt 如果我正确理解了您对另一个问题的回答,您可以创建UIView 的快照以进行移动,为其指定原始UIView 的位置,然后对其进行动画处理目的地。虽然我同意这个逻辑,但我不想创建快照。相反,我想将第一个视图控制器中的原始 UILabel 移动到第二个视图控制器中相应标签位置指定的位置。 @matt 跟进,是的,创建快照会起作用。我想知道如果我正确修改标签为什么它不起作用,以及我可以做些什么来使它起作用。 【参考方案1】:

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

    对“来自”视图中的子视图的位置进行快照,然后隐藏子视图; 在“to”视图中创建子视图位置的快照,然后隐藏子视图; 将所有这些frame 值转换为容器的坐标空间,并将所有这些快照添加到容器视图中; 从零开始“到”快照alpha(因此它们淡入); 动画将“to”快照更改为最终目的地,将其alpha 更改回1。 同时将“from”快照设置为“to”视图最终目的地的位置,并将它们的 alpha 设置为零(因此它们淡出,与第 4 点相结合,产生一种交叉溶解)。 全部完成后,移除快照并取消隐藏快照动画的子视图。

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

例如:

通过将容器视图用于快照动画,它独立于您可能对目标场景的主视图执行的任何动画。在这种情况下,我将其从右侧滑入,但您可以做任何您想做的事情。

或者,您可以使用多个子视图来执行此操作:

(就个人而言,如果是这种情况,几乎所有东西都在滑动,我会失去主视图的滑动动画,因为它现在变得分散注意力,但它给了你基本的想法。另外,在我的关闭动画中,我将哪个视图换成了另一个视图,这是你永远不会做的,但我只是想说明灵活性和衰落。)

为了呈现上面的内容,我在 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] 

为了支持驳回,我将添加逆协议一致性:

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


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

现在,我不希望您迷失在所有这些代码中,因此我建议您专注于高级设计(我在顶部列举的前七点)。但希望这足以让您遵循基本思想。

【讨论】:

非常感谢 Rob 的详细解释!我遵循您的观点,我想我只是迷失在我可以直接操作子视图而不是快照它们并在比两个视图(容器)“更高”的某个级别上操作它们的想法中。我现在意识到我必须使用容器视图,不仅要在视图控制器视图本身中制作动画,还要在它们上的任何我想要动画的子视图中制作动画。 公平地说,有一个更简单的解决方案不需要所有这些自定义动画师的东西,您只需在主动画旁边为子视图制作动画并绕过所有这些容器的东西,但它强加了很多限制(主动画不能涉及主视图的移动等)。 虽然我相信我会坚持这种方法(似乎是“正确”的方法),但我很想了解更多关于另一种方法的信息。是否有任何建议的资源来了解更多信息/搜索可以产生适当结果的字词? 你可以获取视图控制器的transitionCoordinator,然后是animate(alongsideTransition:completion:)。这会为您提供与过渡动画同步的动画(包括擦洗交互式动画手势等)。 这值得拥有自己的教程【参考方案2】:

问题在于处理坐标系。考虑这些数字:

fromViewController.label.frame: 115.5, 313, 144, 41
toViewController.titleLabel.frame: 16, 12, 144, 41

这些数字对不相关

label 的框架位于其父视图的边界坐标中,可能是fromViewController.view

titleLabel 的框架位于 superview 的边界坐标中,可能是toViewController.view

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

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

【讨论】:

很公平。请更新您的答案以包含有关使用快照来克服此问题的说明(以及可能指向您的其他答案的链接,以帮助可能遇到此问题的其他人),我可以标记为已接受! @matt 你能在 github 上做例子吗?您回答了与此问题相关的所有问题,但我们无法弄清楚。

以上是关于视图控制器转换动画子视图位置的主要内容,如果未能解决你的问题,请参考以下文章

在视图控制器转换中获取子视图的最终帧

同一屏幕上的子视图控制器之间的 IOS 转换

动画子视图控制器视图

在过渡期间动画子视图控制器的视图

如何将视图控制器动画转换转换为 Segue 动画转换

更改视图转换会更改 iOS 7 上的视图位置,但不会在 ios8 上更改视图位置