20210716, 发现一些网上的退场动画,跑一下,一脸懵
本文探讨下,怎样解决
入场动画,很简单
-
指定一下动画时间,通过
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
-
如何动画, 通过
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
左手一个 from View Controller , 上一个控制器,
右手一个 to View Controller , 下一个控制器,
让上一个消失,让下一个出现
UIView.animate
调一下动画属性,
一般就是改一下视图 frame ,完了
下面的代码,效果比较简单
一般 present 一个控制器,由下而上,
这里可以做到,从上下左右,都可以推出控制器
enum PresentingDirection{
case top, right, left, bottom
var bounds: CGRect{
UIScreen.main.bounds
}
func offsetF(withFrame viewFrame: CGRect) -> CGRect{
let h = bounds.size.height
let w = bounds.size.width
switch self {
case .top:
return viewFrame.offsetBy(dx: 0, dy: -h)
case .bottom:
return viewFrame.offsetBy(dx: 0, dy: h)
case .left:
return viewFrame.offsetBy(dx: -w, dy: 0)
case .right:
return viewFrame.offsetBy(dx: w, dy: 0)
}
}
}
class CustomPresentationController: NSObject, UIViewControllerAnimatedTransitioning{
fileprivate var presentingDirection: PresentingDirection
init(direction orientation: PresentingDirection) {
presentingDirection = orientation
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else{
return
}
let finalCtrlFrame = transitionContext.finalFrame(for: toCtrl)
let containerView = transitionContext.containerView
toCtrl.view.frame = presentingDirection.offsetF(withFrame: finalCtrlFrame)
containerView.addSubview(toCtrl.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear) {
fromCtrl.view.alpha = 0.5
toCtrl.view.frame = finalCtrlFrame
} completion: { _ in
fromCtrl.view.alpha = 1
transitionContext.completeTransition(true)
}
}
}
出场动画,有更新
接着上面的例子
1,凑合着用
熟悉的配方,同样是处理掉两个方法,
一个是动画时间,一个是怎么动画的
class CustomDismissController: NSObject, UIViewControllerAnimatedTransitioning{
fileprivate var presentingDirection: PresentingDirection
init(direction orientation: PresentingDirection) {
presentingDirection = orientation
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), let toView = toCtrl.view else{
return
}
let finalCtrlFrame = transitionContext.finalFrame(for: fromCtrl)
let containerView = transitionContext.containerView
fromCtrl.view.alpha = 0.5
toView.frame = presentingDirection.offsetF(withFrame: finalCtrlFrame)
containerView.bringSubviewToFront(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear) {
toView.frame = finalCtrlFrame
} completion: { _ in
let success = !transitionContext.transitionWasCancelled
transitionContext.completeTransition(success)
}
}
}
两条重点
fromCtrl.view.alpha = 0.5
这一句保证了,退场动画时,
from.view
不会把 toCtrl.view
遮挡的严严实实
containerView.bringSubviewToFront(toView)
这一句保证了,退场后,不会把 toCtrl.view
丢失
比较好的解决思路
动画是快照的事情,
左手一个 from 控制器,右手一个 to 控制器,布局一下,就好了
class CustomDismissController: NSObject, UIViewControllerAnimatedTransitioning{
fileprivate var presentingDirection: PresentingDirection
init(direction orientation: PresentingDirection) {
presentingDirection = orientation
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), let toView = toCtrl.view, let snapshot = toView.snapshotView(afterScreenUpdates: true) else{
return
}
let finalCtrlFrame = transitionContext.finalFrame(for: fromCtrl)
let containerView = transitionContext.containerView
snapshot.frame =
presentingDirection.offsetF(withFrame: finalCtrlFrame)
containerView.addSubview(snapshot)
containerView.bringSubviewToFront(toView)
toView.frame = finalCtrlFrame
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear) {
snapshot.frame = finalCtrlFrame
} completion: { _ in
snapshot.removeFromSuperview()
let success = !transitionContext.transitionWasCancelled
transitionContext.completeTransition(success)
}
}
}
细节
上面代码中,如果调整下,
拿 from 控制器的快照,放在底部,
拿 to 控制器的视图,放在 from 控制器的快照的上面,去动画
发现, 放置不上去。 设置无效
而且,to 控制器的视图,从父视图 remove 掉,就找不回了
另一种思路
from 控制器的视图,就是在 to 控制器的视图的上面,
可以把 from 控制器的背景色改为透明,内容都给隐藏掉
例子
下面的退场动画,稍微复杂,
使用了关键帧动画,多加了几步
class FlipDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
private let destinationFrame: CGRect
let interactionController: SwipeInteractionController?
init(destinationFrame: CGRect, interactionController: SwipeInteractionController?) {
self.destinationFrame = destinationFrame
self.interactionController = interactionController
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to),
let toView = toVC.view,
let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false)
else {
return
}
snapshot.layer.cornerRadius = CardViewController.cardCornerRadius
snapshot.layer.masksToBounds = true
let containerView = transitionContext.containerView
containerView.addSubview(snapshot)
if let fromC = fromVC as? RevealViewController{
fromC.imageView.isHidden = true
fromC.view.backgroundColor = UIColor.clear
}
AnimationHelper.perspectiveTransform(for: containerView)
toView.layer.transform = AnimationHelper.yRotation(-.pi / 2)
let duration = transitionDuration(using: transitionContext)
containerView.bringSubviewToFront(toView)
UIView.animateKeyframes(
withDuration: duration,
delay: 0,
options: .calculationModeCubic,
animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1/3) {
snapshot.frame = self.destinationFrame
}
UIView.addKeyframe(withRelativeStartTime: 1/3, relativeDuration: 1/3) {
snapshot.layer.transform = AnimationHelper.yRotation(.pi / 2)
}
UIView.addKeyframe(withRelativeStartTime: 2/3, relativeDuration: 1/3) {
toView.layer.transform = AnimationHelper.yRotation(0.0)
}
},
completion: { _ in
snapshot.removeFromSuperview()
if transitionContext.transitionWasCancelled {
containerView.sendSubviewToBack(toView)
}
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
最后,补一下基础
转场动画结构图:
- 每一个控制器,都有一个属性,
transitioningDelegate
通过 UIViewControllerTransitioningDelegate
协议的方法,提供对应的专场动画
- 转场上下文
Transitioning Context
上下文,就是啥都有
可以从里面拿到左手的 from 控制器,和右手的 to 控制器,
和控制器对应的 frame
还可以直接从上下文拿 from 和 to 的视图 view,
个人感觉,用处不大
- 动画完成了,
调用方法 completeTransition(_:)
,反馈给系统框架,动画的完成情况