FLAnimatedImageView.m 10 KB

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