Swift4.0 GIF图播放

前言

最新在学习swift,想了解swift上gif的实现,Kingfisher已经支持了swift,也通过网上很多学习,给自己做个记录

简介 (百度百科)

GIF(Graphics Interchange Format)的原义是“图像互换格式”,是CompuServe公司在 1987 [1] 年开发的图像文件格式。GIF文件的数据,是一种基于LZW算法的连续色调的无损压缩格式。其压缩率一般在50%左右,它不属于任何应用程序。GIF格式可以存多幅彩色图像,如果把存于一个文件中的多幅图像数据逐幅读出并显示到屏幕上,就可构成一种最简单的动画。
GIF格式自1987年由CompuServe公司引入后,因其体积小、成像相对清晰,特别适合于初期慢速的互联网,而大受欢迎。

使用场景

一般都是图片展示预览

GIF在iOS平台上的几种加载方式

  • 使用DispatchSource创建定时器播放gif图
  • 使用UIImageView直接展示
  • 基于Timer定时器的逐帧动画效果
  • 基于CADisplaylink的逐帧动画效果
  • 使用WebView直接加载gif图

*(此处是学习Kingfisher使用CADisplaylink来实现)

GIF的分解

GIF分解分为几个步骤:
1.将GIF图片转为Data
2.使用ImageIO根据Data获取图片的帧数,时间间隔等信息
3.根据时间播放图片


Swift4.0 GIF图播放_第1张图片
1547030079709.jpg
  • 获取放在本地的gif图路径,并转换成Data
let path = Bundle.main.path(forResource: "timg", ofType: "gif")
let data = loadLocalGIF(from: path)
/// 1.本地读取GIF图片,转化为NSData
func loadLocalGIF(from path: String?) -> Data? {
    guard path != nil else {
        print("文件不存在")
            return nil
        }
        var gifData = Data()
        do {
            gifData = try! Data(contentsOf: URL(fileURLWithPath: path!))
        } catch {
            print(error)
        }
        return gifData
    }
  • 开始解析GIF
func animatedImage(data: Data) {
        // kCGImageSourceShouldCache : 表示是否在存储的时候就解码
        // kCGImageSourceTypeIdentifierHint : 指明source type
        //这里的info是为了显示优化。提前解码,指定类型。
        let info: [String: Any] = [
            kCGImageSourceShouldCache as String: true,
            kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF
        ]
        
        //然后通过CGImageSourceCreateWithData 方法创建一个CGImageSource 对象 。
        guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
            print("创建 CGImageSource 失败")
            return
        }
        
        //获取gif的帧数
        let frameCount = CGImageSourceGetCount(imageSource)
        var gifDuration = 0.0
        var images = [UIImage]()
        
        for i in 0.. 0.011 ? frameDuration.doubleValue : defaultFrameDuration
                
                //计算总时间
                gifDuration += gifFrameDuration
                
                
                //2.图片
                let frameImage = UIImage(cgImage: imageRef, scale: 1.0, orientation: .up)
                images.append(frameImage)
            }
        }
        
        self.gifDuration = gifDuration
        self.gifImages = images
        print("解码成功 gifDuration = \(gifDuration)")
        
    }
  • 然后是用UIImageView加载
imageView.animationImages = self.gifImages
imageView.animationDuration = self.gifDuration ?? 0.1
imageView.animationRepeatCount = 0
imageView.startAnimating()

以上部分就可以看到GIF动画,但是,当我想停止在当前帧的时候,我调用stopAnimating(),imageView什么都不显示.那么这就很尴尬了,所以我们要继续.

GIF加载暂停,继续

下面是对Kingfisher的一个学习

对于gif在某一时刻实现暂停,那么我们就需要自己去处理所有的帧,Kingfisher上使用CADisplayLink实现播放,下面我们来看一下实现

  • Kingfisher上定义一个结构体AnimatedFrame来保存每一帧的信息
struct AnimatedFrame {
    var image: UIImage? //图片
    var duration: TimeInterval //执行时间
}
  • 使用Animator来保存整个GIF图的信息以及播放的状态,一些方法属性
private let maxFrameCount: Int//最大帧数
private let imageSource: CGImageSource //
private let maxRepeatCount: Int // 最大重复次数
private let maxTimeStep: TimeInterval = 1.0 //最大间隔
private var animatedFrames = [AnimatedFrame]() //
private var frameCount = 0 //帧的数量
private var timeSinceLastFrameChange: TimeInterval = 0.0 //距离上一帧改变以来的时间
private var currentRepeatCount: UInt = 0 //当前循环次数

