问题
如何修复滚动动画中的抖动?
如下面的动画所示,每次音符(黑色椭圆)到达垂直的蓝线时都会出现短暂的抖动,这使得音符似乎在一瞬间向后退。
滚动动画由一系列 CATransaction 触发,每次滚动动画完成和另一个滚动动画开始时都会发生抖动。
在下面的慢动作视频中,看起来实际上有两个椭圆相互重叠,一个停止并淡出,而另一个继续滚动。但是,代码实际上并没有在另一个椭圆上创建一个椭圆。
视频 (gif) 来自 iPhone SE 屏幕录制,而不是模拟器。
问题约束:
此动画的一个关键目标是在每个音符上进行平滑的线性滚动,当每个音符头到达蓝线时开始和结束。蓝线代表伴奏音乐中的当前时间点。
滚动持续时间和距离会有所不同,并且这些值是在滚动期间动态生成的,因此在执行期间硬编码滚动速率将不起作用。
尝试的解决方案
- 设置一个
isScrolling
标志,以防止在以前的动画完成之前开始新的动画,并没有修复抖动。 - 将滚动开始时间设置得稍微早一点(即 1 或 2 次屏幕重绘的长度),也不起作用。
- 一起做 1 和 2稍微改善了问题,但没有解决它。
代码片段
A StaffLayer
(定义如下)控制滚动:
.scrollAcrossCurrentChordLayer()
管理CATransaction
. 此方法由.scrollTimer
CADisplayLink
.start()
并.scrollTimer
管理CADisplayLink
为了清楚起见,代码被大量缩写
class StaffLayer: CAShapeLayer, CALayerDelegate {
var currentTimePositionX: CGFloat // x-coordinate of blue line
var scrollTimer: CADisplayLink? = nil
/// Sets and starts `scrollTimer`, which is a `CADisplayLink`
func start() {
scrollTimer = CADisplayLink(
target: self,
selector: #selector(scrollAcrossCurrentChordLayer)
)
scrollTimer?.add(
to: .current,
forMode: .defaultRunLoopMode
)
}
/// Trigger scrolling when the currentChordLayer.startTime has passed
@objc func scrollAcrossCurrentChordLayer() {
// don't scroll if the chord hasn't started yet
guard currentChordLayer.startTime < Date().timeIntervalSince1970 else { return }
// compute how far to scroll
let nextChordMinX = convert(
nextChordLayer.bounds.origin,
from: nextChordLayer
).x
let distance = nextChordMinX - currentTimePositionX // distance from note to vertical blue line
// perform scrolling in CATransaction
CATransaction.begin()
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(
name: kCAMediaTimingFunctionLinear
))
CATransaction.setAnimationDuration(
chordLayer.chord.lengthInSeconds ?? 0.0
)
bounds.origin.x += distance
CATransaction.commit()
// set currentChordLayer to next chordLayer
currentChordLayer = currentChordLayer.nextChordLayer
}
}