FLAnimatedImageView.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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 - UIImageView Method Overrides
  167. #pragma mark Image Data
  168. - (UIImage *)image
  169. {
  170. UIImage *image = nil;
  171. if (self.animatedImage) {
  172. // Initially set to the poster image.
  173. image = self.currentFrame;
  174. } else {
  175. image = super.image;
  176. }
  177. return image;
  178. }
  179. - (void)setImage:(UIImage *)image
  180. {
  181. if (image) {
  182. // Clear out the animated image and implicitly pause animation playback.
  183. self.animatedImage = nil;
  184. }
  185. super.image = image;
  186. }
  187. #pragma mark Animating Images
  188. - (NSTimeInterval)frameDelayGreatestCommonDivisor
  189. {
  190. // Presision is set to half of the `kFLAnimatedImageDelayTimeIntervalMinimum` in order to minimize frame dropping.
  191. const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kFLAnimatedImageDelayTimeIntervalMinimum;
  192. NSArray *delays = self.animatedImage.delayTimesForIndexes.allValues;
  193. // Scales the frame delays by `kGreatestCommonDivisorPrecision`
  194. // then converts it to an UInteger for in order to calculate the GCD.
  195. NSUInteger scaledGCD = lrint([delays.firstObject floatValue] * kGreatestCommonDivisorPrecision);
  196. for (NSNumber *value in delays) {
  197. scaledGCD = gcd(lrint([value floatValue] * kGreatestCommonDivisorPrecision), scaledGCD);
  198. }
  199. // Reverse to scale to get the value back into seconds.
  200. return scaledGCD / kGreatestCommonDivisorPrecision;
  201. }
  202. static NSUInteger gcd(NSUInteger a, NSUInteger b)
  203. {
  204. // http://en.wikipedia.org/wiki/Greatest_common_divisor
  205. if (a < b) {
  206. return gcd(b, a);
  207. } else if (a == b) {
  208. return b;
  209. }
  210. while (true) {
  211. NSUInteger remainder = a % b;
  212. if (remainder == 0) {
  213. return b;
  214. }
  215. a = b;
  216. b = remainder;
  217. }
  218. }
  219. - (void)startAnimating
  220. {
  221. if (self.animatedImage) {
  222. // Lazily create the display link.
  223. if (!self.displayLink) {
  224. // It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
  225. // will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
  226. // independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
  227. // link which will lead to the deallocation of both the display link and the weak proxy.
  228. FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
  229. self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
  230. [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
  231. }
  232. // Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
  233. // Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh.
  234. const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
  235. self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
  236. self.displayLink.paused = NO;
  237. } else {
  238. [super startAnimating];
  239. }
  240. }
  241. - (void)setRunLoopMode:(NSString *)runLoopMode
  242. {
  243. if (![@[NSDefaultRunLoopMode, NSRunLoopCommonModes] containsObject:runLoopMode]) {
  244. NSAssert(NO, @"Invalid run loop mode: %@", runLoopMode);
  245. _runLoopMode = [[self class] defaultRunLoopMode];
  246. } else {
  247. _runLoopMode = runLoopMode;
  248. }
  249. }
  250. - (void)stopAnimating
  251. {
  252. if (self.animatedImage) {
  253. self.displayLink.paused = YES;
  254. } else {
  255. [super stopAnimating];
  256. }
  257. }
  258. - (BOOL)isAnimating
  259. {
  260. BOOL isAnimating = NO;
  261. if (self.animatedImage) {
  262. isAnimating = self.displayLink && !self.displayLink.isPaused;
  263. } else {
  264. isAnimating = [super isAnimating];
  265. }
  266. return isAnimating;
  267. }
  268. #pragma mark Highlighted Image Unsupport
  269. - (void)setHighlighted:(BOOL)highlighted
  270. {
  271. // Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
  272. if (!self.animatedImage) {
  273. [super setHighlighted:highlighted];
  274. }
  275. }
  276. #pragma mark - Private Methods
  277. #pragma mark Animation
  278. // Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
  279. // Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed.
  280. - (void)updateShouldAnimate
  281. {
  282. BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
  283. self.shouldAnimate = self.animatedImage && isVisible;
  284. }
  285. - (void)displayDidRefresh:(CADisplayLink *)displayLink
  286. {
  287. // If for some reason a wild call makes it through when we shouldn't be animating, bail.
  288. // Early return!
  289. if (!self.shouldAnimate) {
  290. FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self);
  291. return;
  292. }
  293. NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
  294. // 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).
  295. if (delayTimeNumber) {
  296. NSTimeInterval delayTime = [delayTimeNumber floatValue];
  297. // If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
  298. UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
  299. if (image) {
  300. FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
  301. self.currentFrame = image;
  302. if (self.needsDisplayWhenImageBecomesAvailable) {
  303. [self.layer setNeedsDisplay];
  304. self.needsDisplayWhenImageBecomesAvailable = NO;
  305. }
  306. self.accumulator += displayLink.duration * displayLink.frameInterval;
  307. // While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
  308. while (self.accumulator >= delayTime) {
  309. self.accumulator -= delayTime;
  310. self.currentFrameIndex++;
  311. if (self.currentFrameIndex >= self.animatedImage.frameCount) {
  312. // If we've looped the number of times that this animated image describes, stop looping.
  313. self.loopCountdown--;
  314. if (self.loopCompletionBlock) {
  315. self.loopCompletionBlock(self.loopCountdown);
  316. }
  317. if (self.loopCountdown == 0) {
  318. [self stopAnimating];
  319. return;
  320. }
  321. self.currentFrameIndex = 0;
  322. }
  323. // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
  324. // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
  325. self.needsDisplayWhenImageBecomesAvailable = YES;
  326. }
  327. } else {
  328. FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
  329. #if defined(DEBUG) && DEBUG
  330. if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
  331. [self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:(NSTimeInterval)displayLink.duration * displayLink.frameInterval];
  332. }
  333. #endif
  334. }
  335. } else {
  336. self.currentFrameIndex++;
  337. }
  338. }
  339. + (NSString *)defaultRunLoopMode
  340. {
  341. // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
  342. return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
  343. }
  344. #pragma mark - CALayerDelegate (Informal)
  345. #pragma mark Providing the Layer's Content
  346. - (void)displayLayer:(CALayer *)layer
  347. {
  348. layer.contents = (__bridge id)self.image.CGImage;
  349. }
  350. @end