var isFinished: Bool = false  //是否完成
        
        /// 一个动画的总时长
        var loopDuration: TimeInterval = 0
        
        /// 当前帧索引
        var currentFrameIndex = 0
        
        /// 前一帧索引
        var previousFrameIndex = 0
        
        /// 是否最后一帧
        var isLastFrame: Bool {
            return currentFrameIndex == frameCount - 1
        }
        
        // 当前帧的图片
        var currentFrameImage: UIImage? {
            return frameImage(at: currentFrameIndex)
        }
        
        /// 当前帧的执行时间
        var currentFrameDuration: TimeInterval {
            return frameDuration(at: currentFrameIndex)
        }
        
        /// 最大重复次数
        var isReachMaxRepeatCount: Bool {
            if maxRepeatCount == 0 {
                return false
            }else if currentRepeatCount >= maxRepeatCount - 1 {
                return true
            }else {
                return false
            }
        }
        
        
        /// 填充方式
        var contentMode = UIView.ContentMode.scaleToFill
        
        /// 队列
        private lazy var preloadQueue: DispatchQueue = {
            return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
        }()
        
        
        /// 取帧图片
        private func frameImage(at index: Int) -> UIImage? {
            return animatedFrames[safe: index]?.image
        }
        
        /// 某一帧执行时间
        private func frameDuration(at index: Int) -> TimeInterval {
            return animatedFrames[safe: index]?.duration ?? .infinity
        }
        
        /// 准备数据
        func prepareFramesAsynchronously() {
            frameCount = CGImageSourceGetCount(imageSource)
            animatedFrames.reserveCapacity(frameCount)
            preloadQueue.async { [weak self] in
                self?.setupAnimatedFrames()
            }
        }
        
        /// 设置AnimatedFrames
        private func setupAnimatedFrames() {
            resetAnimatedFrames()
            
            var duration: TimeInterval = 0
            
            (0.. maxFrameCount { return }
                animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(image: loadFrame(at: index))
            }
            // 总时间
            self.loopDuration = duration
        }
        
        /// 重置 animatedFrames
        private func resetAnimatedFrames() {
            animatedFrames = []
        }
        
        /// 加载图片
        private func loadFrame(at index: Int) -> UIImage? {
            guard let image = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else {
                return nil
            }
            return UIImage(cgImage: image)
        }
        
        func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) {
            
            timeSinceLastFrameChange += min(maxTimeStep, duration)
            
            if currentFrameDuration > timeSinceLastFrameChange{
                    //不更新
                handler(false)
            }else {
                    //更新
                timeSinceLastFrameChange -= currentFrameDuration
                currentFrameIndex = increment(frameIndex: currentFrameIndex)
                if isLastFrame && isReachMaxRepeatCount {
                    isFinished = true
                }else if currentFrameIndex == 0{
                    currentRepeatCount += 1
                }
                handler(true)
            }
        }
        
        private func increment(frameIndex: Int, by value: Int = 1) -> Int {
            return (frameIndex + value) % frameCount
        }
        
        static public func createImageSource(data: Data) -> CGImageSource?{
            // kCGImageSourceShouldCache : 表示是否在存储的时候就解码
            // kCGImageSourceTypeIdentifierHint : 指明source type
            //这里的info是为了显示优化。提前解码,指定类型。
            let info: [String: Any] = [
                kCGImageSourceShouldCache as String: true,
                kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF
            ]

            guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
                print("creat imageSource error")
                return nil
            }
            return imageSource
        }
        
        
        //init
        init(imageSource source: CGImageSource,
             contentMode mode: UIView.ContentMode,
             size: CGSize,
             framePreloadCount count: Int,
             repeatCount: Int,
             preloadQueue: DispatchQueue) {
            self.imageSource = source
            self.contentMode = mode
            self.size = size
            self.maxFrameCount = count
            self.maxRepeatCount = repeatCount
            self.preloadQueue = preloadQueue
            }
  • 我们获取到imageSource对象
static public func createImageSource(data: Data) -> CGImageSource?{
// kCGImageSourceShouldCache : 表示是否在存储的时候就解码
// kCGImageSourceTypeIdentifierHint : 指明source type
//这里的info是为了显示优化。提前解码,指定类型。
let info: [String: Any] = [
kCGImageSourceShouldCache as String: true,
 kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF
 ]
 guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
         print("creat imageSource error")
         return nil
     }
     return imageSource
 }
  • Animator构造方法
guard let imageSource = Animator.createImageSource(data: gifData) else {
                    return
                }
                animator = nil
                animator = Animator(imageSource: imageSource,
                                    contentMode: contentMode,
                                    size: bounds.size,
                                    framePreloadCount: 100,
                                    repeatCount: repeatCount,
                                    preloadQueue: preloadQueue)
                animator?.prepareFramesAsynchronously()
  • Animator类中解析保存每一帧数据
