如何实现先交互转场,达阈值后执行不同结束动画?
刚好前段时间做过类似App Store Today卡片的dismiss交互,踩了几个小坑,分享下完整的实现思路和代码吧~
实现带阈值切换的交互式转场(类似Today卡片 dismissal)
常规的交互式转场依赖UIViewControllerAnimatedTransitioning定义动画逻辑,UIPercentDrivenInteractiveTransition控制进度,但这次需求的核心是拖动进度超过阈值后,放弃交互式进度驱动,切换到自定义的快速结束动画,下面是具体实现步骤:
核心思路拆解
- 自定义动画器:需要区分两种结束状态——常规交互式完成/取消,以及触发阈值后的特殊收尾动画,所以要给动画器加一个标记位来切换逻辑
- 自定义交互式控制器:监听拖动手势的进度,当超过设定阈值时,停止进度驱动,手动触发转场完成,并通知动画器切换到特殊动画
- 手势绑定:在卡片页面添加边缘拖动手势,关联交互式控制器并触发转场
关键代码实现
1. 自定义转场动画器
这个类负责定义两种不同的动画逻辑:
class CardDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning { // 标记是否触发阈值后的特殊动画 var shouldUseSpecialFinishAnimation = false private let animationDuration: TimeInterval = 0.35 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return animationDuration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: .from), let toVC = transitionContext.viewController(forKey: .to), let containerView = transitionContext.containerView else { return } containerView.insertSubview(toVC.view, belowSubview: fromVC.view) toVC.view.alpha = 0.8 // 初始背景透明度,贴近原生效果 if shouldUseSpecialFinishAnimation { // 阈值触发后的快速收尾动画:卡片缩小+淡出,背景完全显示 UIView.animate(withDuration: animationDuration * 0.7, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: 1.2, options: .curveEaseOut) { fromVC.view.transform = CGAffineTransform(scaleX: 0.88, y: 0.88) fromVC.view.alpha = 0 toVC.view.alpha = 1 } completion: { _ in fromVC.view.transform = .identity transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } else { // 常规交互式动画:跟随拖动进度的位移+轻微缩小 let finalFrame = CGRect(x: 0, y: containerView.bounds.height, width: fromVC.view.frame.width, height: fromVC.view.frame.height) UIView.animate(withDuration: animationDuration, delay: 0, options: .curveLinear) { fromVC.view.frame = finalFrame fromVC.view.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) } completion: { _ in fromVC.view.transform = .identity transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } } }
2. 自定义交互式转场控制器
这个类是核心,负责监听手势进度、判断阈值并切换动画逻辑:
class CardInteractiveTransition: UIPercentDrivenInteractiveTransition { var isInteractive = false // 自定义阈值,比如拖动进度超过30%触发特殊动画 private let triggerThreshold: CGFloat = 0.3 weak var animator: CardDismissAnimator? func handlePanGesture(_ gesture: UIPanGestureRecognizer) { guard let containerView = gesture.view?.superview else { return } let translation = gesture.translation(in: containerView) // 计算拖动进度:垂直拖动距离 / 容器高度 let progress = translation.y / containerView.bounds.height switch gesture.state { case .began: isInteractive = true // 触发dismiss转场 gesture.view?.window?.rootViewController?.dismiss(animated: true) case .changed: update(progress) // 进度超过阈值,切换到特殊动画并结束交互式驱动 if progress > triggerThreshold { animator?.shouldUseSpecialFinishAnimation = true finish() isInteractive = false } case .ended, .cancelled: guard isInteractive else { return } // 未到阈值时,判断是否完成/取消转场 if progress > triggerThreshold * 0.5 { animator?.shouldUseSpecialFinishAnimation = true finish() } else { cancel() } isInteractive = false default: isInteractive = false cancel() } } }
3. 在卡片页面绑定手势与转场代理
class CardViewController: UIViewController { private let interactiveTransition = CardInteractiveTransition() override func viewDidLoad() { super.viewDidLoad() // 添加左边缘拖动手势(可根据需求改为底部/其他边缘) let edgePan = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleEdgePan(_:))) edgePan.edges = .left view.addGestureRecognizer(edgePan) } @objc private func handleEdgePan(_ gesture: UIScreenEdgePanGestureRecognizer) { interactiveTransition.handlePanGesture(gesture) } // 重写dismiss方法,设置转场代理 override func dismiss(animated: Bool, completion: (() -> Void)? = nil) { transitioningDelegate = self super.dismiss(animated: animated, completion: completion) } } // 实现转场代理协议 extension CardViewController: UIViewControllerTransitioningDelegate { func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { let animator = CardDismissAnimator() interactiveTransition.animator = animator return animator } func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return interactiveTransition.isInteractive ? interactiveTransition : nil } }
注意事项
- 阈值参数
triggerThreshold可以根据产品需求调整,比如App Store的阈值大概是拖动卡片到屏幕中间偏下的位置 - 特殊动画的弹簧参数(阻尼、初速度)需要反复调试,尽量贴近原生流畅感
- 要处理好手势状态的边界情况,比如拖动到阈值后快速松手,确保动画能正常收尾
- 如果是导航控制器的push/pop转场,只需要调整转场代理的对应方法(比如
animationController(forPop:)),核心逻辑完全通用
内容的提问来源于stack exchange,提问作者aunnnn
相关产品推荐
相关产品推荐

