您提到了子视图的动画,但您没有谈论整体动画,但我倾向于使用容器视图来制作动画,以避免在为子视图设置动画时出现任何潜在的混淆/问题和主视图同时进行。但我倾向于:
- 对“来自”视图中的子视图的位置进行快照,然后隐藏子视图;
- 在“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] }
}
现在,我不希望您迷失在所有这些代码中,因此我建议您专注于高级设计(我在顶部列举的前七点)。但希望这足以让您遵循基本思想。