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
isScrolling
flag, to prevent new animations from starting before previous animations have completed, did not fix the jitter. 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
}
}
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.