123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- //
- // FLAnimatedImageView.h
- // Flipboard
- //
- // Created by Raphael Schaad on 7/8/13.
- // Copyright (c) 2013-2015 Flipboard. All rights reserved.
- //
- #import "FLAnimatedImageView.h"
- #import "FLAnimatedImage.h"
- #import <QuartzCore/QuartzCore.h>
- #if defined(DEBUG) && DEBUG
- @protocol FLAnimatedImageViewDebugDelegate <NSObject>
- @optional
- - (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration;
- @end
- #endif
- @interface FLAnimatedImageView ()
- // Override of public `readonly` properties as private `readwrite`
- @property (nonatomic, strong, readwrite) UIImage *currentFrame;
- @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
- @property (nonatomic, assign) NSUInteger loopCountdown;
- @property (nonatomic, assign) NSTimeInterval accumulator;
- @property (nonatomic, strong) CADisplayLink *displayLink;
- @property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image or visibility (window, superview, hidden, alpha) has changed.
- @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
- #if defined(DEBUG) && DEBUG
- @property (nonatomic, weak) id<FLAnimatedImageViewDebugDelegate> debug_delegate;
- #endif
- @end
- @implementation FLAnimatedImageView
- @synthesize runLoopMode = _runLoopMode;
- #pragma mark - Initializers
- // -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be.
- // Using -initWithImage: doesn't call any of the other designated initializers.
- - (instancetype)initWithImage:(UIImage *)image
- {
- self = [super initWithImage:image];
- if (self) {
- [self commonInit];
- }
- return self;
- }
- // -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers.
- - (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage
- {
- self = [super initWithImage:image highlightedImage:highlightedImage];
- if (self) {
- [self commonInit];
- }
- return self;
- }
- - (instancetype)initWithFrame:(CGRect)frame
- {
- self = [super initWithFrame:frame];
- if (self) {
- [self commonInit];
- }
- return self;
- }
- - (instancetype)initWithCoder:(NSCoder *)aDecoder
- {
- self = [super initWithCoder:aDecoder];
- if (self) {
- [self commonInit];
- }
- return self;
- }
- - (void)commonInit
- {
- self.runLoopMode = [[self class] defaultRunLoopMode];
- }
- #pragma mark - Accessors
- #pragma mark Public
- - (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
- {
- if (![_animatedImage isEqual:animatedImage]) {
- if (animatedImage) {
- // Clear out the image.
- super.image = nil;
- // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
- super.highlighted = NO;
- // UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
- [self invalidateIntrinsicContentSize];
- } else {
- // Stop animating before the animated image gets cleared out.
- [self stopAnimating];
- }
-
- _animatedImage = animatedImage;
-
- self.currentFrame = animatedImage.posterImage;
- self.currentFrameIndex = 0;
- if (animatedImage.loopCount > 0) {
- self.loopCountdown = animatedImage.loopCount;
- } else {
- self.loopCountdown = NSUIntegerMax;
- }
- self.accumulator = 0.0;
-
- // Start animating after the new animated image has been set.
- [self updateShouldAnimate];
- if (self.shouldAnimate) {
- [self startAnimating];
- }
-
- [self.layer setNeedsDisplay];
- }
- }
- #pragma mark - Life Cycle
- - (void)dealloc
- {
- // Removes the display link from all run loop modes.
- [_displayLink invalidate];
- }
- #pragma mark - UIView Method Overrides
- #pragma mark Observing View-Related Changes
- - (void)didMoveToSuperview
- {
- [super didMoveToSuperview];
-
- [self updateShouldAnimate];
- if (self.shouldAnimate) {
- [self startAnimating];
- } else {
- [self stopAnimating];
- }
- }
- - (void)didMoveToWindow
- {
- [super didMoveToWindow];
-
- [self updateShouldAnimate];
- if (self.shouldAnimate) {
- [self startAnimating];
- } else {
- [self stopAnimating];
- }
- }
- - (void)setAlpha:(CGFloat)alpha
- {
- [super setAlpha:alpha];
- [self updateShouldAnimate];
- if (self.shouldAnimate) {
- [self startAnimating];
- } else {
- [self stopAnimating];
- }
- }
- - (void)setHidden:(BOOL)hidden
- {
- [super setHidden:hidden];
- [self updateShouldAnimate];
- if (self.shouldAnimate) {
- [self startAnimating];
- } else {
- [self stopAnimating];
- }
- }
- #pragma mark Auto Layout
- - (CGSize)intrinsicContentSize
- {
- // Default to let UIImageView handle the sizing of its image, and anything else it might consider.
- CGSize intrinsicContentSize = [super intrinsicContentSize];
-
- // If we have have an animated image, use its image size.
- // 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.
- // (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.)
- if (self.animatedImage) {
- intrinsicContentSize = self.image.size;
- }
-
- return intrinsicContentSize;
- }
- #pragma mark - UIImageView Method Overrides
- #pragma mark Image Data
- - (UIImage *)image
- {
- UIImage *image = nil;
- if (self.animatedImage) {
- // Initially set to the poster image.
- image = self.currentFrame;
- } else {
- image = super.image;
- }
- return image;
- }
- - (void)setImage:(UIImage *)image
- {
- if (image) {
- // Clear out the animated image and implicitly pause animation playback.
- self.animatedImage = nil;
- }
-
- super.image = image;
- }
- #pragma mark Animating Images
- - (NSTimeInterval)frameDelayGreatestCommonDivisor
- {
- // Presision is set to half of the `kFLAnimatedImageDelayTimeIntervalMinimum` in order to minimize frame dropping.
- const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kFLAnimatedImageDelayTimeIntervalMinimum;
- NSArray *delays = self.animatedImage.delayTimesForIndexes.allValues;
- // Scales the frame delays by `kGreatestCommonDivisorPrecision`
- // then converts it to an UInteger for in order to calculate the GCD.
- NSUInteger scaledGCD = lrint([delays.firstObject floatValue] * kGreatestCommonDivisorPrecision);
- for (NSNumber *value in delays) {
- scaledGCD = gcd(lrint([value floatValue] * kGreatestCommonDivisorPrecision), scaledGCD);
- }
- // Reverse to scale to get the value back into seconds.
- return scaledGCD / kGreatestCommonDivisorPrecision;
- }
- static NSUInteger gcd(NSUInteger a, NSUInteger b)
- {
- // http://en.wikipedia.org/wiki/Greatest_common_divisor
- if (a < b) {
- return gcd(b, a);
- } else if (a == b) {
- return b;
- }
- while (true) {
- NSUInteger remainder = a % b;
- if (remainder == 0) {
- return b;
- }
- a = b;
- b = remainder;
- }
- }
- - (void)startAnimating
- {
- if (self.animatedImage) {
- // Lazily create the display link.
- if (!self.displayLink) {
- // It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
- // will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
- // independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
- // link which will lead to the deallocation of both the display link and the weak proxy.
- FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
- self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
-
- [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
- }
- // Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
- // Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh.
- const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
- self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
- self.displayLink.paused = NO;
- } else {
- [super startAnimating];
- }
- }
- - (void)setRunLoopMode:(NSString *)runLoopMode
- {
- if (![@[NSDefaultRunLoopMode, NSRunLoopCommonModes] containsObject:runLoopMode]) {
- NSAssert(NO, @"Invalid run loop mode: %@", runLoopMode);
- _runLoopMode = [[self class] defaultRunLoopMode];
- } else {
- _runLoopMode = runLoopMode;
- }
- }
- - (void)stopAnimating
- {
- if (self.animatedImage) {
- self.displayLink.paused = YES;
- } else {
- [super stopAnimating];
- }
- }
- - (BOOL)isAnimating
- {
- BOOL isAnimating = NO;
- if (self.animatedImage) {
- isAnimating = self.displayLink && !self.displayLink.isPaused;
- } else {
- isAnimating = [super isAnimating];
- }
- return isAnimating;
- }
- #pragma mark Highlighted Image Unsupport
- - (void)setHighlighted:(BOOL)highlighted
- {
- // Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
- if (!self.animatedImage) {
- [super setHighlighted:highlighted];
- }
- }
- #pragma mark - Private Methods
- #pragma mark Animation
- // Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
- // Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed.
- - (void)updateShouldAnimate
- {
- BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
- self.shouldAnimate = self.animatedImage && isVisible;
- }
- - (void)displayDidRefresh:(CADisplayLink *)displayLink
- {
- // If for some reason a wild call makes it through when we shouldn't be animating, bail.
- // Early return!
- if (!self.shouldAnimate) {
- FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self);
- return;
- }
-
- NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
- // 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).
- if (delayTimeNumber) {
- NSTimeInterval delayTime = [delayTimeNumber floatValue];
- // If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
- UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
- if (image) {
- FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
- self.currentFrame = image;
- if (self.needsDisplayWhenImageBecomesAvailable) {
- [self.layer setNeedsDisplay];
- self.needsDisplayWhenImageBecomesAvailable = NO;
- }
-
- self.accumulator += displayLink.duration * displayLink.frameInterval;
-
- // While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
- while (self.accumulator >= delayTime) {
- self.accumulator -= delayTime;
- self.currentFrameIndex++;
- if (self.currentFrameIndex >= self.animatedImage.frameCount) {
- // If we've looped the number of times that this animated image describes, stop looping.
- self.loopCountdown--;
- if (self.loopCompletionBlock) {
- self.loopCompletionBlock(self.loopCountdown);
- }
-
- if (self.loopCountdown == 0) {
- [self stopAnimating];
- return;
- }
- self.currentFrameIndex = 0;
- }
- // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
- // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
- self.needsDisplayWhenImageBecomesAvailable = YES;
- }
- } else {
- FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
- #if defined(DEBUG) && DEBUG
- if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
- [self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:(NSTimeInterval)displayLink.duration * displayLink.frameInterval];
- }
- #endif
- }
- } else {
- self.currentFrameIndex++;
- }
- }
- + (NSString *)defaultRunLoopMode
- {
- // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
- return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
- }
- #pragma mark - CALayerDelegate (Informal)
- #pragma mark Providing the Layer's Content
- - (void)displayLayer:(CALayer *)layer
- {
- layer.contents = (__bridge id)self.image.CGImage;
- }
- @end
|