为啥当 CATransactions 开始和结束的时间大约在同一时间时动画会抖动?

Posted

技术标签:

【中文标题】为啥当 CATransactions 开始和结束的时间大约在同一时间时动画会抖动?【英文标题】:Why does animation jitter when CATransactions begin and end at about the same time?为什么当 CATransactions 开始和结束的时间大约在同一时间时动画会抖动? 【发布时间】:2017-12-20 23:43:19 【问题描述】:

问题

如何解决滚动动画中的抖动问题?

如下面的动画所示,每当音符(黑色椭圆)到达垂直的蓝线时,都会出现短暂的抖动,这使得音符似乎在一瞬间向后退。

滚动动画由一系列 CATransactions 触发,每次滚动动画完成和另一个滚动动画开始时都会发生抖动。

在下面的慢动作视频中,看起来实际上有两个椭圆相互重叠,一个停止并淡出,而另一个继续滚动。但是,代码实际上并没有在另一个椭圆上创建一个椭圆。

视频 (gif) 来自 iPhone SE 屏幕录制,而不是模拟器。

问题约束:

此动画的一个关键目标是在每个音符上实现平滑的线性滚动,当每个音符头到达蓝线时开始和结束。蓝线代表伴奏音乐中的当前时间点。

滚动持续时间和距离会有所不同,并且这些值是在滚动期间动态生成的,因此在执行期间硬编码滚动速率将不起作用。

尝试的解决方案

    设置 isScrolling 标志以防止新动画在之前的动画完成之前开始,但未修复抖动。 将滚动开始时间设置为稍早(即 1 或 2 次屏幕重绘的长度)也不起作用。 同时执行 1 和 2稍微改善了问题,但没有解决问题。

代码片段

StaffLayer(定义如下)控制滚动:

.scrollAcrossCurrentChordLayer() 管理CATransaction。这个方法被.scrollTimerCADisplayLink调用

.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
    

【问题讨论】:

【参考方案1】:

使 CATransactions 重叠

这看起来像是一个 hack,但它修复了抖动。

如果 CATransaction 应该在y 秒的时间段内将原点移动x,您可以将动画设置为在1.1 * y 秒的时间段内移动1.1 * x。滚动率是一样的,但是第二个 CATransaction 在第一个完成之前就开始了,并且抖动消失了。

这可以通过对原始代码的小修改来实现:

let overlapFactor = 1.1
CATransaction.setAnimationDuration(
        (chordLayer.chord.lengthInSeconds ?? 0.0)
        * overlapFactor // <- ADDED OVERLAP FACTOR HERE
)
bounds.origin.x += distance*CGFloat(overlapFactor) // <- ADDED OVERLAP FACTOR HERE
CATransaction.commit()

我无法严格解释为什么会这样。这可能与CoreAnimation幕后发生的优化有关。

一个缺点是如果重叠太大,重叠会干扰后续动画,所以这不是一个好的通用解决方案,只是一个 hack。

【讨论】:

以上是关于为啥当 CATransactions 开始和结束的时间大约在同一时间时动画会抖动?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 std::variant 用开始和结束迭代器编译?

为啥我应该使用基于块的动画而不是开始/结束动画?

为啥范围(开始,结束)不包括结束?

为啥我的 HKWorkoutSession(通常)没有结束?

为啥我的 HKWorkoutSession(通常)没有结束?

为啥查询不返回存档帖子?