Рассмотрим эту тривиальную синхронизацию,
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: должен быть тем же самым правом вызова, как и очищающий вызов.
Ответ 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