-
Notifications
You must be signed in to change notification settings - Fork 0
/
FLAnimatedImageView.m
executable file
·292 lines (230 loc) · 9.81 KB
/
FLAnimatedImageView.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
//
// 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>
@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, window or superview has changed.
@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
@end
@implementation FLAnimatedImageView
#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];
}
}
#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
- (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:)];
NSString *mode = NSDefaultRunLoopMode;
// Enable playback during scrolling by allowing timer events (i.e. animation) with `NSRunLoopCommonModes`.
// But too keep scrolling smooth, only do this for hardware with more than one core and otherwise keep it at the default `NSDefaultRunLoopMode`.
// The only devices with single-core chips (supporting iOS 6+) are iPhone 3GS/4 and iPod Touch 4th gen.
// Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
if ([NSProcessInfo processInfo].activeProcessorCount > 1) {
mode = NSRunLoopCommonModes;
}
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:mode];
// 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 frame.
}
self.displayLink.paused = NO;
} else {
[super startAnimating];
}
}
- (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, window or superview is changed.
- (void)updateShouldAnimate
{
self.shouldAnimate = self.animatedImage && self.window && self.superview;
}
- (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) {
FLLogWarn(@"Trying to animate image when we shouldn't: %@", self);
return;
}
// If we have a nil image, don't update the view nor playhead.
UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
if (image) {
FLLogVerbose(@"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;
// While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
while (self.accumulator >= [self.animatedImage.delayTimes[self.currentFrameIndex] floatValue]) {
self.accumulator -= [self.animatedImage.delayTimes[self.currentFrameIndex] floatValue];
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.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 {
FLLogDebug(@"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)self.displayLink.duration];
}
#endif
}
}
#pragma mark - CALayerDelegate (Informal)
#pragma mark Providing the Layer's Content
- (void)displayLayer:(CALayer *)layer
{
layer.contents = (__bridge id)self.image.CGImage;
}
@end