简体   繁体   中英

Why does animation jitter when CATransactions begin and end at about the same time?

Problem

How can I fix the jitter in my scrolling animation?

As seen in the animations below, there is a brief jitter every time the notes (black ovals) reach the vertical blue line, which makes it appear that the notes went backwards for a split second.

The scrolling animations are triggered by a series of CATransactions, and the jitter occurs every time one scrolling animation completes and another starts.

全屏录制

In the slow motion video below, it looks like there are actually two ovals on top of each other, one which stops and fades out while the other keeps scrolling. But, the code does not actually create one oval atop another.

慢动作变焦屏幕录制

The videos (gifs) are from an iPhone SE screen recording, not a simulator.

Problem Constraints:

  • A key objective of this animation is to have smooth, linear scrolling across each note, that begins and ends exactly as each note head reaches the blue line. The blue line represents the current point in time in accompanying music.

  • The scrolling durations and distances will vary, and these values are generated dynamically during the scrolling, so hard coding the scroll rate for the duration of execution will not work.

Attempted Solutions

  1. Setting an isScrolling flag, to prevent new animations from starting before previous animations have completed, did not fix the jitter.
  2. Setting the scrolling start time to happen slightly early (ie the length of 1 or 2 screen redraws), didn't work either.
  3. Doing 1 and 2 together slightly improved the problem, but did not fix it.

Code Snippet

A StaffLayer (defined below) controls the scrolling:

  • .scrollAcrossCurrentChordLayer() manages the CATransaction . This method is called by the .scrollTimer CADisplayLink

  • .start() and .scrollTimer manage the CADisplayLink

Code heavily abbreviated for clarity

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
    }
}

Make the CATransactions overlap

This seems like a hack, but it fixes the jitter.

If the CATransaction should shift origin by x over a period of y seconds, you can set the animation to go 1.1 * x over a period of 1.1 * y seconds. The the scroll rate is the same, but the second CATransaction starts before the first one has finished, and the jitter disappears.

This can be achieved by a small modification of the original code:

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()

I can't give a rigorous explanation for why this works. It may be related to the optimizations happening behind the scenes in CoreAnimation.

A drawback is that the overlap can interfere with subsequent animations if the overlap is too large, so this is not a good general purpose solution, just a hack.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM