123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578 |
- //
- // YYAnimatedImageView.m
- // YYKit <https://github.com/ibireme/YYKit>
- //
- // Created by ibireme on 14/10/19.
- // Copyright (c) 2015 ibireme.
- //
- // This source code is licensed under the MIT-style license found in the
- // LICENSE file in the root directory of this source tree.
- //
- #import "YYAnimatedImageView.h"
- #import "YYWeakProxy.h"
- #import "UIDevice+YYAdd.h"
- #import "YYImageCoder.h"
- #import "YYKitMacro.h"
- #define BUFFER_SIZE (10 * 1024 * 1024) // 10MB (minimum memory buffer size)
- #define LOCK(...) dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER); \
- __VA_ARGS__; \
- dispatch_semaphore_signal(self->_lock);
- #define LOCK_VIEW(...) dispatch_semaphore_wait(view->_lock, DISPATCH_TIME_FOREVER); \
- __VA_ARGS__; \
- dispatch_semaphore_signal(view->_lock);
- typedef NS_ENUM(NSUInteger, YYAnimatedImageType) {
- YYAnimatedImageTypeNone = 0,
- YYAnimatedImageTypeImage,
- YYAnimatedImageTypeHighlightedImage,
- YYAnimatedImageTypeImages,
- YYAnimatedImageTypeHighlightedImages,
- };
- @interface YYAnimatedImageView() {
- @package
- UIImage <YYAnimatedImage> *_curAnimatedImage;
-
- dispatch_semaphore_t _lock; ///< lock for _buffer
- NSOperationQueue *_requestQueue; ///< image request queue, serial
-
- CADisplayLink *_link; ///< ticker for change frame
- NSTimeInterval _time; ///< time after last frame
-
- UIImage *_curFrame; ///< current frame to display
- NSUInteger _curIndex; ///< current frame index (from 0)
- NSUInteger _totalFrameCount; ///< total frame count
-
- BOOL _loopEnd; ///< whether the loop is end.
- NSUInteger _curLoop; ///< current loop count (from 0)
- NSUInteger _totalLoop; ///< total loop count, 0 means infinity
-
- NSMutableDictionary *_buffer; ///< frame buffer
- BOOL _bufferMiss; ///< whether miss frame on last opportunity
- NSUInteger _maxBufferCount; ///< maximum buffer count
- NSInteger _incrBufferCount; ///< current allowed buffer count (will increase by step)
-
- CGRect _curContentsRect;
- BOOL _curImageHasContentsRect; ///< image has implementated "animatedImageContentsRectAtIndex:"
- }
- @property (nonatomic, readwrite) BOOL currentIsPlayingAnimation;
- - (void)calcMaxBufferCount;
- @end
- /// An operation for image fetch
- @interface _YYAnimatedImageViewFetchOperation : NSOperation
- @property (nonatomic, weak) YYAnimatedImageView *view;
- @property (nonatomic, assign) NSUInteger nextIndex;
- @property (nonatomic, strong) UIImage <YYAnimatedImage> *curImage;
- @end
- @implementation _YYAnimatedImageViewFetchOperation
- - (void)main {
- __strong YYAnimatedImageView *view = _view;
- if (!view) return;
- if ([self isCancelled]) return;
- view->_incrBufferCount++;
- if (view->_incrBufferCount == 0) [view calcMaxBufferCount];
- if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) {
- view->_incrBufferCount = view->_maxBufferCount;
- }
- NSUInteger idx = _nextIndex;
- NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount;
- NSUInteger total = view->_totalFrameCount;
- view = nil;
-
- for (int i = 0; i < max; i++, idx++) {
- @autoreleasepool {
- if (idx >= total) idx = 0;
- if ([self isCancelled]) break;
- __strong YYAnimatedImageView *view = _view;
- if (!view) break;
- LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil));
- if (miss) {
- UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
- img = img.imageByDecoded;
- if ([self isCancelled]) break;
- LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
- view = nil;
- }
- }
- }
- }
- @end
- @implementation YYAnimatedImageView
- - (instancetype)init {
- self = [super init];
- _runloopMode = NSRunLoopCommonModes;
- _autoPlayAnimatedImage = YES;
- return self;
- }
- - (instancetype)initWithFrame:(CGRect)frame {
- self = [super initWithFrame:frame];
- _runloopMode = NSRunLoopCommonModes;
- _autoPlayAnimatedImage = YES;
- return self;
- }
- - (instancetype)initWithImage:(UIImage *)image {
- self = [super init];
- _runloopMode = NSRunLoopCommonModes;
- _autoPlayAnimatedImage = YES;
- self.frame = (CGRect) {CGPointZero, image.size };
- self.image = image;
- return self;
- }
- - (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage {
- self = [super init];
- _runloopMode = NSRunLoopCommonModes;
- _autoPlayAnimatedImage = YES;
- CGSize size = image ? image.size : highlightedImage.size;
- self.frame = (CGRect) {CGPointZero, size };
- self.image = image;
- self.highlightedImage = highlightedImage;
- return self;
- }
- // init the animated params.
- - (void)resetAnimated {
- if (!_link) {
- _lock = dispatch_semaphore_create(1);
- _buffer = [NSMutableDictionary new];
- _requestQueue = [[NSOperationQueue alloc] init];
- _requestQueue.maxConcurrentOperationCount = 1;
- _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(step:)];
- if (_runloopMode) {
- [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
- }
- _link.paused = YES;
-
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
- }
-
- [_requestQueue cancelAllOperations];
- LOCK(
- if (_buffer.count) {
- NSMutableDictionary *holder = _buffer;
- _buffer = [NSMutableDictionary new];
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
- // Capture the dictionary to global queue,
- // release these images in background to avoid blocking UI thread.
- [holder class];
- });
- }
- );
- _link.paused = YES;
- _time = 0;
- if (_curIndex != 0) {
- [self willChangeValueForKey:@"currentAnimatedImageIndex"];
- _curIndex = 0;
- [self didChangeValueForKey:@"currentAnimatedImageIndex"];
- }
- _curAnimatedImage = nil;
- _curFrame = nil;
- _curLoop = 0;
- _totalLoop = 0;
- _totalFrameCount = 1;
- _loopEnd = NO;
- _bufferMiss = NO;
- _incrBufferCount = 0;
- }
- - (void)setImage:(UIImage *)image {
- if (self.image == image) return;
- [self setImage:image withType:YYAnimatedImageTypeImage];
- }
- - (void)setHighlightedImage:(UIImage *)highlightedImage {
- if (self.highlightedImage == highlightedImage) return;
- [self setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage];
- }
- - (void)setAnimationImages:(NSArray *)animationImages {
- if (self.animationImages == animationImages) return;
- [self setImage:animationImages withType:YYAnimatedImageTypeImages];
- }
- - (void)setHighlightedAnimationImages:(NSArray *)highlightedAnimationImages {
- if (self.highlightedAnimationImages == highlightedAnimationImages) return;
- [self setImage:highlightedAnimationImages withType:YYAnimatedImageTypeHighlightedImages];
- }
- - (void)setHighlighted:(BOOL)highlighted {
- [super setHighlighted:highlighted];
- if (_link) [self resetAnimated];
- [self imageChanged];
- }
- - (id)imageForType:(YYAnimatedImageType)type {
- switch (type) {
- case YYAnimatedImageTypeNone: return nil;
- case YYAnimatedImageTypeImage: return self.image;
- case YYAnimatedImageTypeHighlightedImage: return self.highlightedImage;
- case YYAnimatedImageTypeImages: return self.animationImages;
- case YYAnimatedImageTypeHighlightedImages: return self.highlightedAnimationImages;
- }
- return nil;
- }
- - (YYAnimatedImageType)currentImageType {
- YYAnimatedImageType curType = YYAnimatedImageTypeNone;
- if (self.highlighted) {
- if (self.highlightedAnimationImages.count) curType = YYAnimatedImageTypeHighlightedImages;
- else if (self.highlightedImage) curType = YYAnimatedImageTypeHighlightedImage;
- }
- if (curType == YYAnimatedImageTypeNone) {
- if (self.animationImages.count) curType = YYAnimatedImageTypeImages;
- else if (self.image) curType = YYAnimatedImageTypeImage;
- }
- return curType;
- }
- - (void)setImage:(id)image withType:(YYAnimatedImageType)type {
- [self stopAnimating];
- if (_link) [self resetAnimated];
- _curFrame = nil;
- switch (type) {
- case YYAnimatedImageTypeNone: break;
- case YYAnimatedImageTypeImage: super.image = image; break;
- case YYAnimatedImageTypeHighlightedImage: super.highlightedImage = image; break;
- case YYAnimatedImageTypeImages: super.animationImages = image; break;
- case YYAnimatedImageTypeHighlightedImages: super.highlightedAnimationImages = image; break;
- }
- [self imageChanged];
- }
- - (void)imageChanged {
- YYAnimatedImageType newType = [self currentImageType];
- id newVisibleImage = [self imageForType:newType];
- NSUInteger newImageFrameCount = 0;
- BOOL hasContentsRect = NO;
- if ([newVisibleImage isKindOfClass:[UIImage class]] &&
- [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
- newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
- if (newImageFrameCount > 1) {
- hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
- }
- }
- if (!hasContentsRect && _curImageHasContentsRect) {
- if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
- [CATransaction begin];
- [CATransaction setDisableActions:YES];
- 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) {
- [self resetAnimated];
- _curAnimatedImage = newVisibleImage;
- _curFrame = newVisibleImage;
- _totalLoop = _curAnimatedImage.animatedImageLoopCount;
- _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
- [self calcMaxBufferCount];
- }
- [self setNeedsDisplay];
- [self didMoved];
- }
- // dynamically adjust buffer size for current memory.
- - (void)calcMaxBufferCount {
- int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame;
- if (bytes == 0) bytes = 1024;
-
- int64_t total = [UIDevice currentDevice].memoryTotal;
- int64_t free = [UIDevice currentDevice].memoryFree;
- int64_t max = MIN(total * 0.2, free * 0.6);
- max = MAX(max, BUFFER_SIZE);
- if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max;
- double maxBufferCount = (double)max / (double)bytes;
- maxBufferCount = YY_CLAMP(maxBufferCount, 1, 512);
- _maxBufferCount = maxBufferCount;
- }
- - (void)dealloc {
- [_requestQueue cancelAllOperations];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
- [_link invalidate];
- }
- - (BOOL)isAnimating {
- return self.currentIsPlayingAnimation;
- }
- - (void)stopAnimating {
- [super stopAnimating];
- [_requestQueue cancelAllOperations];
- _link.paused = YES;
- self.currentIsPlayingAnimation = NO;
- }
- - (void)startAnimating {
- YYAnimatedImageType type = [self currentImageType];
- if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) {
- NSArray *images = [self imageForType:type];
- if (images.count > 0) {
- [super startAnimating];
- self.currentIsPlayingAnimation = YES;
- }
- } else {
- if (_curAnimatedImage && _link.paused) {
- _curLoop = 0;
- _loopEnd = NO;
- _link.paused = NO;
- self.currentIsPlayingAnimation = YES;
- }
- }
- }
- - (void)didReceiveMemoryWarning:(NSNotification *)notification {
- [_requestQueue cancelAllOperations];
- [_requestQueue addOperationWithBlock: ^{
- _incrBufferCount = -60 - (int)(arc4random() % 120); // about 1~3 seconds to grow back..
- NSNumber *next = @((_curIndex + 1) % _totalFrameCount);
- LOCK(
- NSArray * keys = _buffer.allKeys;
- for (NSNumber * key in keys) {
- if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation
- [_buffer removeObjectForKey:key];
- }
- }
- )//LOCK
- }];
- }
- - (void)didEnterBackground:(NSNotification *)notification {
- [_requestQueue cancelAllOperations];
- NSNumber *next = @((_curIndex + 1) % _totalFrameCount);
- LOCK(
- NSArray * keys = _buffer.allKeys;
- for (NSNumber * key in keys) {
- if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation
- [_buffer removeObjectForKey:key];
- }
- }
- )//LOCK
- }
- - (void)step:(CADisplayLink *)link {
- UIImage <YYAnimatedImage> *image = _curAnimatedImage;
- NSMutableDictionary *buffer = _buffer;
- UIImage *bufferedImage = nil;
- NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
- BOOL bufferIsFull = NO;
-
- if (!image) return;
- if (_loopEnd) { // view will keep in last frame
- [self stopAnimating];
- return;
- }
-
- NSTimeInterval delay = 0;
- if (!_bufferMiss) {
- _time += link.duration;
- delay = [image animatedImageDurationAtIndex:_curIndex];
- if (_time < delay) return;
- _time -= delay;
- if (nextIndex == 0) {
- _curLoop++;
- if (_curLoop >= _totalLoop && _totalLoop != 0) {
- _loopEnd = YES;
- [self stopAnimating];
- [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
- return; // stop at last frame
- }
- }
- delay = [image animatedImageDurationAtIndex:nextIndex];
- if (_time > delay) _time = delay; // do not jump over frame
- }
- LOCK(
- bufferedImage = buffer[@(nextIndex)];
- if (bufferedImage) {
- if ((int)_incrBufferCount < _totalFrameCount) {
- [buffer removeObjectForKey:@(nextIndex)];
- }
- [self willChangeValueForKey:@"currentAnimatedImageIndex"];
- _curIndex = nextIndex;
- [self didChangeValueForKey:@"currentAnimatedImageIndex"];
- _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
- if (_curImageHasContentsRect) {
- _curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];
- [self setContentsRect:_curContentsRect forImage:_curFrame];
- }
- nextIndex = (_curIndex + 1) % _totalFrameCount;
- _bufferMiss = NO;
- if (buffer.count == _totalFrameCount) {
- bufferIsFull = YES;
- }
- } else {
- _bufferMiss = YES;
- }
- )//LOCK
-
- if (!_bufferMiss) {
- [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
- }
-
- if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity
- _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];
- operation.view = self;
- operation.nextIndex = nextIndex;
- operation.curImage = image;
- [_requestQueue addOperation:operation];
- }
- }
- - (void)displayLayer:(CALayer *)layer {
- if (_curFrame) {
- layer.contents = (__bridge id)_curFrame.CGImage;
- }
- }
- - (void)setContentsRect:(CGRect)rect forImage:(UIImage *)image{
- CGRect layerRect = CGRectMake(0, 0, 1, 1);
- if (image) {
- CGSize imageSize = image.size;
- if (imageSize.width > 0.01 && imageSize.height > 0.01) {
- layerRect.origin.x = rect.origin.x / imageSize.width;
- layerRect.origin.y = rect.origin.y / imageSize.height;
- layerRect.size.width = rect.size.width / imageSize.width;
- layerRect.size.height = rect.size.height / imageSize.height;
- layerRect = CGRectIntersection(layerRect, CGRectMake(0, 0, 1, 1));
- if (CGRectIsNull(layerRect) || CGRectIsEmpty(layerRect)) {
- layerRect = CGRectMake(0, 0, 1, 1);
- }
- }
- }
- [CATransaction begin];
- [CATransaction setDisableActions:YES];
- self.layer.contentsRect = layerRect;
- [CATransaction commit];
- }
- - (void)didMoved {
- if (self.autoPlayAnimatedImage) {
- if(self.superview && self.window) {
- [self startAnimating];
- } else {
- [self stopAnimating];
- }
- }
- }
- - (void)didMoveToWindow {
- [super didMoveToWindow];
- [self didMoved];
- }
- - (void)didMoveToSuperview {
- [super didMoveToSuperview];
- [self didMoved];
- }
- - (void)setCurrentAnimatedImageIndex:(NSUInteger)currentAnimatedImageIndex {
- if (!_curAnimatedImage) return;
- if (currentAnimatedImageIndex >= _curAnimatedImage.animatedImageFrameCount) return;
- if (_curIndex == currentAnimatedImageIndex) return;
-
- dispatch_async_on_main_queue(^{
- LOCK(
- [_requestQueue cancelAllOperations];
- [_buffer removeAllObjects];
- [self willChangeValueForKey:@"currentAnimatedImageIndex"];
- _curIndex = currentAnimatedImageIndex;
- [self didChangeValueForKey:@"currentAnimatedImageIndex"];
- _curFrame = [_curAnimatedImage animatedImageFrameAtIndex:_curIndex];
- if (_curImageHasContentsRect) {
- _curContentsRect = [_curAnimatedImage animatedImageContentsRectAtIndex:_curIndex];
- }
- _time = 0;
- _loopEnd = NO;
- _bufferMiss = NO;
- [self.layer setNeedsDisplay];
- )//LOCK
- });
- }
- - (NSUInteger)currentAnimatedImageIndex {
- return _curIndex;
- }
- - (void)setRunloopMode:(NSString *)runloopMode {
- if ([_runloopMode isEqual:runloopMode]) return;
- if (_link) {
- if (_runloopMode) {
- [_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
- }
- if (runloopMode.length) {
- [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:runloopMode];
- }
- }
- _runloopMode = runloopMode.copy;
- }
- #pragma mark - Overrice NSObject(NSKeyValueObservingCustomization)
- + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
- if ([key isEqualToString:@"currentAnimatedImageIndex"]) {
- return NO;
- }
- return [super automaticallyNotifiesObserversForKey:key];
- }
- #pragma mark - NSCoding
- - (instancetype)initWithCoder:(NSCoder *)aDecoder {
- self = [super initWithCoder:aDecoder];
- _runloopMode = [aDecoder decodeObjectForKey:@"runloopMode"];
- if (_runloopMode.length == 0) _runloopMode = NSRunLoopCommonModes;
- if ([aDecoder containsValueForKey:@"autoPlayAnimatedImage"]) {
- _autoPlayAnimatedImage = [aDecoder decodeBoolForKey:@"autoPlayAnimatedImage"];
- } else {
- _autoPlayAnimatedImage = YES;
- }
-
- UIImage *image = [aDecoder decodeObjectForKey:@"YYAnimatedImage"];
- UIImage *highlightedImage = [aDecoder decodeObjectForKey:@"YYHighlightedAnimatedImage"];
- if (image) {
- self.image = image;
- [self setImage:image withType:YYAnimatedImageTypeImage];
- }
- if (highlightedImage) {
- self.highlightedImage = highlightedImage;
- [self setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage];
- }
- return self;
- }
- - (void)encodeWithCoder:(NSCoder *)aCoder {
- [super encodeWithCoder:aCoder];
- [aCoder encodeObject:_runloopMode forKey:@"runloopMode"];
- [aCoder encodeBool:_autoPlayAnimatedImage forKey:@"autoPlayAnimatedImage"];
-
- BOOL ani, multi;
- ani = [self.image conformsToProtocol:@protocol(YYAnimatedImage)];
- multi = (ani && ((UIImage <YYAnimatedImage> *)self.image).animatedImageFrameCount > 1);
- if (multi) [aCoder encodeObject:self.image forKey:@"YYAnimatedImage"];
-
- ani = [self.highlightedImage conformsToProtocol:@protocol(YYAnimatedImage)];
- multi = (ani && ((UIImage <YYAnimatedImage> *)self.highlightedImage).animatedImageFrameCount > 1);
- if (multi) [aCoder encodeObject:self.highlightedImage forKey:@"YYHighlightedAnimatedImage"];
- }
- @end
|