在開發時,我們有時候會遇到需要定時對UIView進行重繪的需求,進而讓view產生不同的動畫效果。
本文項目
效果圖
初探 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
方法了,在這個方法中我們會根據我們剛才配置的兩個數組中的相關數據對字符串進行變換。
核心代碼
- 通過開始時間與當前時間獲取動畫進度
- 根據字符位置對應
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)
- 在 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。