zoukankan      html  css  js  c++  java
  • Swift 玩转gif

    众所周知,iOS默认是不支持gif类型图片的显示的,但是我们项目中常常是需要显示gif为动态图片。那肿么办?第三方库?是的 ,很多第三方都支持gif , 如果一直只停留在用第三方上,技术难有提高。上版本的 Kingfisher 也支持gif ,研究了一番,也在网上搜索了一番,稍微了解了下iOS实现gif的显示,在此略做记录。

    本篇文章要实现的效果如图:

     

    可以开始和暂停gif的播放,滑动时停止播放,这个简书也是这么做得,好多app为了滑动时顺畅,停止了gif。

    下面要进入正文啦!

    期待…

    分解gif帧进行显示

    我们一般从网络上下载的gif图片其实是将很多帧静态图片循环播放产生的动态效果,那么在iOS中,如果我们想要显示动态图,同样需要先把gif资源解析为一阵一阵的UIImage然后设定间隔时长,不断播放即可。思路是不是很简单呢?那么看看如何实现。

    分几个步骤:

    1. 将gif图片转为NSData。

    2. 根据NSData获取CGImageSource对象

    3. 获取帧数

    4. 根据帧数获取每一帧对应的UIImage对象和时间间隔

    5. 循环播放

    首先我们需要引入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动起来了。

    原来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..

    这里其实就是得到总帧数然后给animatedFrames赋值,Kingfisher这里使用了readuce,累加的方式pure 方法是将一个值转成一个单值数组。

    private func pure(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

    默认是自动播放,可以手动设置。

    文章比较长,可能描述的不是很到位,有啥不清楚可以留言交流。

    github地址:https://github.com/smalldu/ImageDemo

  • 相关阅读:
    app被Rejected 的各种原因翻译
    UIView Border color
    Centos7下安装docker
    利用Yum彻底移除docker
    Docker删除全部镜像和容器
    【转】哈哈笑一笑
    【转】KAFKA分布式消息系统
    java读取properties文件
    java反序列化php序列化的对象
    Java 1.7.0_06中String类内部实现的一些变化【转】
  • 原文地址:https://www.cnblogs.com/fengmin/p/5880510.html
Copyright © 2011-2022 走看看