Почему анимация дрожит, когда CATransaction начинается и заканчивается примерно в одно и то же время?

проблема

Как я могу исправить дрожание в моей анимации прокрутки?

Как видно из анимации ниже, каждый раз, когда ноты (черные овалы) достигают вертикальной синей линии, возникает короткое дрожание, из-за чего кажется, что ноты пошли назад на долю секунды.

Анимации прокрутки запускаются серией транзакций CAT, и дрожание возникает каждый раз, когда одна анимация прокрутки завершается и начинается другая.

полноэкранная запись

В замедленном видео ниже, похоже, что на самом деле есть два овала друг на друга, один из которых останавливается и затухает, а другой продолжает прокручиваться. Но код на самом деле не создает один овал поверх другого.

замедленная запись экрана с увеличенным движением

Видео (GIF-изображения) взяты с экрана iPhone SE, а не с симулятора.

Ограничения проблемы:

  • Основной целью этой анимации является плавная линейная прокрутка каждой ноты, которая начинается и заканчивается точно по мере того, как каждая нота достигает синей линии. Синяя линия представляет текущий момент времени в сопровождающей музыке.

  • Продолжительность прокрутки и расстояния будут различаться, и эти значения генерируются динамически во время прокрутки, поэтому жесткое кодирование скорости прокрутки на время выполнения не будет работать.

Попытки Решения

  1. Настройка isScrolling флаг, предотвращающий запуск новых анимаций до завершения предыдущих анимаций, не устранял дрожание.
  2. Задание времени начала прокрутки, чтобы оно происходило немного раньше (т.е. длина 1 или 2 перерисовок экрана), тоже не сработало.
  3. Совместное выполнение 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 ответ

Сделайте так, чтобы 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.

Недостатком является то, что перекрытие может мешать последующей анимации, если перекрытие слишком велико, так что это не хорошее решение общего назначения, а просто взлом.

Другие вопросы по тегам