大家都知道如果想讓UIImageView顯示動態(tài)圖片,可以設(shè)置animationImages賦值一個圖片數(shù)組,然后設(shè)置一下動畫時間橱健,再開啟動畫∩沉總感覺使用很麻煩拘荡,而且如果是一個gif格式或者其他格式的動態(tài)圖,直接就無法使用了撬陵。后來在github上發(fā)現(xiàn)了YYImage,發(fā)現(xiàn)使用起來很方便珊皿,但同時也很好奇是怎么做到的,現(xiàn)在就以YYAnimatedImageView為主介紹一下這個庫是怎么實(shí)現(xiàn)顯示動態(tài)圖的袱结。廢話不多說亮隙,上圖
先寫兩句代碼,運(yùn)行一下垢夹,然后打斷點(diǎn)進(jìn)去看看是怎么加載的
YYAnimatedImageView* animatedView = [[YYAnimatedImageView alloc] init];
animatedView.frame=CGRectMake(0,0,200,150);
animatedView.center=self.view.center;
[self.view addSubview:animatedView];
YYImage* image = [YYImage imageNamed:@"test.gif"];
animatedView.image= image;
這樣就能顯示動態(tài)圖了溢吻。
- 首先從YYImage說起
A YYImage object is a high-level way to display animated image data
作者繼承UIImage類寫了YYImage,他自己的介紹是這是可以高效的展示動態(tài)圖的類,我們從imageNamed:方法看起促王,這個方法主要是去遍歷工程文件中是否有匹配的文件犀盟,如果找到路徑然后直接獲取圖片的二進(jìn)制文件,YYImage重寫了initWithData:scale:方法蝇狼,在這個里面使用了YYImageDecoder對圖片進(jìn)行界面阅畴,這就是YYImage為什么比較快的原因了
- 從
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:datascale:scale]進(jìn)入YYImagecoder
去看看是怎么處理的
根據(jù)方法調(diào)用和參數(shù)傳遞,來到如上圖所示方法迅耘,是yyImageCoder第一個做實(shí)事的方法贱枣,YYImageDetectType先得到這個圖片的格式(比如PNG,JPEG,GIF等),具體怎么得出來的可以點(diǎn)進(jìn)去仔細(xì)看看颤专,大概就是獲得這個二進(jìn)制文件的前16字節(jié)纽哥,然后進(jìn)行匹配,得到圖片的格式栖秕,得到了圖片格式之后進(jìn)入下一步
[self _updateSource]
在這里作者對webp和apng格式的動畫進(jìn)行了專門的解碼優(yōu)化所以進(jìn)入不同方法春塌,總之這個方法就是對不同格式的圖片進(jìn)行解碼路由,我們點(diǎn)進(jìn)去_updateSourceImageIO看看怎么做的簇捍,看下主要代碼(去除了一些條件判斷)
//使用ImageIO框架去獲得圖片
//使用CGImageSourceCreateWithData獲得圖片類型是CGImageSourceRef
_source=CGImageSourceCreateWithData((__bridgeCFDataRef)_data,NULL);
//這里frameCount代表圖片的數(shù)量只壳,比如GIF其實(shí)就是一組圖片
//下面是一些不同類型的判斷
_frameCount = CGImageSourceGetCount(_source);
if (_type == YYImageTypeGIF) {
//這字典打印出來是
//FileSize = 487202;
//"{GIF}" = {
// HasGlobalColorMap = 1;
// LoopCount = 0;
//};
// loopCount = 0 表示會無線循環(huán)當(dāng)前的gif
CFDictionaryRef properties = CGImageSourceCopyProperties(_source, NULL);
if (properties) {
CFTypeRef loop = CFDictionaryGetValue(properties, kCGImagePropertyGIFLoopCount);
if (loop) CFNumberGetValue(loop, kCFNumberNSIntegerType, &_loopCount);
CFRelease(properties);
}
}
//建立一個數(shù)組,把每個圖片的索引暑塑,延遲時間等封裝成_YYImageDecoderFrame類
//加入到數(shù)組中
NSMutableArray *frames = [NSMutableArray new];
for (NSUInteger i = 0; i < _frameCount; i++) {
_YYImageDecoderFrame *frame = [_YYImageDecoderFrame new];
frame.index = i;
frame.blendFromIndex = i;
frame.hasAlpha = YES;
frame.isFullSize = YES;
[frames addObject:frame];
//得到每一幀圖片的屬性
//包括圖片的 寬 高 延遲時間 顏色空間等
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_source, i, NULL);
if (properties) {
NSTimeInterval duration = 0;
NSInteger orientationValue = 0, width = 0, height = 0;
CFTypeRef value = NULL;
value = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &width);
value = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &height);
if (_type == YYImageTypeGIF) {
CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
//此處打斷點(diǎn) 輸出
// {
// DelayTime = "0.07";
// UnclampedDelayTime = "0.07";
// }
//表示每一幀圖片的延遲時間吼句,也就是需要顯示的時間
if (gif) {
value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);
if (!value) {
value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);
}
if (value) CFNumberGetValue(value, kCFNumberDoubleType, &duration);
}
}
//對動畫中需要到的關(guān)鍵屬性進(jìn)行賦值
frame.width = width;
frame.height = height;
frame.duration = duration;
然后把每一幀圖片加入到frames數(shù)組中,這基本上是YYImageCoder
完成的大部分工作了
- 我們再回到Y(jié)YImage中
YYImageCoder
還是之前那個initWithData:scale
方法梯投,我加了注釋命辖,看一下
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
UIImage *image = frame.image;
if (!image) return nil;
//這里返回的是第一幀的圖片
self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
if (!self) return nil;
_animatedImageType = decoder.type;
//當(dāng)frameCount >1 也就是 當(dāng)圖片是動態(tài)圖的時候
if (decoder.frameCount > 1) {
//注意看這里,這里把decoder 當(dāng)做自己成員變量了
//為什么要這樣做分蓖? 因?yàn)楫?dāng)時上面返回的是第一幀的圖片尔艇,但是對于frameCount > 1的動態(tài)圖,
//當(dāng)然返回一張是不夠的么鹤,這里保留decoder终娃,在需要使用YYAnimatedImageView代理方法的時候可以通過decoder來返回不同索引的圖片
_decoder = decoder;
_bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
_animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
}
- 好了,
YYImage
所做的工作已經(jīng)完成了蒸甜,我們現(xiàn)在來看看YYAnimatedImageView是怎么做最后的舞臺的
從setImage:withType:
方法出發(fā)棠耕,來到- (void)imageChanged
方法,同樣我也添加了注釋
YYAnimatedImageType newType = [self currentImageType];
id newVisibleImage = [self imageForType:newType];
NSUInteger newImageFrameCount = 0;
BOOL hasContentsRect = NO;
if ([newVisibleImage isKindOfClass:[UIImage class]] &&
//這句話其實(shí)就是把newVisibleImage當(dāng)做代理對象使用
//因?yàn)檫@里用的都是YYImage類型柠新,YYImage已經(jīng)實(shí)現(xiàn)了<YYAnimatedImage>代理方法
[newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
//得到圖片的個數(shù)
newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
if (newImageFrameCount > 1) {
hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
}
}
if (!hasContentsRect && _curImageHasContentsRect) {
//這個是關(guān)閉默認(rèn)的隱式動畫窍荧,防止對自己的動畫播放產(chǎn)生影響,
//有興趣的可以看看 core animation
if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
//設(shè)置layer的顯示范圍是整個寄宿圖片
self.layer.contentsRect = CGRectMake(0, 0, 1, 1);
[CATransaction commit];
}
}
_curImageHasContentsRect = hasContentsRect;
if (hasContentsRect) {
CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
[self setContentsRect:rect forImage:newVisibleImage];
}
if (newImageFrameCount > 1) {
//如果是 圖片數(shù)量>1 針對動態(tài)圖
//resetAnimated就是創(chuàng)建了一個CADisplayLink定時器去刷新圖片顯示
[self resetAnimated];
_curAnimatedImage = newVisibleImage;
_curFrame = newVisibleImage;
_totalLoop = _curAnimatedImage.animatedImageLoopCount;
_totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
[self calcMaxBufferCount];
}
[self setNeedsDisplay];
//開始播放動畫
[self didMoved];
在[self resetAnimated]
方法中恨憎,使用dispatch_once
來保證這個imageView中只起一次定時器蕊退,同時把這個定時器加到mainRunLoop
,模式默認(rèn)為NSRunLoopCommonModes
郊楣,也就是說在你滑動的時候不會影響到動態(tài)圖的播放,同時添加進(jìn)通知中心瓤荔,對于關(guān)于內(nèi)存警告的通知净蚤,和后臺的通知進(jìn)行相應(yīng)的一些處理,定時器定時調(diào)起step:
方法输硝,這個方法主要是做什么呢今瀑,
_time += link.duration;
//拿到當(dāng)前索引圖片的延遲時間,也就是需要顯示的時間
delay = [image animatedImageDurationAtIndex:_curIndex];
//如果當(dāng)前的link.duration還沒到点把,直接返回等到下一次調(diào)起
//就拿文章頭部的那個動態(tài)圖來說橘荠,每張圖顯示的時間大約在0.07秒左右
//而CADisplayLink每次任務(wù)執(zhí)行的時間大約是0.016秒
//所以不會用每次都刷新圖片顯示
if (_time < delay) return;
//如果調(diào)用了就用當(dāng)前的時間減去 當(dāng)前圖片需要顯示的時間
_time -= delay;
if (nextIndex == 0) {
_curLoop++;
if (_curLoop >= _totalLoop && _totalLoop != 0) {
_loopEnd = YES;
[self stopAnimating];
//主動調(diào)起刷新layer,系統(tǒng)會調(diào)用displayLayer
[self.layer setNeedsDisplay];
return;
}
}
_curFrame就是當(dāng)前要顯示的圖片愉粤,_curFrame的賦值也在step中砾医,具體就不解釋了,然后就通過下面這句代碼完成了imageview的layer的寄宿圖的設(shè)置
layer.contents = (__bridge id)_curFrame.CGImage;
好了到這衣厘,YYAnimatedImageView就開始播放動態(tài)圖了。