Swift 玩转gif
众所周知,iOS默认是不支持gif类型图片的显示的,但是我们项目中常常是需要显示gif为动态图片。那肿么办?第三方库?是的 ,很多第三方都支持gif , 如果一直只停留在用第三方上,技术难有提高。上版本的 Kingfisher 也支持gif ,研究了一番,也在网上搜索了一番,稍微了解了下iOS实现gif的显示,在此略做记录。
本篇文章要实现的效果如图:
可以开始和暂停gif的播放,滑动时停止播放,这个简书也是这么做得,好多app为了滑动时顺畅,停止了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动起来了。
原来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
Swift 玩转gif的更多相关文章
- Swift 玩转 3D Touch 之 Peek & Pop
什么是3D Touch 3D Touch 是iOS9之后专为 iPhone6s 机型加入的新特性,这一新技术移植于 Mac Book 上的 ForceTouch 更准确地说应该是 ForceTouch ...
- iOS开发——UI篇Swift篇&玩转UItableView(四)自定义&封装
UItableView自定义&封装 一:Model class AppsModel: NSObject { //定义模型的三个属性 var imageName:String! //图片名称 v ...
- iOS开发——UI篇Swift篇&玩转UItableView(三)分组功能
UItableView分组功能 class UITableViewControllerGroup: UIViewController, UITableViewDataSource, UITableVi ...
- iOS开发——UI篇Swift篇&玩转UItableView(二)高级功能
UItableView高级功能 class UITableViewControllerAF: UIViewController, UITableViewDataSource, UITableViewD ...
- iOS开发——UI篇Swift篇&玩转UItableView(一)基本使用
UItableView基本使用 class ListViewController: UIViewController , UITableViewDataSource, UITableViewDeleg ...
- swift开发:试玩 Apple 站点的 playground
https://developer.apple.com/library/prerelease/ios/documentation/swift/conceptual/swift_programming_ ...
- Swift变量名的一种玩法
大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 是的,Swift的变量名可以用任何合法的Unicode字符,这 ...
- 一步一步学习Swift之(四)玩转UIWebView
实现原理: 1.通过UIWebView的stringByEvaluatingJavaScriptFromString方法来触发脚本 2.通过自定义连接来触发oc代码 实现过程 @IBOutlet we ...
- 【swift学习笔记】四.swift使用Alamofire和swiftyJson
Alamofire是AFNetworking的swift版本,功能灰常强大. github:https://github.com/Alamofire/Alamofire SwiftyJSON是操作js ...
随机推荐
- bzoj 3698 XWW的难题(有源汇的上下界最大流)
[题意] 对每个格子确定上下取整,使得满足1.A[n][n]=0 2.每行列前n-1个之和为第n个 3.格子之和尽量大. [思路] 设格子(i,j)上下取整分别为up(i,j)down(i,j),构图 ...
- 区间DP+next求循环节 uva 6876
// 区间DP+next求循环节 uva 6876 // 题意:化简字符串 并表示出来 // 思路:dp[i][j]表示 i到j的最小长度 // 分成两部分 再求一个循环节 #include < ...
- Lucene学习笔记:一,全文检索的基本原理
一.总论 根据http://lucene.apache.org/java/docs/index.html定义: Lucene是一个高效的,基于Java的全文检索库. 所以在了解Lucene之前要费一番 ...
- SQL Server 2000的并发连接数是多少
开始->管理工具->性能(或者是运行里面输入 mmc)然后通过 添加计数器添加 SQL 的常用统计(MSSQL General Statistics) 然后在下面列出的项目里面选择 用户连 ...
- java的动态代理机制
前几天看到java的动态代理机制,不知道是啥玩意,然后看了看.死活不知道 invoke(Object proxy, Method m, Object[] args)种的proxy是个什么东西,放在这里 ...
- NodeJs 开源
iwebpp.io - 运行P2P Node.js web 服务,穿透防火墙,NAT https://github.com/InstantWebP2P/iwebpp.io pm 是一个轻量级的Node ...
- gratitute
韩信帮刘邦夺得天下,最终又得到了什么?姑且不问当初刘邦拜将是何心态?虽然他的所拜之相并不是的那边从芒砀山带下来的哥们或是在沛县时候一起打混的兄弟? 韩信在汉军营得以重用,在项羽处屈其才,此真正的原因在 ...
- POJ2763-Housewife Wind(树链剖分)
也是入门题,和上一题不一样的是权值在边上. 调了半天后来发现线段树写错了,build的时候没有pushup...蠢哭了好吗.... 做题还是不专心,太慢辣.. #include <algorit ...
- UVALive 6910 Cutting Tree(离线逆序并查集)
[题目]:(地址:) http://acm.hust.edu.cn/vjudge/contest/view.action?cid=97671#problem/E [题意]: 给出多棵树和两类操作:操作 ...
- 判断滑动方向UITableView
CGFloat lastContentOffset; //ScoreView 滑动位置 -(void)scrollViewWillBeginDragging:(UIScrollView*)scrol ...