SDAnimatedImageView.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. /*
  2. * This file is part of the SDWebImage package.
  3. * (c) Olivier Poitrey <rs@dailymotion.com>
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. #import "SDAnimatedImageView.h"
  9. #if SD_UIKIT || SD_MAC
  10. #import "UIImage+Metadata.h"
  11. #import "NSImage+Compatibility.h"
  12. #import "SDInternalMacros.h"
  13. #import "objc/runtime.h"
  14. @interface UIImageView () <CALayerDelegate>
  15. @end
  16. @interface SDAnimatedImageView () {
  17. BOOL _initFinished; // Extra flag to mark the `commonInit` is called
  18. NSRunLoopMode _runLoopMode;
  19. NSUInteger _maxBufferSize;
  20. double _playbackRate;
  21. SDAnimatedImagePlaybackMode _playbackMode;
  22. }
  23. @property (nonatomic, strong, readwrite) SDAnimatedImagePlayer *player;
  24. @property (nonatomic, strong, readwrite) UIImage *currentFrame;
  25. @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
  26. @property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
  27. @property (nonatomic, assign) BOOL shouldAnimate;
  28. @property (nonatomic, assign) BOOL isProgressive;
  29. @property (nonatomic) CALayer *imageViewLayer; // The actual rendering layer.
  30. @end
  31. @implementation SDAnimatedImageView
  32. #if SD_UIKIT
  33. @dynamic animationRepeatCount; // we re-use this property from `UIImageView` super class on iOS.
  34. #endif
  35. #pragma mark - Initializers
  36. #if SD_MAC
  37. + (instancetype)imageViewWithImage:(NSImage *)image
  38. {
  39. NSRect frame = NSMakeRect(0, 0, image.size.width, image.size.height);
  40. SDAnimatedImageView *imageView = [[SDAnimatedImageView alloc] initWithFrame:frame];
  41. [imageView setImage:image];
  42. return imageView;
  43. }
  44. #else
  45. // -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be.
  46. // Using -initWithImage: doesn't call any of the other designated initializers.
  47. - (instancetype)initWithImage:(UIImage *)image
  48. {
  49. self = [super initWithImage:image];
  50. if (self) {
  51. [self commonInit];
  52. }
  53. return self;
  54. }
  55. // -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers.
  56. - (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage
  57. {
  58. self = [super initWithImage:image highlightedImage:highlightedImage];
  59. if (self) {
  60. [self commonInit];
  61. }
  62. return self;
  63. }
  64. #endif
  65. - (instancetype)initWithFrame:(CGRect)frame
  66. {
  67. self = [super initWithFrame:frame];
  68. if (self) {
  69. [self commonInit];
  70. }
  71. return self;
  72. }
  73. - (instancetype)initWithCoder:(NSCoder *)aDecoder
  74. {
  75. self = [super initWithCoder:aDecoder];
  76. if (self) {
  77. [self commonInit];
  78. }
  79. return self;
  80. }
  81. - (void)commonInit
  82. {
  83. // Pay attention that UIKit's `initWithImage:` will trigger a `setImage:` during initialization before this `commonInit`.
  84. // So the properties which rely on this order, should using lazy-evaluation or do extra check in `setImage:`.
  85. self.autoPlayAnimatedImage = YES;
  86. self.shouldCustomLoopCount = NO;
  87. self.shouldIncrementalLoad = YES;
  88. self.playbackRate = 1.0;
  89. #if SD_MAC
  90. self.wantsLayer = YES;
  91. #endif
  92. // Mark commonInit finished
  93. _initFinished = YES;
  94. }
  95. #pragma mark - Accessors
  96. #pragma mark Public
  97. - (void)setImage:(UIImage *)image
  98. {
  99. if (self.image == image) {
  100. return;
  101. }
  102. // Check Progressive rendering
  103. [self updateIsProgressiveWithImage:image];
  104. if (!self.isProgressive) {
  105. // Stop animating
  106. self.player = nil;
  107. self.currentFrame = nil;
  108. self.currentFrameIndex = 0;
  109. self.currentLoopCount = 0;
  110. }
  111. // We need call super method to keep function. This will impliedly call `setNeedsDisplay`. But we have no way to avoid this when using animated image. So we call `setNeedsDisplay` again at the end.
  112. super.image = image;
  113. if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
  114. if (!self.player) {
  115. id<SDAnimatedImageProvider> provider;
  116. // Check progressive loading
  117. if (self.isProgressive) {
  118. provider = [self progressiveAnimatedCoderForImage:image];
  119. } else {
  120. provider = (id<SDAnimatedImage>)image;
  121. }
  122. // Create animated player
  123. self.player = [SDAnimatedImagePlayer playerWithProvider:provider];
  124. } else {
  125. // Update Frame Count
  126. self.player.totalFrameCount = [(id<SDAnimatedImage>)image animatedImageFrameCount];
  127. }
  128. if (!self.player) {
  129. // animated player nil means the image format is not supported, or frame count <= 1
  130. return;
  131. }
  132. // Custom Loop Count
  133. if (self.shouldCustomLoopCount) {
  134. self.player.totalLoopCount = self.animationRepeatCount;
  135. }
  136. // RunLoop Mode
  137. self.player.runLoopMode = self.runLoopMode;
  138. // Max Buffer Size
  139. self.player.maxBufferSize = self.maxBufferSize;
  140. // Play Rate
  141. self.player.playbackRate = self.playbackRate;
  142. // Play Mode
  143. self.player.playbackMode = self.playbackMode;
  144. // Setup handler
  145. @weakify(self);
  146. self.player.animationFrameHandler = ^(NSUInteger index, UIImage * frame) {
  147. @strongify(self);
  148. self.currentFrameIndex = index;
  149. self.currentFrame = frame;
  150. [self.imageViewLayer setNeedsDisplay];
  151. };
  152. self.player.animationLoopHandler = ^(NSUInteger loopCount) {
  153. @strongify(self);
  154. // Progressive image reach the current last frame index. Keep the state and pause animating. Wait for later restart
  155. if (self.isProgressive) {
  156. NSUInteger lastFrameIndex = self.player.totalFrameCount - 1;
  157. [self.player seekToFrameAtIndex:lastFrameIndex loopCount:0];
  158. [self.player pausePlaying];
  159. } else {
  160. self.currentLoopCount = loopCount;
  161. }
  162. };
  163. // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
  164. super.highlighted = NO;
  165. [self stopAnimating];
  166. [self checkPlay];
  167. }
  168. [self.imageViewLayer setNeedsDisplay];
  169. }
  170. #pragma mark - Configuration
  171. - (void)setRunLoopMode:(NSRunLoopMode)runLoopMode
  172. {
  173. _runLoopMode = [runLoopMode copy];
  174. self.player.runLoopMode = runLoopMode;
  175. }
  176. - (NSRunLoopMode)runLoopMode
  177. {
  178. if (!_runLoopMode) {
  179. _runLoopMode = [[self class] defaultRunLoopMode];
  180. }
  181. return _runLoopMode;
  182. }
  183. + (NSString *)defaultRunLoopMode {
  184. // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
  185. return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
  186. }
  187. - (void)setMaxBufferSize:(NSUInteger)maxBufferSize
  188. {
  189. _maxBufferSize = maxBufferSize;
  190. self.player.maxBufferSize = maxBufferSize;
  191. }
  192. - (NSUInteger)maxBufferSize {
  193. return _maxBufferSize; // Defaults to 0
  194. }
  195. - (void)setPlaybackRate:(double)playbackRate
  196. {
  197. _playbackRate = playbackRate;
  198. self.player.playbackRate = playbackRate;
  199. }
  200. - (double)playbackRate
  201. {
  202. if (!_initFinished) {
  203. return 1.0; // Defaults to 1.0
  204. }
  205. return _playbackRate;
  206. }
  207. - (void)setPlaybackMode:(SDAnimatedImagePlaybackMode)playbackMode {
  208. _playbackMode = playbackMode;
  209. self.player.playbackMode = playbackMode;
  210. }
  211. - (SDAnimatedImagePlaybackMode)playbackMode {
  212. if (!_initFinished) {
  213. return SDAnimatedImagePlaybackModeNormal; // Default mode is normal
  214. }
  215. return _playbackMode;
  216. }
  217. - (BOOL)shouldIncrementalLoad
  218. {
  219. if (!_initFinished) {
  220. return YES; // Defaults to YES
  221. }
  222. return _initFinished;
  223. }
  224. #pragma mark - UIView Method Overrides
  225. #pragma mark Observing View-Related Changes
  226. #if SD_MAC
  227. - (void)viewDidMoveToSuperview
  228. #else
  229. - (void)didMoveToSuperview
  230. #endif
  231. {
  232. #if SD_MAC
  233. [super viewDidMoveToSuperview];
  234. #else
  235. [super didMoveToSuperview];
  236. #endif
  237. [self checkPlay];
  238. }
  239. #if SD_MAC
  240. - (void)viewDidMoveToWindow
  241. #else
  242. - (void)didMoveToWindow
  243. #endif
  244. {
  245. #if SD_MAC
  246. [super viewDidMoveToWindow];
  247. #else
  248. [super didMoveToWindow];
  249. #endif
  250. [self checkPlay];
  251. }
  252. #if SD_MAC
  253. - (void)setAlphaValue:(CGFloat)alphaValue
  254. #else
  255. - (void)setAlpha:(CGFloat)alpha
  256. #endif
  257. {
  258. #if SD_MAC
  259. [super setAlphaValue:alphaValue];
  260. #else
  261. [super setAlpha:alpha];
  262. #endif
  263. [self checkPlay];
  264. }
  265. - (void)setHidden:(BOOL)hidden
  266. {
  267. [super setHidden:hidden];
  268. [self checkPlay];
  269. }
  270. #pragma mark - UIImageView Method Overrides
  271. #pragma mark Image Data
  272. - (void)setAnimationRepeatCount:(NSInteger)animationRepeatCount
  273. {
  274. #if SD_UIKIT
  275. [super setAnimationRepeatCount:animationRepeatCount];
  276. #else
  277. _animationRepeatCount = animationRepeatCount;
  278. #endif
  279. if (self.shouldCustomLoopCount) {
  280. self.player.totalLoopCount = animationRepeatCount;
  281. }
  282. }
  283. - (void)startAnimating
  284. {
  285. if (self.player) {
  286. [self updateShouldAnimate];
  287. if (self.shouldAnimate) {
  288. [self.player startPlaying];
  289. }
  290. } else {
  291. #if SD_UIKIT
  292. [super startAnimating];
  293. #else
  294. [super setAnimates:YES];
  295. #endif
  296. }
  297. }
  298. - (void)stopAnimating
  299. {
  300. if (self.player) {
  301. if (self.resetFrameIndexWhenStopped) {
  302. [self.player stopPlaying];
  303. } else {
  304. [self.player pausePlaying];
  305. }
  306. if (self.clearBufferWhenStopped) {
  307. [self.player clearFrameBuffer];
  308. }
  309. } else {
  310. #if SD_UIKIT
  311. [super stopAnimating];
  312. #else
  313. [super setAnimates:NO];
  314. #endif
  315. }
  316. }
  317. #if SD_UIKIT
  318. - (BOOL)isAnimating
  319. {
  320. if (self.player) {
  321. return self.player.isPlaying;
  322. } else {
  323. return [super isAnimating];
  324. }
  325. }
  326. #endif
  327. #if SD_MAC
  328. - (BOOL)animates
  329. {
  330. if (self.player) {
  331. return self.player.isPlaying;
  332. } else {
  333. return [super animates];
  334. }
  335. }
  336. - (void)setAnimates:(BOOL)animates
  337. {
  338. if (animates) {
  339. [self startAnimating];
  340. } else {
  341. [self stopAnimating];
  342. }
  343. }
  344. #endif
  345. #pragma mark Highlighted Image Unsupport
  346. - (void)setHighlighted:(BOOL)highlighted
  347. {
  348. // Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
  349. if (!self.player) {
  350. [super setHighlighted:highlighted];
  351. }
  352. }
  353. #pragma mark - Private Methods
  354. #pragma mark Animation
  355. /// Check if it should be played
  356. - (void)checkPlay
  357. {
  358. // Only handle for SDAnimatedImage, leave UIAnimatedImage or animationImages for super implementation control
  359. if (self.player && self.autoPlayAnimatedImage) {
  360. [self updateShouldAnimate];
  361. if (self.shouldAnimate) {
  362. [self startAnimating];
  363. } else {
  364. [self stopAnimating];
  365. }
  366. }
  367. }
  368. // Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
  369. // Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed.
  370. - (void)updateShouldAnimate
  371. {
  372. #if SD_MAC
  373. BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alphaValue > 0.0;
  374. #else
  375. BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
  376. #endif
  377. self.shouldAnimate = self.player && isVisible;
  378. }
  379. // Update progressive status only after `setImage:` call.
  380. - (void)updateIsProgressiveWithImage:(UIImage *)image
  381. {
  382. self.isProgressive = NO;
  383. if (!self.shouldIncrementalLoad) {
  384. // Early return
  385. return;
  386. }
  387. // We must use `image.class conformsToProtocol:` instead of `image conformsToProtocol:` here
  388. // Because UIKit on macOS, using internal hard-coded override method, which returns NO
  389. id<SDAnimatedImageCoder> currentAnimatedCoder = [self progressiveAnimatedCoderForImage:image];
  390. if (currentAnimatedCoder) {
  391. UIImage *previousImage = self.image;
  392. if (!previousImage) {
  393. // If current animated coder supports progressive, and no previous image to check, start progressive loading
  394. self.isProgressive = YES;
  395. } else {
  396. id<SDAnimatedImageCoder> previousAnimatedCoder = [self progressiveAnimatedCoderForImage:previousImage];
  397. if (previousAnimatedCoder == currentAnimatedCoder) {
  398. // If current animated coder is the same as previous, start progressive loading
  399. self.isProgressive = YES;
  400. }
  401. }
  402. }
  403. }
  404. // Check if image can represent a `Progressive Animated Image` during loading
  405. - (id<SDAnimatedImageCoder, SDProgressiveImageCoder>)progressiveAnimatedCoderForImage:(UIImage *)image
  406. {
  407. if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)] && image.sd_isIncremental && [image respondsToSelector:@selector(animatedCoder)]) {
  408. id<SDAnimatedImageCoder> animatedCoder = [(id<SDAnimatedImage>)image animatedCoder];
  409. if ([animatedCoder respondsToSelector:@selector(initIncrementalWithOptions:)]) {
  410. return (id<SDAnimatedImageCoder, SDProgressiveImageCoder>)animatedCoder;
  411. }
  412. }
  413. return nil;
  414. }
  415. #pragma mark Providing the Layer's Content
  416. #pragma mark - CALayerDelegate
  417. - (void)displayLayer:(CALayer *)layer
  418. {
  419. UIImage *currentFrame = self.currentFrame;
  420. if (currentFrame) {
  421. layer.contentsScale = currentFrame.scale;
  422. layer.contents = (__bridge id)currentFrame.CGImage;
  423. } else {
  424. // If we have no animation frames, call super implementation. iOS 14+ UIImageView use this delegate method for rendering.
  425. if ([UIImageView instancesRespondToSelector:@selector(displayLayer:)]) {
  426. [super displayLayer:layer];
  427. } else {
  428. // Fallback to implements the static image rendering by ourselves (like macOS or before iOS 14)
  429. currentFrame = super.image;
  430. layer.contentsScale = currentFrame.scale;
  431. layer.contents = (__bridge id)currentFrame.CGImage;
  432. }
  433. }
  434. }
  435. #if SD_UIKIT
  436. - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
  437. // See: #3635
  438. // From iOS 17, when UIImageView entering the background, it will receive the trait collection changes, and modify the CALayer.contents by `self.image.CGImage`
  439. // However, For animated image, `self.image.CGImge != self.currentFrame.CGImage`, right ?
  440. // So this cause the render issue, we need to reset the CALayer.contents again
  441. [super traitCollectionDidChange:previousTraitCollection];
  442. [self.imageViewLayer setNeedsDisplay];
  443. }
  444. #endif
  445. #if SD_MAC
  446. // NSImageView use a subview. We need this subview's layer for actual rendering.
  447. // Why using this design may because of properties like `imageAlignment` and `imageScaling`, which it's not available for UIImageView.contentMode (it's impossible to align left and keep aspect ratio at the same time)
  448. - (NSView *)imageView {
  449. NSImageView *imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageView));
  450. if (!imageView) {
  451. // macOS 10.14
  452. imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageSubview));
  453. }
  454. return imageView;
  455. }
  456. // on macOS, it's the imageView subview's layer (we use layer-hosting view to let CALayerDelegate works)
  457. - (CALayer *)imageViewLayer {
  458. NSView *imageView = self.imageView;
  459. if (!imageView) {
  460. return nil;
  461. }
  462. if (!_imageViewLayer) {
  463. _imageViewLayer = [CALayer new];
  464. _imageViewLayer.delegate = self;
  465. imageView.layer = _imageViewLayer;
  466. imageView.wantsLayer = YES;
  467. }
  468. return _imageViewLayer;
  469. }
  470. #else
  471. // on iOS, it's the imageView itself's layer
  472. - (CALayer *)imageViewLayer {
  473. return self.layer;
  474. }
  475. #endif
  476. @end
  477. #endif