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

时间:2017-10-06 00:07:26

标签: ios objective-c animation uiviewcontroller

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

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}}

我不知道为什么我没有得到预期的行为,以及发生了什么。

关于我可以更改哪些内容以使我的动画正常运行以及为什么我看到此行为的任何建议都将非常感谢。

2 个答案:

答案 0 :(得分:19)

你提到了子视图的动画,但你没有谈论整体动画,但是我倾向于使用动画的容器视图,以避免任何潜在的混淆/问题,如果你&# 39;同时为主视图重新设置子视图的动画。但是我倾向于:

  1. 制作"来自"中的子视图的快照。查看然后隐藏子视图;
  2. 制作"到"中的子视图的快照。查看然后隐藏子视图;
  3. 将所有这些frame值转换为容器的坐标空间,并将所有这些快照添加到容器视图中;
  4. 启动"到"快照' alpha为零(因此它们会淡入);
  5. 动画更改"到"快照到最终目的地,将alpha更改回1
  6. 同时动画"来自"快照到"到"的位置查看最终目的地并将其alpha设置为零(因此它们淡出,与第4点相结合,产生一种交叉溶解)。
  7. 完成所有操作后,删除快照并取消隐藏其快照已设置动画的子视图。
  8. 净效果是标签从一个位置滑动到另一个位置,如果初始和最终内容不同,则在它们被移动时产生交叉溶解。

    例如:

    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] }
    }
    

    为了支持解雇,我添加了相反的协议一致性:

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

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

答案 1 :(得分:1)

问题在于处理坐标系。请考虑以下数字:

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

这些数字对无关

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

  • titleLabel的框架位于超级视图的边界坐标中,可能是toViewController.view

此外,在大多数自定义视图过渡中,两个视图控制器的视图在整个过程中都处于运动状态。这使得很难根据其中任何一个说出中间视图在任何时刻的位置。

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