Почему анимация дрожит, когда CATransaction начинается и заканчивается примерно в одно и то же время?
проблема
Как я могу исправить дрожание в моей анимации прокрутки?
Как видно из анимации ниже, каждый раз, когда ноты (черные овалы) достигают вертикальной синей линии, возникает короткое дрожание, из-за чего кажется, что ноты пошли назад на долю секунды.
Анимации прокрутки запускаются серией транзакций CAT, и дрожание возникает каждый раз, когда одна анимация прокрутки завершается и начинается другая.
В замедленном видео ниже, похоже, что на самом деле есть два овала друг на друга, один из которых останавливается и затухает, а другой продолжает прокручиваться. Но код на самом деле не создает один овал поверх другого.
Видео (GIF-изображения) взяты с экрана iPhone SE, а не с симулятора.
Ограничения проблемы:
Основной целью этой анимации является плавная линейная прокрутка каждой ноты, которая начинается и заканчивается точно по мере того, как каждая нота достигает синей линии. Синяя линия представляет текущий момент времени в сопровождающей музыке.
Продолжительность прокрутки и расстояния будут различаться, и эти значения генерируются динамически во время прокрутки, поэтому жесткое кодирование скорости прокрутки на время выполнения не будет работать.
Попытки Решения
- Настройка
isScrolling
флаг, предотвращающий запуск новых анимаций до завершения предыдущих анимаций, не устранял дрожание. - Задание времени начала прокрутки, чтобы оно происходило немного раньше (т.е. длина 1 или 2 перерисовок экрана), тоже не сработало.
- Совместное выполнение 1 и 2 немного улучшило проблему, но не устранило ее.
Фрагмент кода
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
}
}
1 ответ
Сделайте так, чтобы CATransaction перекрывались
Это похоже на взлом, но это устраняет дрожание.
Если CATransaction должна сместить происхождение на x
в течение периода y
секунд, вы можете установить анимацию, чтобы идти 1.1 * x
в течение периода 1.1 * y
секунд. Скорость прокрутки та же, но вторая транзакция CAT начинается до завершения первой, и дрожание исчезает.
Это может быть достигнуто небольшой модификацией исходного кода:
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.
Недостатком является то, что перекрытие может мешать последующей анимации, если перекрытие слишком велико, так что это не хорошее решение общего назначения, а просто взлом.