Swift 玩轉gif
gif study
眾所周知,iOS默認是不支持gif類型圖片的顯示的,但是我們項目中常常是需要顯示gif為動態圖片。那腫么辦?第三方庫?是的 ,很多第三方都支持gif , 如果一直只停留在用第三方上,技術難有提高。上版本的 Kingfisher 也支持gif ,研究了一番,也在網上搜索了一番,稍微了解了下iOS實現gif的顯示,在此略做記錄。
本篇文章要實現的效果如圖:
gif顯示效果
分解gif幀進行顯示
我們一般從網絡上下載的gif圖片其實是將很多幀靜態圖片循環播放產生的動態效果,那么在iOS中,如果我們想要顯示動態圖,同樣需要先把gif資源解析為一陣一陣的 UIImage 然后設定間隔時長,不斷播放即可。思路是不是很簡單呢?那么看看如何實現。
分幾個步驟:
-
將gif圖片轉為 NSData 。
-
根據 NSData 獲取 CGImageSource 對象
-
獲取幀數
-
根據幀數獲取每一幀對應的 UIImage 對象和時間間隔
-
循環播放
首先我們需要引入 import ImageIO , 提供了很多對圖片操作的函數。
這里我們從網上down了一個gif的圖片,其實下載也是一樣的 ,我們需要的是 NSData 類型的數據,用 NSURLSession 下載也可以得到 NSData 類型的數據,這里下載的數據如何判斷是否為gif呢?
Kingfisher 庫中給出了解決方案,每種格式的圖片前面幾位都是固定的。所以只需要對比就能判斷出類型,這里給出 Kingfisher 判斷類型的代碼。
private let pngHeader: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
private let jpgHeaderSOI: [UInt8] = [0xFF, 0xD8]
private let jpgHeaderIF: [UInt8] = [0xFF]
private let gifHeader: [UInt8] = [0x47, 0x49, 0x46]
enum ImageFormat {
case Unknown, PNG, JPEG, GIF
}
extension NSData {
var kf_imageFormat: ImageFormat {
var buffer = [UInt8](count: 8, repeatedValue: 0)
self.getBytes(&buffer, length: 8)
if buffer == pngHeader {
return .PNG
} else if buffer[0] == jpgHeaderSOI[0] &&
buffer[1] == jpgHeaderSOI[1] &&
buffer[2] == jpgHeaderIF[0]
{
return .JPEG
} else if buffer[0] == gifHeader[0] &&
buffer[1] == gifHeader[1] &&
buffer[2] == gifHeader[2]
{
return .GIF
}
return .Unknown
}
}
有了這個擴展判斷起來就方便很多了。
為了使demo簡單,我們直接將gif放在本地沙盒。下載好直接拖進項目就OK了。
這樣就可以很容易的得到 NSData 類型的數據
let path = NSBundle.mainBundle().pathForResource("xxx", ofType: "gif")
let data = NSData(contentsOfFile: path!)
第一步已經完成啦。
然后通過 CGImageSourceCreateWithData 方法創建一個 CGImageSource 對象 。
// kCGImageSourceShouldCache : 表示是否在存儲的時候就解碼
// kCGImageSourceTypeIdentifierHint : 指明source type
let options: NSDictionary = [kCGImageSourceShouldCache as String: NSNumber(bool: true), kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF]
guard let imageSource = CGImageSourceCreateWithData(data, options) else {
return
}
這里的options是為了顯示優化。提前解碼,指定類型。
拿到 CGImageSource 對象就可以為所欲為了。
// 獲取gif幀數
let frameCount = CGImageSourceGetCount(imageSource)
var images = [UIImage]()
var gifDuration = 0.0
for i in 0 ..< frameCount {
// 獲取對應幀的 CGImage
guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, options) else {
return
}
if frameCount == 1 {
// 單幀
gifDuration = Double.infinity
} else{
// gif 動畫
// 獲取到 gif每幀時間間隔
guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) , gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
frameDuration = (gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber) else
{
return
}
// print(frameDuration)
gifDuration += frameDuration.doubleValue
// 獲取幀的img
let image = UIImage(CGImage: imageRef , scale: UIScreen.mainScreen().scale , orientation: UIImageOrientation.Up)
// 添加到數組
images.append(image)
}
}
先獲取幀數,然后循環根據幀數獲取對應的圖片,然后獲取沒幀間隔時間。累加時間間隔得到總共的時間,把圖片存在一個圖片數組中。
有了這些參數,我們就可以播放gif了。
界面上隨便拖出來一個 UIImageView 然后給以下屬性賦值即可。
imgV.contentMode = .ScaleAspectFit
imgV.animationImages = images
imgV.animationDuration = gifDuration
imgV.animationRepeatCount = 0 // 無限循環
imgV.startAnimating()
運行項目,發現gif動起來了。
happy...
原來gif也沒那么難,哈哈... ...
但是這樣你添加一個開始和暫停的按鈕
@IBAction func start(sender: AnyObject) {
if !imgV.isAnimating() {
imgV.startAnimating()
}
}
@IBAction func stop(sender: AnyObject) {
if imgV.isAnimating() {
imgV.stopAnimating()
}
}
你會發現,暫停時白板,什么圖都沒有,而且滾動的時候也不會暫停。。。
吐血...
這只是個開始,后面的路還很長,坐好繼續。
處理gif的暫停、播放 滑動暫停等
以下部分基本上算是對 Kingfisher 的一個理解,我們繼續。
簡單說下思路,要實現暫停在某幀,滑動暫停某幀這個就不能用 UIImageView 的 startAnimating 直接操作了,需要我們自己處理幀和動畫,動畫在 Kingfisher 中使用 CADisplayLink 處理的,寫了一個 UIImageView 的子類 AnimatedImageView ,重寫了 startAnimating 、 stopAnimating 等方法。關于 CADisplayLink 不熟悉的,看這篇文章 -CADisplayLink , 需要滑動暫停就把 CADisplayLink 加到 NSDefaultRunLoopMode 模式的runloop下。 關于對幀的處理單獨寫了一個 Animator . 下面來看看具體實現。
Animator 類處理幀
首先定義一個結構體,里面就有兩個屬性 UIImage 圖像 和 NSTimeInterval 幀之間時間間隔。
struct AnimatedFrame {
var image: UIImage?
let duration: NSTimeInterval
static func null() -> AnimatedFrame {
return AnimatedFrame(image: .None, duration: 0.0)
}
}
接著就可以創建一個 Animator 并定義一些需要用的屬性
class Animator{
private let maxFrameCount: Int = 100 // 最大幀數
private var imageSource:CGImageSource! // imageSource 處理幀相關操作
private var animatedFrames = [AnimatedFrame]() //
private var frameCount = 0 // 幀的數量
private var currentFrameIndex = 0 // 當前幀下標
private var currentPreloadIndex = 0 // 當前預緩存幀的下標
private var timeSinceLastFrameChange: NSTimeInterval = 0.0 // 距離上一幀改變的時間
/// 循環次數
private var loopCount = 0
/// 做大間隔
private let maxTimeStep: NSTimeInterval = 1.0
}
然后是一個隊數據操作的方法,因為Kingfiher是處理網絡圖片的,所以我這邊處理方式略不同
/**
根據data創建 CGImageSource
- parameter data: gif data
*/
func createImageSource(data:NSData){
let options: NSDictionary = [kCGImageSourceShouldCache as String: NSNumber(bool: true), kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF]
imageSource = CGImageSourceCreateWithData(data, options)
}
這個方法就是前面的根據 NSData 獲取 CGImageSource 對象,以備后用。
然后寫一個將每一幀轉換為我們剛定義的結構體 AnimatedFrame 對象
/// 準備某幀 的 frame
func prepareFrame(index: Int) -> AnimatedFrame {
// 獲取對應幀的 CGImage
guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index , nil) else {
return AnimatedFrame.null()
}
// 獲取到 gif每幀時間間隔
guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index , nil) , gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
frameDuration = (gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber) else
{
return AnimatedFrame.null()
}
let image = UIImage(CGImage: imageRef , scale: UIScreen.mainScreen().scale , orientation: UIImageOrientation.Up)
return AnimatedFrame(image: image, duration: Double(frameDuration) ?? 0.0)
}
就是根據 imageSource 獲取 CGImage 再轉為 UIImage , 然后獲取幀間隔時間,構建結構體。 很easy 。沒啥說的。
下面還需要一個預備所有幀的方法
/**
預備所有frames
*/
func prepareFrames() {
frameCount = CGImageSourceGetCount(imageSource)
if let properties = CGImageSourceCopyProperties(imageSource, nil),
gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
loopCount = gifInfo[kCGImagePropertyGIFLoopCount as String] as? Int {
self.loopCount = loopCount
}
// 總共幀數
let frameToProcess = min(frameCount, maxFrameCount)
animatedFrames.reserveCapacity(frameToProcess)
// 相當于累加
animatedFrames = (0..<frameToProcess).reduce([]) { $0 + pure(prepareFrame($1))}
// 上面相當于這個
// for i in 0..<frameToProcess {
// animatedFrames.append(prepareFrame(i))
// }
}
這里其實就是得到總幀數然后給 animatedFrames 賦值, Kingfisher 這里使用了readuce,累加的方式 pure 方法是將一個值轉成一個單值數組。
private func pure<T>(value: T) -> [T] {
return [value]
}
根據下表取幀
/**
根據下標獲取幀
*/
func frameAtIndex(index: Int) -> UIImage? {
return animatedFrames[index].image
}
當前幀和contentMode屬性
var currentFrame: UIImage? {
return frameAtIndex(currentFrameIndex)
}
var contentMode: UIViewContentMode = .ScaleToFill
AnimatedImageView-可以播放gif的ImageView
基本成型,還差一個更新當前幀的方法,暫時不處理,先看去用實現一個繼承自 UIImageView 的 AnimatedImageView 并聲明幾個屬性。
public class AnimatedImageView : UIImageView {
/// 是否自動播放
public var autoPlayAnimatedImage = true
/// `Animator` 對象 將幀和指定圖片存儲內存中
private var animator: Animator?
/// displayLink 為懶加載 避免還沒有加載好的時候使用了 造成異常
private var displayLinkInitialized: Bool = false
}
這里利用 CADisplayLink 不斷執行某個方法,等達到幀之間的間隔時間的時候就去更新 UIImageView 的 layer 的 contens 屬性。這個屬性需要一個 CGImage 的對象。
為了防止 AnimatedImageView 和 CADisplayLink 之間的循環引用,Kingfisher在 AnimatedImageView 內部寫了一個代理類。
/// 防止循環引用
class TargetProxy {
private weak var target: AnimatedImageView?
init(target: AnimatedImageView) {
self.target = target
}
@objc func onScreenUpdate() {
target?.updateFrame()
}
}
就是通過 TargetProxy 來調用 AnimatedImageView 中的 updateFrame 方法,大家可以先寫一個空方法。
然后創建一個 CADisplayLink 對象,這里使用懶加載。
private lazy var displayLink: CADisplayLink = {
self.displayLinkInitialized = true
let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: self.runLoopMode)
displayLink.paused = true
return displayLink
}()
用這個 self.displayLinkInitialized 標志 CADisplayLink 已經加載,然后用代理就調用自己的 updateFrame() 方法
在添加個指定RunLoopMode的屬性
// NSRunLoopCommonModes
public var runLoopMode = NSDefaultRunLoopMode {
willSet {
if runLoopMode == newValue {
return
} else {
stopAnimating()
displayLink.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: runLoopMode)
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: newValue)
startAnimating()
}
}
}
Kingfisher 默認是 NSRunLoopCommonModes 滑動不暫停,我這邊換成 NSDefaultRunLoopMode 滑動暫停 。
NSRunLoopCommonModes 包含兩個模式 UITrackingRunLoopMode 和 NSDefaultRunLoopMode , 其中 UITrackingRunLoopMode 是滑動時候的模式
,如果只在 NSDefaultRunLoopMode 模式下,那滑動模式就不會執行 CADisplayLink 的方法, NSTimer 也可以指定 模式。非本篇重點 ,這里就不細說了
kingfisher 是重寫了 image 屬性進行 Animator 的初始化和重置的 , 這里為了demo的easy 我們給 AnimatedImageView 新增一個屬性,叫 gifData.
public var gifData:NSData?{
didSet{
if let gifData = gifData {
animator = nil
animator = Animator()
animator?.createImageSource(gifData)
animator?.prepareFrames()
didMove()
setNeedsDisplay()
layer.setNeedsDisplay()
}
}
}
創建 Animator 對象 ,緩存幀。 這里didMove() 方法是處理自動播放的
private func didMove() {
if autoPlayAnimatedImage && animator != nil {
if let _ = superview, _ = window {
startAnimating()
} else {
stopAnimating()
}
}
}
后面會重寫 startAnimating 和 stopAnimating .
先來看 CADisplayLink 每次調用的方法 updateFrame() , 這里默認是每秒60次 , 根據屏幕刷新頻率。
要實現 updateFrame() 放法首先要在, Animator 中添加一個更新當前幀的方法。上面提到的,現在可以來寫了。
func updateCurrentFrame(duration: CFTimeInterval) -> Bool {
// 計算距離上一幀 改變的時間 每次進來都累加 直到frameDuration <= timeSinceLastFrameChange 時候才繼續走下去
timeSinceLastFrameChange += min(maxTimeStep, duration)
guard let frameDuration = animatedFrames[safe: currentFrameIndex]?.duration where frameDuration <= timeSinceLastFrameChange else {
return false
}
// 減掉 我們每幀間隔時間
timeSinceLastFrameChange -= frameDuration
let lastFrameIndex = currentFrameIndex
currentFrameIndex += 1 // 一直累加
// 這里取了余數
currentFrameIndex = currentFrameIndex % animatedFrames.count
if animatedFrames.count < frameCount {
animatedFrames[lastFrameIndex] = prepareFrame(currentPreloadIndex)
currentPreloadIndex += 1
currentPreloadIndex = currentPreloadIndex % frameCount
}
return true
}
傳入的 duration 是 displayLink.duration 默認是 1/60 秒,這里先對每次的 duration 進行累加,直到我們的幀間隔時間小于等于它了 才去獲取當前幀和增加下標,返回true , 否則一直返回false
然后 AnimatedImageView 中的 updateFrame 方法就是調用那個方法,直到它返回true才進行處理,這里就是調用了 layer.setNeedsDisplay()
private func updateFrame() {
if animator?.updateCurrentFrame(displayLink.duration) ?? false {
// 此方法會觸發 displayLayer
layer.setNeedsDisplay()
}
}
layer.setNeedsDisplay() 會觸發 displayLayer 方法,我們只要重寫這個方法,就能處理每幀的顯示了。
override public func displayLayer(layer: CALayer) {
if let currentFrame = animator?.currentFrame {
layer.contents = currentFrame.CGImage
} else {
layer.contents = image?.CGImage
}
}
搞了這么多,終于到顯示了,不容易呀。。。
這里重寫了幾個方法,都去調用了didMove
override public func didMoveToWindow() {
super.didMoveToWindow()
didMove()
}
override public func didMoveToSuperview() {
super.didMoveToSuperview()
didMove()
}
這里gif的暫停是利用了 CADisplayLink 的 paused 屬性控制的
override public func isAnimating() -> Bool {
if displayLinkInitialized {
return !displayLink.paused
} else {
return super.isAnimating()
}
}
/// Starts the animation.
override public func startAnimating() {
if self.isAnimating() {
return
} else {
displayLink.paused = false
}
}
/// Stops the animation.
override public func stopAnimating() {
super.stopAnimating()
if displayLinkInitialized {
displayLink.paused = true
}
}
這里 displayLinkInitialized 判斷 CADisplayLink 是否加載好了。
最后記得在對象銷毀的時候吧displaylink也停掉
deinit {
if displayLinkInitialized {
displayLink.invalidate()
}
}
至此,所有基本功能已經全部OK了,使用也很簡單。
let path = NSBundle.mainBundle().pathForResource("xxx", ofType: "gif")
let data = NSData(contentsOfFile: path!)
imgV.gifData = data
默認是自動播放,可以手動設置。
來自:http://www.jianshu.com/p/b60a06bdb375