前言
最新在学习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.根据时间播放图片
- 获取放在本地的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