iOS关键帧交互动画实现Youtube式关闭效果时手势结束恢复异常
问题根因
你遇到的跳转问题是交互式转场和UIView关键帧动画的属性提交逻辑不匹配导致的,具体原因有两个:
- 你将所有约束(或手动frame)的最终值修改逻辑直接写在了
animateKeyframes的根闭包中,没有包裹在对应时间区间的addKeyframe闭包里。这会导致动画启动的瞬间,所有属性的最终状态就已经被提交给系统,关键帧仅控制layoutIfNeeded的动画插值过程,属性的最终值本身不参与动画进度的绑定。 UIPercentDrivenInteractiveTransition对UIView.animateKeyframes的 scrub 兼容本身存在缺陷,尤其是使用calculationModeCubic模式时,当调用cancel()回退动画时,系统不会自动回滚你直接提交的属性修改,就会出现停留在最终帧的问题。
非交互场景下动画正常是因为动画会完整从头播放到尾,属性的连续插值会掩盖掉提前提交最终值的问题。
解决方案
推荐优先替换为UIViewPropertyAnimator实现动画,这是iOS10之后专门为交互式场景设计的动画API,对进度控制、取消/完成的状态处理兼容性更好,修改方案如下:
1. 替换动画实现
将原来的animateTransition中的关键帧动画替换为多段式UIViewPropertyAnimator实现:
// 新增存储初始顶部约束的属性 var playerTopCon: NSLayoutConstraint! func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { self.transitionContext = transitionContext let fromVC = transitionContext.viewController(forKey: .from) as! ViewController let toVC = transitionContext.viewController(forKey: .to) as! RootViewController let containerView = transitionContext.containerView innerViewCopy = fromVC.view tabBarHeight = toVC.tabBarVC.tabBar.frame.height let duration = transitionDuration(using: transitionContext) fromVC.view.isHidden = true let tabBar = toVC.tabBarVC.tabBar let tabBarSuperview = tabBar.superview! // 先初始化所有视图、约束到初始状态 setupInitialState(containerView: containerView, fromVC: fromVC) // 创建动画器 let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut) // 第一阶段动画:0~70%进度 animator.addAnimations({ self.playerHeightCon.constant /= self.playerResizeByInitial NSLayoutConstraint.deactivate([self.playerTopCon]) NSLayoutConstraint.activate([self.playerBottomCon]) self.dimmingView.alpha = 0.75 containerView.insertSubview(tabBar, aboveSubview: self.dimmingView) tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: -self.tabBarHeight) containerView.layoutIfNeeded() }, delayFactor: 0) // 第二阶段动画:70%~100%进度 animator.addAnimations({ NSLayoutConstraint.deactivate([self.playerTrailingCon]) NSLayoutConstraint.activate([ self.playerCopy.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: self.playerMinimizedMultiplier) ]) self.playerHeightCon.constant /= self.playerResizeByFinal self.playerBottomCon.constant = -self.tabBarHeight containerView.layoutIfNeeded() }, delayFactor: 0.7) // 动画完成回调 animator.addCompletion { position in tabBarSuperview.addSubview(tabBar) self.tableSnapshot.removeFromSuperview() self.dimmingView.removeFromSuperview() let success = position == .end ? !transitionContext.transitionWasCancelled : false transitionContext.completeTransition(success) self.transitionContext = nil } animator.startAnimation() } // 单独抽离初始状态设置,不涉及任何动画修改 func setupInitialState(containerView: UIView, fromVC: ViewController) { playerCopy = innerViewCopy.viewWithTag(ViewTags.video) as? YTPlayer containerView.addSubview(playerCopy) tableSnapshot = fromVC.relatedVideosTableView.snapshotView(afterScreenUpdates: true)! tableSnapshot.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(tableSnapshot) dimmingView = UIView() dimmingView.translatesAutoresizingMaskIntoConstraints = false dimmingView.backgroundColor = .white dimmingView.alpha = 0 containerView.addSubview(dimmingView) playerHeightCon = playerCopy.heightAnchor.constraint(equalToConstant: playerMaxHeight) playerTrailingCon = playerCopy.trailingAnchor.constraint(equalTo: containerView.trailingAnchor) let playerLeadingCon = playerCopy.leadingAnchor.constraint(equalTo: containerView.leadingAnchor) playerTopCon = playerCopy.topAnchor.constraint(equalTo: containerView.topAnchor, constant: UIApplication.shared.keyWindow!.safeAreaInsets.top) playerBottomCon = playerCopy.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -tabBarHeight-tabBarDividerHeight) NSLayoutConstraint.deactivate(playerCopy.constraints) NSLayoutConstraint.activate([ playerLeadingCon, playerTrailingCon, playerTopCon, playerHeightCon, tableSnapshot.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), tableSnapshot.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), tableSnapshot.topAnchor.constraint(equalTo: playerCopy.bottomAnchor), tableSnapshot.heightAnchor.constraint(equalToConstant: fromVC.relatedVideosTableView.frame.height), dimmingView.leadingAnchor.constraint(equalTo: tableSnapshot.leadingAnchor), dimmingView.trailingAnchor.constraint(equalTo: tableSnapshot.trailingAnchor), dimmingView.bottomAnchor.constraint(equalTo: tableSnapshot.bottomAnchor), dimmingView.heightAnchor.constraint(equalTo: tableSnapshot.heightAnchor) ]) UIView.performWithoutAnimation { containerView.layoutIfNeeded() } }
2. 交互逻辑适配
不需要修改原有DismissalController的代码,UIViewPropertyAnimator和UIPercentDrivenInteractiveTransition完全兼容,原来的update(_ progress:)、finish()、cancel()调用都可以正常工作。
3. 额外修复点
你原来的代码中注释掉了转场完成的回调逻辑,无论转场成功还是取消都必须调用transitionContext.completeTransition(_:),否则会出现内存泄漏和界面无响应的问题。
内容的提问来源于stack exchange,提问作者Rokata
相关产品推荐
相关产品推荐

