使用CADisplayLink實現UILabel動畫特效

在開發時,我們有時候會遇到需要定時對UIView進行重繪的需求,進而讓view產生不同的動畫效果。

本文項目

效果圖

typewritter
shine
fade
wave

初探 CADisplayLink

定時對View進行定時重繪可能會第一時間想到使用NSTimer,但是這樣的動畫實現起來是不流暢的,因為在timer所處的runloop中要處理多種不同的輸入,導致timer的最小周期是在50到100毫秒之間,一秒鐘之內最多只能跑20次左右。

但如果我們希望在屏幕上看到流暢的動畫,我們就要維持60幀的刷新頻率,也就意味著每一幀的間隔要在0.016秒左右,NSTimer是無法實現的。所以要用到Core Animation的另一個timer,CADisplayLink

CADisplayLink的頭文件中,我們可以看到它的使用方法跟NSTimer是十分類似的,其同樣也是需要注冊到RunLoop中,但不同于NSTimer的是,它在屏幕需要進行重繪時就會讓RunLoop調用CADisplayLink指定的selector,用于準備下一幀顯示的數據。而NSTimer是需要在上一次RunLoop整個完成之后才會調用制定的selector,所以在調用頻率與上比NSTimer要頻繁得多。

另外和NSTimer不同的是,NSTimer可以指定timeInterval,對應的是selector調用的間隔,但如果NSTimer觸發的時間到了,而RunLoop處于阻塞狀態,其觸發時間就會推遲到下一個RunLoop。而CADisplayLink的timer間隔是不能調整的,固定就是一秒鐘發生60次,不過可以通過設置其frameInterval屬性,設置調用一次selector之間的間隔幀數。另外需要注意的是如果selector執行的代碼超過了frameInterval的持續時間,那么CADisplayLink就會直接忽略這一幀,在下一次的更新時候再接著運行。

配置 RunLoop

在創建CADisplayLink的時候,我們需要指定一個RunLoop和RunLoopMode,通常RunLoop我們都是選擇使用主線程的RunLoop,因為所有UI更新的操作都必須放到主線程來完成,而在模式的選擇就可以用NSDefaultRunLoopMode,但是不能保證動畫平滑的運行,所以就可以用NSRunLoopCommonModes來替代。但是要小心,因為如果動畫在一個高幀率情況下運行,會導致一些別的類似于定時器的任務或者類似于滑動的其他iOS動畫會暫停,直到動畫結束。

private func setup() {
    _displayLink = CADisplayLink(target: self, selector: #selector(update))
    _displayLink?.isPaused = true
    _displayLink?.add(to: RunLoop.main, forMode: .commonModes)
}

實現不同的字符變換動畫

在成功建立CADisplayLink計時器后,就可以著手對字符串進行各類動畫操作了。在這里我們會使用NSAttributedString來實現效果

setupAnimatedText(from labelText: String?)這個方法中,我們需要使用到兩個數組,一個是durationArray,一個是delayArray,通過配置這兩個數組中的數值,我們可以實現對字符串中各個字符的出現時間出現時長的控制。

打字機效果的配置

  • 每個字符出現所需時間相同
  • 下一個字符等待上一個字符出現完成后再出現
  • 通過修改NSAttributedStringKey.baselineOffset調整字符位置
case .typewriter:
    attributedString.addAttribute(.baselineOffset, value: -label.font.lineHeight, range: NSRange(location: 0, length: attributedString.length))
    let displayInterval = duration / TimeInterval(attributedString.length)
    for index in 0..<attributedString.length {
        durationArray.append(displayInterval)
        delayArray.append(TimeInterval(index) * displayInterval)
    }

閃爍效果的配置

  • 每個字符出現所需時間隨機
  • 確保所有字符能夠在duration內均完成出現
  • 修改NSAttributedStringKey.foregroundColor透明度來實現字符的出現效果
case .shine:
    attributedString.addAttribute(.foregroundColor, value: label.textColor.withAlphaComponent(0), range: NSRange(location: 0, length: attributedString.length))
    for index in 0..<attributedString.length {
        delayArray.append(TimeInterval(arc4random_uniform(UInt32(duration) / 2 * 100) / 100))
        let remain = duration - Double(delayArray[index])
        durationArray.append(TimeInterval(arc4random_uniform(UInt32(remain) * 100) / 100))
    }

漸現效果的配置

  • 每個字符出現所需時間漸減
  • 修改NSAttributedStringKey.foregroundColor透明度來實現字符的出現效果
case .fade:
    attributedString.addAttribute(.foregroundColor, value: label.textColor.withAlphaComponent(0), range: NSRange(location: 0, length: attributedString.length))
    let displayInterval = duration / TimeInterval(attributedString.length)
    for index in 0..<attributedString.length  {
        delayArray.append(TimeInterval(index) * displayInterval)
        durationArray.append(duration - delayArray[index])
    }

完善每一幀的字符串更新效果

接下來就需要完善剛才在CADisplayLink中配置的update方法了,在這個方法中我們會根據我們剛才配置的兩個數組中的相關數據對字符串進行變換。

核心代碼

  • 通過開始時間當前時間獲取動畫進度
  • 根據字符位置對應duationArraydelayArray中的數據
  • 根據durationArraydelayArray中的數據計算當前字符的顯示進度
var percent = (CGFloat(currentTime - beginTime) - CGFloat(delayArray[index])) / CGFloat(durationArray[index])
percent = fmax(0.0, percent)
percent = fmin(1.0, percent)
attributedString.addAttribute(.baselineOffset, value: (percent - 1) * label!.font.lineHeight, range: range)

隨后便可以將處理完的NSAttributedString返回給label進行更新

番外:利用正弦函數實現波紋進度

波紋路徑

首先介紹一下正弦函數:y = A * sin(ax + b)

  • 在 x 軸方向平移 b 個單位(左加右減)
  • 橫坐標伸長(0 < a < 1)或者縮短(a > 1) 1/a 倍
  • 縱坐標伸長(A > 1)或者縮短(0 < A < 1)A 倍

在簡單了解了這些知識后,我們回到wavePath()方法中,在這個方法我們使用正弦函數來繪制一段UIBezierPath

let originY = (label.bounds.size.height + label.font.lineHeight) / 2
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: _waveHeight!))
var yPosition = 0.0
for xPosition in 0..<Int(label.bounds.size.width) {
    yPosition = _zoom! * sin(Double(xPosition) / 180.0 * Double.pi - 4 * _translate! / Double.pi) * 5 + _waveHeight!
    path.addLine(to: CGPoint(x: Double(xPosition), y: yPosition))
}
path.addLine(to: CGPoint(x: label.bounds.size.width, y: originY))
path.addLine(to: CGPoint(x: 0, y: originY))
path.addLine(to: CGPoint(x: 0, y: _waveHeight!))
path.close()

波紋高度與動畫的更新

  • 隨著進度高度不斷升高
  • 隨著進度波紋不斷波動

CADisplayLink注冊的update的方法中,我們對承載了波紋路徑的Layer進行更新

_waveHeight! -= duration / Double(label!.font.lineHeight)
_translate! += 0.1
if !_reverse {
    _zoom! += 0.02
    if _zoom! >= 1.2 {
        _reverse = true
    }
} else {
    _zoom! -= 0.02
    if _zoom! <= 1.0 {
        _reverse = false
    }
}
shapeLayer.path = wavePath()

結語

以上就是我對CADisplayLink的一些運用,其實它的使用方法還有很多,可以利用它實現更多更復雜而精美的動畫,同時希望各位如果有更好的改進也能與我分享。

如果你喜歡這個項目,歡迎到GitHub上給我一個star。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容