FLAnimatedImageView.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. //
  2. // FLAnimatedImageView.h
  3. // Flipboard
  4. //
  5. // Created by Raphael Schaad on 7/8/13.
  6. // Copyright (c) 2013-2015 Flipboard. All rights reserved.
  7. //
  8. #import "FLAnimatedImageView.h"
  9. #import "FLAnimatedImage.h"
  10. #import <QuartzCore/QuartzCore.h>
  11. @interface FLAnimatedImageView ()
  12. // Override of public `readonly` properties as private `readwrite`
  13. @property (nonatomic, strong, readwrite) UIImage *currentFrame;
  14. @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
  15. @property (nonatomic, assign) NSUInteger loopCountdown;
  16. @property (nonatomic, assign) NSTimeInterval accumulator;
  17. @property (nonatomic, strong) CADisplayLink *displayLink;
  18. @property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image, window or superview has changed.
  19. @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
  20. @end
  21. @implementation FLAnimatedImageView
  22. #pragma mark - Accessors
  23. #pragma mark Public
  24. - (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
  25. {
  26. if (![_animatedImage isEqual:animatedImage]) {
  27. if (animatedImage) {
  28. // Clear out the image.
  29. super.image = nil;
  30. // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
  31. super.highlighted = NO;
  32. // UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
  33. [self invalidateIntrinsicContentSize];
  34. } else {
  35. // Stop animating before the animated image gets cleared out.
  36. [self stopAnimating];
  37. }
  38. _animatedImage = animatedImage;
  39. self.currentFrame = animatedImage.posterImage;
  40. self.currentFrameIndex = 0;
  41. if (animatedImage.loopCount > 0) {
  42. self.loopCountdown = animatedImage.loopCount;
  43. } else {
  44. self.loopCountdown = NSUIntegerMax;
  45. }
  46. self.accumulator = 0.0;
  47. // Start animating after the new animated image has been set.
  48. [self updateShouldAnimate];
  49. if (self.shouldAnimate) {
  50. [self startAnimating];
  51. }
  52. [self.layer setNeedsDisplay];
  53. }
  54. }
  55. #pragma mark - Life Cycle
  56. - (void)dealloc
  57. {
  58. // Removes the display link from all run loop modes.
  59. [_displayLink invalidate];
  60. }
  61. #pragma mark - UIView Method Overrides
  62. #pragma mark Observing View-Related Changes
  63. - (void)didMoveToSuperview
  64. {
  65. [super didMoveToSuperview];
  66. [self updateShouldAnimate];
  67. if (self.shouldAnimate) {
  68. [self startAnimating];
  69. } else {
  70. [self stopAnimating];
  71. }
  72. }
  73. - (void)didMoveToWindow
  74. {
  75. [super didMoveToWindow];
  76. [self updateShouldAnimate];
  77. if (self.shouldAnimate) {
  78. [self startAnimating];
  79. } else {
  80. [self stopAnimating];
  81. }
  82. }
  83. #pragma mark Auto Layout
  84. - (CGSize)intrinsicContentSize
  85. {
  86. // Default to let UIImageView handle the sizing of its image, and anything else it might consider.
  87. CGSize intrinsicContentSize = [super intrinsicContentSize];
  88. // If we have have an animated image, use its image size.
  89. // UIImageView's intrinsic content size seems to be the size of its image. The obvious approach, simply calling `-invalidateIntrinsicContentSize` when setting an animated image, results in UIImageView steadfastly returning `{UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric}` for its intrinsicContentSize.
  90. // (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.)
  91. if (self.animatedImage) {
  92. intrinsicContentSize = self.image.size;
  93. }
  94. return intrinsicContentSize;
  95. }
  96. #pragma mark - UIImageView Method Overrides
  97. #pragma mark Image Data
  98. - (UIImage *)image
  99. {
  100. UIImage *image = nil;
  101. if (self.animatedImage) {
  102. // Initially set to the poster image.
  103. image = self.currentFrame;
  104. } else {
  105. image = super.image;
  106. }
  107. return image;
  108. }
  109. - (void)setImage:(UIImage *)image
  110. {
  111. if (image) {
  112. // Clear out the animated image and implicitly pause animation playback.
  113. self.animatedImage = nil;
  114. }
  115. super.image = image;
  116. }
  117. #pragma mark Animating Images
  118. - (void)startAnimating
  119. {
  120. if (self.animatedImage) {
  121. // Lazily create the display link.
  122. if (!self.displayLink) {
  123. // It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
  124. // will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
  125. // independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
  126. // link which will lead to the deallocation of both the display link and the weak proxy.
  127. FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
  128. self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
  129. NSString *mode = NSDefaultRunLoopMode;
  130. // Enable playback during scrolling by allowing timer events (i.e. animation) with `NSRunLoopCommonModes`.
  131. // But too keep scrolling smooth, only do this for hardware with more than one core and otherwise keep it at the default `NSDefaultRunLoopMode`.
  132. // The only devices with single-core chips (supporting iOS 6+) are iPhone 3GS/4 and iPod Touch 4th gen.
  133. // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
  134. if ([NSProcessInfo processInfo].activeProcessorCount > 1) {
  135. mode = NSRunLoopCommonModes;
  136. }
  137. [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:mode];
  138. // Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
  139. // Setting it to 2 divides the frame rate by 2 and hence calls back at every other frame.
  140. }
  141. self.displayLink.paused = NO;
  142. } else {
  143. [super startAnimating];
  144. }
  145. }
  146. - (void)stopAnimating
  147. {
  148. if (self.animatedImage) {
  149. self.displayLink.paused = YES;
  150. } else {
  151. [super stopAnimating];
  152. }
  153. }
  154. - (BOOL)isAnimating
  155. {
  156. BOOL isAnimating = NO;
  157. if (self.animatedImage) {
  158. isAnimating = self.displayLink && !self.displayLink.isPaused;
  159. } else {
  160. isAnimating = [super isAnimating];
  161. }
  162. return isAnimating;
  163. }
  164. #pragma mark Highlighted Image Unsupport
  165. - (void)setHighlighted:(BOOL)highlighted
  166. {
  167. // Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
  168. if (!self.animatedImage) {
  169. [super setHighlighted:highlighted];
  170. }
  171. }
  172. #pragma mark - Private Methods
  173. #pragma mark Animation
  174. // Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
  175. // Just update our cached value whenever the animated image, window or superview is changed.
  176. - (void)updateShouldAnimate
  177. {
  178. self.shouldAnimate = self.animatedImage && self.window && self.superview;
  179. }
  180. - (void)displayDidRefresh:(CADisplayLink *)displayLink
  181. {
  182. // If for some reason a wild call makes it through when we shouldn't be animating, bail.
  183. // Early return!
  184. if (!self.shouldAnimate) {
  185. FLLogWarn(@"Trying to animate image when we shouldn't: %@", self);
  186. return;
  187. }
  188. NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
  189. // If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
  190. if (delayTimeNumber) {
  191. NSTimeInterval delayTime = [delayTimeNumber floatValue];
  192. // If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
  193. UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
  194. if (image) {
  195. FLLogVerbose(@"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
  196. self.currentFrame = image;
  197. if (self.needsDisplayWhenImageBecomesAvailable) {
  198. [self.layer setNeedsDisplay];
  199. self.needsDisplayWhenImageBecomesAvailable = NO;
  200. }
  201. self.accumulator += displayLink.duration;
  202. // While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
  203. while (self.accumulator >= delayTime) {
  204. self.accumulator -= delayTime;
  205. self.currentFrameIndex++;
  206. if (self.currentFrameIndex >= self.animatedImage.frameCount) {
  207. // If we've looped the number of times that this animated image describes, stop looping.
  208. self.loopCountdown--;
  209. if (self.loopCompletionBlock) {
  210. self.loopCompletionBlock(self.loopCountdown);
  211. }
  212. if (self.loopCountdown == 0) {
  213. [self stopAnimating];
  214. return;
  215. }
  216. self.currentFrameIndex = 0;
  217. }
  218. // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
  219. // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
  220. self.needsDisplayWhenImageBecomesAvailable = YES;
  221. }
  222. } else {
  223. FLLogDebug(@"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
  224. #if defined(DEBUG) && DEBUG
  225. if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
  226. [self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:(NSTimeInterval)self.displayLink.duration];
  227. }
  228. #endif
  229. }
  230. } else {
  231. self.currentFrameIndex++;
  232. }
  233. }
  234. #pragma mark - CALayerDelegate (Informal)
  235. #pragma mark Providing the Layer's Content
  236. - (void)displayLayer:(CALayer *)layer
  237. {
  238. layer.contents = (__bridge id)self.image.CGImage;
  239. }
  240. @end