簡體   English   中英

在Swift自定義動畫中正確處理/清理CADisplayLink?

[英]Correct handling / cleanup / etc of CADisplayLink in Swift custom animation?

使用CADisplayLink考慮這個簡單的同步動畫,

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:
    ????!!!!
    }

似乎存在各種問題。

在(A :),即使link不為null,也可能無法從運行循環中刪除它。 (例如,某人可能已使用link = link:CADisplayLink()對其進行初始化 - 嘗試進行崩潰。)

其次在(B :)它似乎是一團糟...肯定有一個更好的(和更多的Swift)方式,如果它是零,即使時間剛剛過期怎么辦?

最后在(C :)如果你想打破這個目標...我感到沮喪並且不知道什么是最好的。

實際上A:和B:的代碼應該是相同的呼叫權限,是一種清理呼叫。

這是一個簡單的例子,展示了我如何實現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's non-nil, then set to nil
  func stopDisplayLink() {
    displayLink?.invalidate()
    displayLink = nil
  }
}

注意事項:

  • 我們在這里使用nil來表示顯示鏈接未運行的狀態 - 因為沒有簡單的方法從無效的顯示鏈接獲取此信息。
  • 我們使用的是invalidate() ,而不是使用removeFromRunLoop() ,如果顯示鏈接尚未添加到運行循環中,則不會崩潰。 但是,這種情況絕不應該首先出現 - 因為我們總是在創建后立即將顯示鏈接添加到運行循環中。
  • 我們將displayLink私有,以防止外部類將其置於意外狀態(例如使其無效但不將其設置為nil )。
  • 我們有一個stopDisplayLink()方法,它既使顯示鏈接無效(如果它是非零)並將其設置為nil - 而不是復制和粘貼此邏輯。
  • 在使顯示鏈接無效之前,我們沒有將paused設置為true ,因為這是多余的。
  • 在檢查displayLink之后,我們使用可選鏈接,例如displayLink?.invalidate() (如果顯示鏈接不是nil則調用invalidate() ,而不是強制解displayLink 雖然力量展開在你給定的情況下可能是“安全的”(因為你正在檢查零) - 在未來的重構時它可能是不安全的,因為你可能會重新構造你的邏輯而不考慮它對力量展開的影響。
  • 我們將elapsed時間鉗制到動畫持續時間,以確保后面的動畫邏輯不會產生超出預期范圍的值。
  • 我們的更新方法displayLinkDidFire(_:) 根據文檔的要求采用 CADisplayLink類型的單個參數。

我意識到這個問題已經有了一個很好的答案,但這是另一種略有不同的方法,它有助於實現與顯示鏈接幀速率無關的平滑動畫。

**(鏈接到本答案底部的演示項目 - 更新:演示項目源代碼現已更新為Swift 4)

對於我的實現,我選擇將顯示鏈接包裝在它自己的類中,並設置一個委托引用,它將使用增量時間(最后一次顯示鏈接調用和當前調用之間的時間)調用,這樣我們就可以再執行一次動畫了順利。

我目前正在使用這種方法在游戲中同時圍繞屏幕制作約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's ready to be used. We simply check if it's 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演示項目

以上是如何有效使用CADisplayLink的最佳示例。 感謝@Fattie和@digitalHound

我無法抗拒使用WKWebView在PdfViewer中使用'digitalHound'添加我對CADisplayLink和DisplayUpdater類的使用。 我的要求是繼續以用戶可選擇的速度自動滾動pdf。

可能這里答案不正確的地方,但我打算在這里展示CADisplayLink的用法。 (對於像我這樣的人,他們可以實現他們的要求。)

//
//  PdfViewController.swift
//

import UIKit
import WebKit

class PdfViewController: UIViewController, DisplayUpdateReceiver {

    @IBOutlet var mySpeedScrollSlider: UISlider!    // UISlider in storyboard

    var displayLinker: DisplayUpdateNotifier?

    var myPdfFileName = ""                          
    var myPdfFolderPath = ""
    var myViewTitle = "Pdf View"
    var myCanAnimate = false
    var mySlowSkip = 0.0

    // 0.125<=slow, 0.25=normal, 0.5=fast, 0.75>=faster
    var cuScrollSpeed = 0.25

    fileprivate var myPdfWKWebView = WKWebView(frame: CGRect.zero)

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        self.title = myViewTitle
        let leftItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(PdfViewController.PdfBackClick))
        navigationItem.leftBarButtonItem = leftItem

        self.view.backgroundColor = UIColor.white.cgColor
        mySpeedScrollSlider.minimumValue = 0.05
        mySpeedScrollSlider.maximumValue = 4.0
        mySpeedScrollSlider.isContinuous = true
        mySpeedScrollSlider.addTarget(self, action: #selector(PdfViewController.updateSlider), for: [.valueChanged]) 
        mySpeedScrollSlider.setValue(Float(cuScrollSpeed), animated: false)
        mySpeedScrollSlider.backgroundColor = UIColor.white.cgColor

        self.configureWebView()
        let folderUrl = URL(fileURLWithPath: myPdfFolderPath)
        let url = URL(fileURLWithPath: myPdfFolderPath + myPdfFileName)
        myPdfWKWebView.loadFileURL(url, allowingReadAccessTo: folderUrl)
    }

    //MARK: - Button Action

    @objc func PdfBackClick()
    {
        _ = self.navigationController?.popViewController(animated: true)
    }

    @objc func updateSlider()
    {
        if ( mySpeedScrollSlider.value <= mySpeedScrollSlider.minimumValue ) {
            myCanAnimate = false
        } else {
            myCanAnimate = true
        }
        cuScrollSpeed = Double(mySpeedScrollSlider.value)
    }

    fileprivate func configureWebView() {
        myPdfWKWebView.frame = view.bounds
        myPdfWKWebView.translatesAutoresizingMaskIntoConstraints = false
        myPdfWKWebView.navigationDelegate = self
        myPdfWKWebView.isMultipleTouchEnabled = true
        myPdfWKWebView.scrollView.alwaysBounceVertical = true
        myPdfWKWebView.layer.backgroundColor = UIColor.red.cgColor //test
        view.addSubview(myPdfWKWebView)
        myPdfWKWebView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor ).isActive = true
        myPdfWKWebView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        myPdfWKWebView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        myPdfWKWebView.bottomAnchor.constraint(equalTo: mySpeedScrollSlider.topAnchor).isActive = true
    }

    //MARK: - DisplayUpdateReceiver delegate

    func displayWillUpdate(deltaTime: CFTimeInterval) {

        guard myCanAnimate == true else {
            return
        }

        var maxSpeed = 0.0

        if cuScrollSpeed < 0.5 {
            if mySlowSkip > 0.25 {
                mySlowSkip = 0.0
            } else {
                mySlowSkip += cuScrollSpeed
                return
            }
            maxSpeed = 0.5
        } else {
            maxSpeed = cuScrollSpeed
        }

        let scrollViewHeight = self.myPdfWKWebView.scrollView.frame.size.height
        let scrollContentSizeHeight = self.myPdfWKWebView.scrollView.contentSize.height
        let scrollOffset = self.myPdfWKWebView.scrollView.contentOffset.y
        let xOffset = self.myPdfWKWebView.scrollView.contentOffset.x

        if (scrollOffset + scrollViewHeight >= scrollContentSizeHeight)
        {
            return
        }

        let newYOffset = CGFloat( max( min( deltaTime , 1 ), maxSpeed ) )
        self.myPdfWKWebView.scrollView.setContentOffset(CGPoint(x: xOffset, y: scrollOffset+newYOffset), animated: false)
    }

}

extension PdfViewController: WKNavigationDelegate {
    // MARK: - WKNavigationDelegate
    public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        //print("didStartProvisionalNavigation")
    }

    public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        //print("didFinish")
        displayLinker = DisplayUpdateNotifier.init(listener: self)
        myCanAnimate = true
    }

    public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        //print("didFailProvisionalNavigation error:\(error)")
    }

    public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        //print("didFail")
    }

}

示例從另一個視圖調用如下。

從Document文件夾加載PDF文件。

func callPdfViewController( theFileName:String, theFileParentPath:String){
    if ( !theFileName.isEmpty && !theFileParentPath.isEmpty ) {
        let pdfViewController = self.storyboard!.instantiateViewController(withIdentifier: "PdfViewController") as? PdfViewController
        pdfViewController?.myPdfFileName = theFileName
        pdfViewController?.myPdfFolderPath = theFileParentPath
        self.navigationController!.pushViewController(pdfViewController!, animated: true)
    } else {
        // Show error.
    }
}

可以“修改”此示例以加載網頁並以用戶選擇的速度自動滾動它們。

問候

桑傑。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM