在開發時,咱們有時候會遇到須要定時對UIView進行重繪的需求,進而讓view產生不一樣的動畫效果。html
本文項目ios
定時對View進行定時重繪可能會第一時間想到使用NSTimer
,可是這樣的動畫實現起來是不流暢的,由於在timer所處的runloop
中要處理多種不一樣的輸入,致使timer的最小週期是在50到100毫秒之間,一秒鐘以內最多隻能跑20次左右。git
但若是咱們但願在屏幕上看到流暢的動畫,咱們就要維持60幀的刷新頻率,也就意味着每一幀的間隔要在0.016秒左右,NSTimer
是沒法實現的。因此要用到Core Animation
的另外一個timer,CADisplayLink
。github
在CADisplayLink
的頭文件中,咱們能夠看到它的使用方法跟NSTimer
是十分相似的,其一樣也是須要註冊到RunLoop中,但不一樣於NSTimer
的是,它在屏幕須要進行重繪時就會讓RunLoop調用CADisplayLink
指定的selector,用於準備下一幀顯示的數據。而NSTimer
是須要在上一次RunLoop整個完成以後纔會調用制定的selector,因此在調用頻率與上比NSTimer
要頻繁得多。swift
另外和NSTimer
不一樣的是,NSTimer
能夠指定timeInterval
,對應的是selector調用的間隔,但若是NSTimer
觸發的時間到了,而RunLoop處於阻塞狀態,其觸發時間就會推遲到下一個RunLoop。而CADisplayLink
的timer間隔是不能調整的,固定就是一秒鐘發生60次,不過能夠經過設置其frameInterval
屬性,設置調用一次selector之間的間隔幀數。另外須要注意的是若是selector執行的代碼超過了frameInterval
的持續時間,那麼CADisplayLink
就會直接忽略這一幀,在下一次的更新時候再接着運行。數組
在建立CADisplayLink的時候,咱們須要指定一個RunLoop和RunLoopMode
,一般RunLoop咱們都是選擇使用主線程的RunLoop,由於全部UI更新的操做都必須放到主線程來完成,而在模式的選擇就能夠用NSDefaultRunLoopMode
,可是不能保證動畫平滑的運行,因此就能夠用NSRunLoopCommonModes
來替代。可是要當心,由於若是動畫在一個高幀率狀況下運行,會致使一些別的相似於定時器的任務或者相似於滑動的其餘iOS動畫會暫停,直到動畫結束。app
private func setup() {
_displayLink = CADisplayLink(target: self, selector: #selector(update))
_displayLink?.isPaused = true
_displayLink?.add(to: RunLoop.main, forMode: .commonModes)
}
複製代碼
在成功創建CADisplayLink
計時器後,就能夠着手對字符串進行各種動畫操做了。在這裏咱們會使用NSAttributedString
來實現效果dom
在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
方法了,在這個方法中咱們會根據咱們剛纔配置的兩個數組中的相關數據對字符串進行變換。oop
duationArray
與delayArray
中的數據durationArray
與delayArray
中的數據計算當前字符的顯示進度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)
在簡單瞭解了這些知識後,咱們回到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。