/// 准备数据
        func prepareFramesAsynchronously() {
            frameCount = CGImageSourceGetCount(imageSource)
            animatedFrames.reserveCapacity(frameCount)
            preloadQueue.async { [weak self] in
                self?.setupAnimatedFrames()
            }
        }
        
        /// 设置AnimatedFrames
        private func setupAnimatedFrames() {
            resetAnimatedFrames()
            
            var duration: TimeInterval = 0
            
            (0.. maxFrameCount { return }
                animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(image: loadFrame(at: index))
            }
            // 总时间
            self.loopDuration = duration
        }
  • AnimateView中重写方法
open override var isAnimating: Bool {
        if displayLinkInitialized {
            return !displayLink.isPaused
        }else {
            return super.isAnimating
        }
    }
    
    open override func startAnimating() {
        guard !isAnimating else {
            return
        }
        if animator?.isReachMaxRepeatCount ?? false {
            return
        }
        
        displayLink.isPaused = false
    }
    
    open override func stopAnimating() {
        super.stopAnimating()
        if displayLinkInitialized {
            displayLink.isPaused = true
        }
    }
    
    open override func display(_ layer: CALayer) {
        if let currentFrame = animator?.currentFrameImage {
            layer.contents = currentFrame.cgImage
        }else {
            layer.contents = image?.cgImage
        }
    }
    
    open override func didMoveToWindow() {
        super.didMoveToWindow()
        didMove()
    }
    
    open override func didMoveToSuperview() {
        super.didMoveToSuperview()
        didMove()
    }

  • CADisplayLink
/// displayLink 为懒加载 避免还没有加载好的时候使用了 造成异常
    private var displayLinkInitialized: Bool = false
    
private lazy var displayLink: CADisplayLink = {
        displayLinkInitialized = true
        let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
        displayLink.add(to: RunLoop.main, forMode: runLoopMode)
        displayLink.isPaused = true
        return displayLink
    }()
  • TargetProxy防止循环引用
//防止循环引用
    private class TargetProxy {
        
        private weak var target: XTAnimatedImageView?
        
        init(target: XTAnimatedImageView) {
            self.target = target
        }
        
        @objc func onScreenUpdate() {
            self.target?.updateFrameIfNeeded()
        }
    }

使用TargetProxy防止循环引用,DisplayLink会去刷新调用onScreenUpdate方法,从而调用AnimateView的updateFrameIfNeeded()方法

/// 更新显示的帧数据
    private func updateFrameIfNeeded() {
        guard let animator = animator else {
            return
        }
        
        guard !animator.isFinished else {
            stopAnimating()
            return
        }
        
        let duration: CFTimeInterval
    
        if displayLink.preferredFramesPerSecond == 0 {
            duration = displayLink.duration
        } else {
            duration = 1.0 / Double(displayLink.preferredFramesPerSecond)
        }
        animator.shouldChangeFrame(with: duration) { (updateFrame) in
            if updateFrame {
                 // 此方法会触发 displayLayer
                self.layer.setNeedsDisplay()
            }
        }
    }

layer.setNeedsDisplay方法调用,回触发display方法

 open override func display(_ layer: CALayer) {
        if let currentFrame = animator?.currentFrameImage {
            layer.contents = currentFrame.cgImage
        }else {
            layer.contents = image?.cgImage
        }
    }

shouldChangeFrame这个方法中,判断当前帧时长是否执行完成

func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) {
            
            timeSinceLastFrameChange += min(maxTimeStep, duration)
            
            if currentFrameDuration > timeSinceLastFrameChange{
                    //不更新
                handler(false)
            }else {
                    //更新
                timeSinceLastFrameChange -= currentFrameDuration
                currentFrameIndex = increment(frameIndex: currentFrameIndex)
                if isLastFrame && isReachMaxRepeatCount {
                    isFinished = true
                }else if currentFrameIndex == 0{
                    currentRepeatCount += 1
                }
                handler(true)
            }
        }
        
        private func increment(frameIndex: Int, by value: Int = 1) -> Int {
            return (frameIndex + value) % frameCount
        }

最后加载本地gif图

//本地gif路径
    var gifFilePath: String? {
        didSet {
            let data = XTGIFAnimatedImage.loadLocalGIF(from: gifFilePath)
            gifData = data
        }
    }
    
    /// 设置数据
    var gifData: Data? {
        didSet {
            if let gifData = gifData {
                guard let imageSource = Animator.createImageSource(data: gifData) else {
                    return
                }
                animator = nil
                animator = Animator(imageSource: imageSource,
                                    contentMode: contentMode,
                                    size: bounds.size,
                                    framePreloadCount: 100,
                                    repeatCount: repeatCount,
                                    preloadQueue: preloadQueue)
                animator?.prepareFramesAsynchronously()
            }
            didMove()
        }
    }

写的乱七八糟,给自己做记录 DEMO

来自
Swift 玩转gif
Kingfisher

你可能感兴趣的:(Swift4.0 GIF图播放)