UIViewPropertyAnimator快速时序参数/短时长导致Scrubbing初始阶段出现值跳转问题
这个问题我之前做底部弹窗动画时也踩过坑,确实挺让人费解的——明明拖拽scrubbing应该是全程平滑的,结果短时长或者“快速”时序参数下,刚一开始拖拽就出现明显的跳变。下面我来帮你拆解原因和解决办法:
问题根源
UIViewPropertyAnimator 在处理极短时长(比如你用的0.25秒)的动画时,内部会做性能优化:为了减少计算开销,它会把动画的关键帧间隔拉大。当你开始scrubbing时,fractionComplete从0开始微小变化,系统直接跳到了第一个预计算的关键帧位置,而不是线性过渡,视觉上就表现为“突然跳转”。而时长较长时(比如1.25秒),关键帧足够密集,scrubbing的平滑度就符合预期了。
另外,哪怕你设置了dampingRatio:1(理论上是线性曲线),系统的默认时序参数实现还是会因为短时长触发这个优化逻辑,导致跳变。
解决方案
这里有几个直接有效的修复方式,结合你的代码来看:
1. 显式使用线性时序参数,跳过系统优化
把原来依赖duration+dampingRatio的初始化方式,换成显式的线性时序曲线参数,强制让fractionComplete和动画进度严格线性映射,这样无论时长多短,scrubbing都是平滑的。
修改你的makeOffsetAnimator方法:
private func makeOffsetAnimator( animating view: UIView, to offset: CGFloat ) -> UIViewPropertyAnimator { // 显式创建线性时序参数,避免系统的短时长优化 let linearTiming = UICubicTimingParameters(animationCurve: .linear) let propertyAnimator = UIViewPropertyAnimator(duration: 0.25, timingParameters: linearTiming) propertyAnimator.addAnimations { self.bottomConstraint?.constant = offset view.superview?.layoutIfNeeded() } propertyAnimator.addCompletion { position in self.bottomConstraint?.constant = position == .end ? offset : 0 } return propertyAnimator }
2. 分离拖拽与自动完成的动画逻辑(如果需要弹簧效果)
如果你希望松手后保留弹簧动画的自然感,而拖拽时保持平滑,可以分开处理:
- 拖拽scrubbing阶段:使用线性时序的animator,保证平滑
- 松手自动完成阶段:切换回带dampingRatio的弹簧动画
比如修改stop方法:
func stop(animating view: UIView, with velocity: CGPoint) { guard let currentFraction = offsetAnimator?.fractionComplete else { return } // 先停止当前的线性animator offsetAnimator?.stopAnimation(true) // 创建弹簧动画的animator,基于当前进度继续 let targetOffset = currentFraction > 0.5 ? initialSheetHeight : 0 let springAnimator = UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) springAnimator.addAnimations { self.bottomConstraint?.constant = targetOffset view.superview?.layoutIfNeeded() } springAnimator.startAnimation() }
3. 微调scrubbing的映射逻辑(备选方案)
如果不想改animator的初始化,也可以在move方法里对初始阶段的fraction做平滑插值,比如给小的fraction值加一个缓入处理,避免从0直接跳变:
func move(_ view: UIView, basedOn translation: CGPoint) { var fractionComplete = min(max(translation.y, 0) / initialSheetHeight, 1) // 对初始阶段的fraction做平滑处理,减少跳变感 if fractionComplete < 0.1 { fractionComplete = fractionComplete * 10 // 线性放大初始区间 } offsetAnimator?.fractionComplete = fractionComplete }
不过这个方法是治标不治本,还是推荐第一种方案从根源解决。
验证效果
修改后再测试短时长的情况,你会发现scrubbing从一开始就是平滑的,不会再出现跳变了——系统不会再因为短时长去稀疏关键帧,因为我们强制指定了线性时序曲线,要求全程均匀映射。
备注:内容来源于stack exchange,提问作者thomsmed

