FLAnimatedImageView.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  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. #if defined(DEBUG) && DEBUG
  12. @protocol FLAnimatedImageViewDebugDelegate <NSObject>
  13. @optional
  14. - (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration;
  15. @end
  16. #endif
  17. @interface FLAnimatedImageView ()
  18. // Override of public `readonly` properties as private `readwrite`
  19. @property (nonatomic, strong, readwrite) UIImage *currentFrame;
  20. @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
  21. @property (nonatomic, assign) NSUInteger loopCountdown;
  22. @property (nonatomic, assign) NSTimeInterval accumulator;
  23. @property (nonatomic, strong) CADisplayLink *displayLink;
  24. @property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image or visibility (window, superview, hidden, alpha) has changed.
  25. @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
  26. #if defined(DEBUG) && DEBUG
  27. @property (nonatomic, weak) id<FLAnimatedImageViewDebugDelegate> debug_delegate;
  28. #endif
  29. @end
  30. @implementation FLAnimatedImageView
  31. @synthesize runLoopMode = _runLoopMode;
  32. #pragma mark - Initializers
  33. // -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be.
  34. // Using -initWithImage: doesn't call any of the other designated initializers.
  35. - (instancetype)initWithImage:(UIImage *)image
  36. {
  37. self = [super initWithImage:image];
  38. if (self) {
  39. [self commonInit];
  40. }
  41. return self;
  42. }
  43. // -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers.
  44. - (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage
  45. {
  46. self = [super initWithImage:image highlightedImage:highlightedImage];
  47. if (self) {
  48. [self commonInit];
  49. }
  50. return self;
  51. }
  52. - (instancetype)initWithFrame:(CGRect)frame
  53. {
  54. self = [super initWithFrame:frame];
  55. if (self) {
  56. [self commonInit];
  57. }
  58. return self;
  59. }
  60. - (instancetype)initWithCoder:(NSCoder *)aDecoder
  61. {
  62. self = [super initWithCoder:aDecoder];
  63. if (self) {
  64. [self commonInit];
  65. }
  66. return self;
  67. }
  68. - (void)commonInit
  69. {
  70. self.runLoopMode = [[self class] defaultRunLoopMode];
  71. }
  72. #pragma mark - Accessors
  73. #pragma mark Public
  74. - (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
  75. {
  76. if (![_animatedImage isEqual:animatedImage]) {
  77. if (animatedImage) {
  78. // Clear out the image.
  79. super.image = nil;
  80. // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
  81. super.highlighted = NO;
  82. // UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
  83. [self invalidateIntrinsicContentSize];
  84. } else {
  85. // Stop animating before the animated image gets cleared out.
  86. [self stopAnimating];
  87. }
  88. _animatedImage = animatedImage;
  89. self.currentFrame = animatedImage.posterImage;
  90. self.currentFrameIndex = 0;
  91. if (animatedImage.loopCount > 0) {
  92. self.loopCountdown = animatedImage.loopCount;
  93. } else {
  94. self.loopCountdown = NSUIntegerMax;
  95. }
  96. self.accumulator = 0.0;
  97. // Start animating after the new animated image has been set.
  98. [self updateShouldAnimate];
  99. if (self.shouldAnimate) {
  100. [self startAnimating];
  101. }
  102. [self.layer setNeedsDisplay];
  103. }
  104. }
  105. #pragma mark - Life Cycle
  106. - (void)dealloc
  107. {
  108. // Removes the display link from all run loop modes.
  109. [_displayLink invalidate];
  110. }
  111. #pragma mark - UIView Method Overrides
  112. #pragma mark Observing View-Related Changes
  113. - (void)didMoveToSuperview
  114. {
  115. [super didMoveToSuperview];
  116. [self updateShouldAnimate];
  117. if (self.shouldAnimate) {
  118. [self startAnimating];
  119. } else {
  120. [self stopAnimating];
  121. }
  122. }
  123. - (void)didMoveToWindow
  124. {
  125. [super didMoveToWindow];
  126. [self updateShouldAnimate];
  127. if (self.shouldAnimate) {
  128. [self startAnimating];
  129. } else {
  130. [self stopAnimating];
  131. }
  132. }
  133. - (void)setAlpha:(CGFloat)alpha
  134. {
  135. [super setAlpha:alpha];
  136. [self updateShouldAnimate];
  137. if (self.shouldAnimate) {
  138. [self startAnimating];
  139. } else {
  140. [self stopAnimating];
  141. }
  142. }
  143. - (void)setHidden:(BOOL)hidden
  144. {
  145. [super setHidden:hidden];
  146. [self updateShouldAnimate];
  147. if (self.shouldAnimate) {
  148. [self startAnimating];
  149. } else {
  150. [self stopAnimating];
  151. }
  152. }
  153. #pragma mark Auto Layout
  154. - (CGSize)intrinsicContentSize
  155. {
  156. // Default to let UIImageView handle the sizing of its image, and anything else it might consider.
  157. CGSize intrinsicContentSize = [super intrinsicContentSize];
  158. // If we have have an animated image, use its image size.
  159. // 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.
  160. // (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.)
  161. if (self.animatedImage) {
  162. intrinsicContentSize = self.image.size;
  163. }
  164. return intrinsicContentSize;
  165. }
  166. #pragma mark Smart Invert Colors
  167. - (BOOL)accessibilityIgnoresInvertColors
  168. {
  169. return YES;
  170. }
  171. #pragma mark - UIImageView Method Overrides
  172. #pragma mark Image Data
  173. - (UIImage *)image
  174. {
  175. UIImage *image = nil;
  176. if (self.animatedImage) {
  177. // Initially set to the poster image.
  178. image = self.currentFrame;
  179. } else {
  180. image = super.image;
  181. }
  182. return image;
  183. }
  184. - (void)setImage:(UIImage *)image
  185. {
  186. if (image) {
  187. // Clear out the animated image and implicitly pause animation playback.
  188. self.animatedImage = nil;
  189. }
  190. super.image = image;
  191. }
  192. #pragma mark Animating Images
  193. - (NSTimeInterval)frameDelayGreatestCommonDivisor
  194. {
  195. // Presision is set to half of the `kFLAnimatedImageDelayTimeIntervalMinimum` in order to minimize frame dropping.
  196. const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kFLAnimatedImageDelayTimeIntervalMinimum;
  197. NSArray *delays = self.animatedImage.delayTimesForIndexes.allValues;
  198. // Scales the frame delays by `kGreatestCommonDivisorPrecision`
  199. // then converts it to an UInteger for in order to calculate the GCD.
  200. NSUInteger scaledGCD = lrint([delays.firstObject floatValue] * kGreatestCommonDivisorPrecision);
  201. for (NSNumber *value in delays) {
  202. scaledGCD = gcd(lrint([value floatValue] * kGreatestCommonDivisorPrecision), scaledGCD);
  203. }
  204. // Reverse to scale to get the value back into seconds.
  205. return scaledGCD / kGreatestCommonDivisorPrecision;
  206. }
  207. static NSUInteger gcd(NSUInteger a, NSUInteger b)
  208. {
  209. // http://en.wikipedia.org/wiki/Greatest_common_divisor
  210. if (a < b) {
  211. return gcd(b, a);
  212. } else if (a == b) {
  213. return b;
  214. }
  215. while (true) {
  216. NSUInteger remainder = a % b;
  217. if (remainder == 0) {
  218. return b;
  219. }
  220. a = b;
  221. b = remainder;
  222. }
  223. }
  224. - (void)startAnimating
  225. {
  226. if (self.animatedImage) {
  227. // Lazily create the display link.
  228. if (!self.displayLink) {
  229. // It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
  230. // will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
  231. // independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
  232. // link which will lead to the deallocation of both the display link and the weak proxy.
  233. FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
  234. self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
  235. [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
  236. }
  237. // Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
  238. // Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh.
  239. const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
  240. self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
  241. self.displayLink.paused = NO;
  242. } else {
  243. [super startAnimating];
  244. }
  245. }
  246. - (void)setRunLoopMode:(NSString *)runLoopMode
  247. {
  248. if (![@[NSDefaultRunLoopMode, NSRunLoopCommonModes] containsObject:runLoopMode]) {
  249. NSAssert(NO, @"Invalid run loop mode: %@", runLoopMode);
  250. _runLoopMode = [[self class] defaultRunLoopMode];
  251. } else {
  252. _runLoopMode = runLoopMode;
  253. }
  254. }
  255. - (void)stopAnimating
  256. {
  257. if (self.animatedImage) {
  258. self.displayLink.paused = YES;
  259. } else {
  260. [super stopAnimating];
  261. }
  262. }
  263. - (BOOL)isAnimating
  264. {
  265. BOOL isAnimating = NO;
  266. if (self.animatedImage) {
  267. isAnimating = self.displayLink && !self.displayLink.isPaused;
  268. } else {
  269. isAnimating = [super isAnimating];
  270. }
  271. return isAnimating;
  272. }
  273. #pragma mark Highlighted Image Unsupport
  274. - (void)setHighlighted:(BOOL)highlighted
  275. {
  276. // Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
  277. if (!self.animatedImage) {
  278. [super setHighlighted:highlighted];
  279. }
  280. }
  281. #pragma mark - Private Methods
  282. #pragma mark Animation
  283. // Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
  284. // Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed.
  285. - (void)updateShouldAnimate
  286. {
  287. BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
  288. self.shouldAnimate = self.animatedImage && isVisible;
  289. }
  290. - (void)displayDidRefresh:(CADisplayLink *)displayLink
  291. {
  292. // If for some reason a wild call makes it through when we shouldn't be animating, bail.
  293. // Early return!
  294. if (!self.shouldAnimate) {
  295. FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self);
  296. return;
  297. }
  298. NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
  299. // 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).
  300. if (delayTimeNumber) {
  301. NSTimeInterval delayTime = [delayTimeNumber floatValue];
  302. // If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
  303. UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
  304. if (image) {
  305. FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
  306. self.currentFrame = image;
  307. if (self.needsDisplayWhenImageBecomesAvailable) {
  308. [self.layer setNeedsDisplay];
  309. self.needsDisplayWhenImageBecomesAvailable = NO;
  310. }
  311. self.accumulator += displayLink.duration * displayLink.frameInterval;
  312. // While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
  313. while (self.accumulator >= delayTime) {
  314. self.accumulator -= delayTime;
  315. self.currentFrameIndex++;
  316. if (self.currentFrameIndex >= self.animatedImage.frameCount) {
  317. // If we've looped the number of times that this animated image describes, stop looping.
  318. self.loopCountdown--;
  319. if (self.loopCompletionBlock) {
  320. self.loopCompletionBlock(self.loopCountdown);
  321. }
  322. if (self.loopCountdown == 0) {
  323. [self stopAnimating];
  324. return;
  325. }
  326. self.currentFrameIndex = 0;
  327. }
  328. // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
  329. // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
  330. self.needsDisplayWhenImageBecomesAvailable = YES;
  331. }
  332. } else {
  333. FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
  334. #if defined(DEBUG) && DEBUG
  335. if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
  336. [self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:(NSTimeInterval)displayLink.duration * displayLink.frameInterval];
  337. }
  338. #endif
  339. }
  340. } else {
  341. self.currentFrameIndex++;
  342. }
  343. }
  344. + (NSString *)defaultRunLoopMode
  345. {
  346. // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
  347. return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
  348. }
  349. #pragma mark - CALayerDelegate (Informal)
  350. #pragma mark Providing the Layer's Content
  351. - (void)displayLayer:(CALayer *)layer
  352. {
  353. layer.contents = (__bridge id)self.image.CGImage;
  354. }
  355. @end