Подтвердить что ты не робот

Правильная обработка/очистка/etc CADisplayLink в пользовательской анимации Swift?

Рассмотрим эту тривиальную синхронизацию,

var link:CADisplayLink?
var startTime:Double = 0.0
let animTime:Double = 0.2
let animMaxVal:CGFloat = 0.4

private func yourAnim()
    {
    if ( link != nil )
        {
        link!.paused = true
        //A:
        link!.removeFromRunLoop(
          NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
        link = nil
        }

    link = CADisplayLink(target: self, selector: #selector(doorStep) )
    startTime = CACurrentMediaTime()
    link!.addToRunLoop(
      NSRunLoop.currentRunLoop(), forMode:NSDefaultRunLoopMode)
    }

func doorStep()
    {
    let elapsed = CACurrentMediaTime() - startTime

    var ping = elapsed
    if (elapsed > (animTime / 2.0)) {ping = animTime - elapsed}

    let frac = ping / (animTime / 2.0)
    yourAnimFunction(CGFloat(frac) * animMaxVal)

    if (elapsed > animTime)
        {
        //B:
        link!.paused = true
        link!.removeFromRunLoop(
          NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
        link = nil
        yourAnimFunction(0.0)
        }
    }

func killAnimation()
    {
    // for example if the cell disappears or is reused
    //C:
    ????!!!!
    }

Кажется, что существуют различные проблемы.

At (A:), хотя link не является нулевым, может быть невозможно удалить его из цикла запуска. (Например, кто-то может инициализировать его с помощью link = link:CADisplayLink() - попробуйте его для сбоя.)

Во-вторых, в (B:) кажется, что это беспорядок... конечно, есть лучший (и более быстрый) способ, и что, если он равен нулю, даже если время просто истекло?

Наконец, в (C:), если вы хотите разбить анимацию... Я впал в депрессию и понятия не имею, что лучше.

И действительно, код в A: и B: должен быть тем же самым правом вызова, как и очищающий вызов.

4b9b3361

Ответ 1

Вот простой пример, показывающий, как Id выполняет реализацию CADisplayLink (в Swift 3):

class C { // your view class or whatever

  private var displayLink: CADisplayLink?
  private var startTime = 0.0
  private let animLength = 5.0

  func startDisplayLink() {

    stopDisplayLink() // make sure to stop a previous running display link
    startTime = CACurrentMediaTime() // reset start time

    // create displayLink & add it to the run-loop
    let displayLink = CADisplayLink(
      target: self, selector: #selector(displayLinkDidFire)
    )
    displayLink.add(to: .main, forMode: .commonModes)
    self.displayLink = displayLink
  }

  @objc func displayLinkDidFire(_ displayLink: CADisplayLink) {

    var elapsed = CACurrentMediaTime() - startTime

    if elapsed > animLength {
      stopDisplayLink()
      elapsed = animLength // clamp the elapsed time to the anim length
    }

    // do your animation logic here
  }

  // invalidate display link if it non-nil, then set to nil
  func stopDisplayLink() {
    displayLink?.invalidate()
    displayLink = nil
  }
}

Примечание:

  • Использовали nil здесь, чтобы представить состояние, в котором линия отображения не работает, поскольку нет простого способа получить эту информацию из недействительной ссылки на изображение.
  • Вместо использования removeFromRunLoop() использовали invalidate(), который не будет сбой, если ссылка на ссылку уже не добавлена ​​в цикл цикла. Однако эта ситуация никогда не должна возникать в первую очередь - как всегда, сразу же добавляя ссылку на ссылку на цикл выполнения после ее создания.
  • Weve сделал private displayLink для предотвращения того, чтобы внешние классы помещали его в неожиданное состояние (например, недействительным, но не устанавливая его на nil).
  • У нас есть один метод stopDisplayLink(), который делает недействительным ссылку на изображение (если она не равна нулю) и устанавливает ее на nil - вместо копирования и вставки этой логики.
  • Не устанавливайте paused на true, прежде чем аннулировать ссылку на отображение, поскольку это избыточно.
  • Вместо того, чтобы принудительно разворачивать displayLink после проверки не-nil, использовали необязательную цепочку, например displayLink?.invalidate() (которая вызовет invalidate(), если ссылка на соединение не равна nil). Хотя развертывание силы может быть "безопасным в вашей конкретной ситуации (как вы проверяете на нуль) - его потенциально опасно, когда дело доходит до будущего рефакторинга, так как вы можете переструктурировать свою логику, не учитывая, какое влияние это оказывает на развертывание силы.
  • Затягивание времени elapsed на продолжительность анимации, чтобы гарантировать, что более поздняя анимационная логика не выдаст значение из ожидаемого диапазона.
  • Наш метод обновления displayLinkDidFire(_:) принимает один аргумент типа CADisplayLink, если требуется по документации.

Ответ 2

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

** (Ссылка на демонстрационный проект в нижней части этого ответа - написанная в Swift 3)

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

В настоящее время я использую этот метод для анимации около 60 просмотров вокруг экрана одновременно в игре.

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

// defines an interface for receiving display update notifications
protocol DisplayUpdateReceiver: class {
    func displayWillUpdate(deltaTime: CFTimeInterval)
}

Далее мы собираемся определить наш класс оболочки отображения. Этот класс возьмет ссылку на инициализацию делегата. При инициализации он автоматически запустит нашу ссылку на изображение и очистит ее на deinit.

import UIKit

class DisplayUpdateNotifier {

    // **********************************************
    //  MARK: Variables
    // **********************************************

    /// A weak reference to the delegate/listener that will be notified/called on display updates
    weak var listener: DisplayUpdateReceiver?

    /// The display link that will be initiating our updates
    internal var displayLink: CADisplayLink? = nil

    /// Tracks the timestamp from the previous displayLink call
    internal var lastTime: CFTimeInterval = 0.0

    // **********************************************
    //  MARK: Setup & Tear Down
    // **********************************************

    deinit {
        stopDisplayLink()
    }

    init(listener: DisplayUpdateReceiver) {
        // setup our delegate listener reference
        self.listener = listener

        // setup & kick off the display link
        startDisplayLink()
    }

    // **********************************************
    //  MARK: CADisplay Link
    // **********************************************

    /// Creates a new display link if one is not already running
    private func startDisplayLink() {
        guard displayLink == nil else {
            return
        }

        displayLink = CADisplayLink(target: self, selector: #selector(linkUpdate))
        displayLink?.add(to: .main, forMode: .commonModes)
        lastTime = 0.0
    }

    /// Invalidates and destroys the current display link. Resets timestamp var to zero
    private func stopDisplayLink() {
        displayLink?.invalidate()
        displayLink = nil
        lastTime = 0.0
    }

    /// Notifier function called by display link. Calculates the delta time and passes it in the delegate call.
    @objc private func linkUpdate() {
        // bail if our display link is no longer valid
        guard let displayLink = displayLink else {
            return
        }

        // get the current time
        let currentTime = displayLink.timestamp

        // calculate delta (
        let delta: CFTimeInterval = currentTime - lastTime

        // store as previous
        lastTime = currentTime

        // call delegate
        listener?.displayWillUpdate(deltaTime: delta)
    }
}

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

class ViewController: UIViewController, DisplayUpdateReceiver {

    var displayLinker: DisplayUpdateNotifier?
    var animView: MoveableView?

    override func viewDidLoad() {
        super.viewDidLoad()

        // setup our animatable view and add as subview
        animView = MoveableView.init(frame: CGRect.init(x: 150.0, y: 400.0, width: 20.0, height: 20.0))
        animView?.configureMovement()
        animView?.backgroundColor = .blue
        view.addSubview(animView!)

        // setup our display link notifier wrapper class
        displayLinker = DisplayUpdateNotifier.init(listener: self)
    }

    // implement DisplayUpdateReceiver function to receive updates from display link wrapper class
    func displayWillUpdate(deltaTime: CFTimeInterval) {
        // pass the update call off to our animating view or views
        _ = animView?.update(deltaTime: deltaTime)

        // in this example, the animatable view will remove itself from its superview when its animation is complete and set a flag
        // that it ready to be used. We simply check if it ready to be recycled, if so we reset its position and add it to
        // our view again
        if animView?.isReadyForReuse == true {
            animView?.reset(center: CGPoint.init(x: CGFloat.random(low: 20.0, high: 300.0), y: CGFloat.random(low: 20.0, high: 700.0)))
            view.addSubview(animView!)
        }
    }
}

Наша функция обновления перемещаемых представлений выглядит следующим образом:

func update(deltaTime: CFTimeInterval) -> Bool {
    guard canAnimate == true, isReadyForReuse == false else {
        return false
    }

    // by multiplying our x/y values by the delta time new values are generated that will generate a smooth animation independent of the framerate.
    let smoothVel = CGPoint(x: CGFloat(Double(velocity.x)*deltaTime), y: CGFloat(Double(velocity.y)*deltaTime))
    let smoothAccel = CGPoint(x: CGFloat(Double(acceleration.x)*deltaTime), y: CGFloat(Double(acceleration.y)*deltaTime))

    // update velocity with smoothed acceleration
    velocity.adding(point: smoothAccel)

    // update center with smoothed velocity
    center.adding(point: smoothVel)

    currentTime += 0.01
    if currentTime >= timeLimit {
        canAnimate = false
        endAnimation()
        return false
    }

    return true
}

Если вы хотите просмотреть полный демонстрационный проект, вы можете скачать его из GitHub здесь: Демо-проект CADisplayLink