我试图在两个视图控制器之间创建一个简单的过渡动画,两个视图控制器都有相同的标签。我只是想将标签从第一个视图控制器中的位置动画到第二个视图控制器中的位置(见下图)。
我已经设置了我的视图控制器以使用自定义动画控制器,我可以通过插座访问视图控制器和标签。
在动画块中,我只是将第一个视图控制器上的标签框架设置为第二个视图控制器上的标签框架。
[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}}
我不知道为什么我没有得到预期的行为,以及发生了什么。
关于我可以更改哪些内容以使我的动画正常运行以及为什么我看到此行为的任何建议都将非常感谢。
答案 0 :(得分:19)
你提到了子视图的动画,但你没有谈论整体动画,但是我倾向于使用动画的容器视图,以避免任何潜在的混淆/问题,如果你&# 39;同时为主视图重新设置子视图和的动画。但是我倾向于:
frame
值转换为容器的坐标空间,并将所有这些快照添加到容器视图中; alpha
为零(因此它们会淡入); alpha
更改回1
。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] }
}
现在,我不希望你迷失在所有这些代码中,所以我建议专注于高级设计(我在顶部列举的前七个点)。但希望这足以让你遵循基本的想法。
答案 1 :(得分:1)
问题在于处理坐标系。请考虑以下数字:
fromViewController.label.frame: {{115.5, 313}, {144, 41}}
toViewController.titleLabel.frame: {{16, 12}, {144, 41}}
这些数字对无关:
label
的框架位于其超级视图的边界坐标中,可能是fromViewController.view
。
titleLabel
的框架位于其超级视图的边界坐标中,可能是toViewController.view
。
此外,在大多数自定义视图过渡中,两个视图控制器的视图在整个过程中都处于运动状态。这使得很难根据其中任何一个说出中间视图在任何时刻的位置。
因此,您需要在一些常见的坐标系中表达此视图的运动,高于其中任何一个。这就是为什么,在我的回答here中,我使用的是在更高的上下文视图中松散的快照视图。