/*
|
* This file is part of the SDWebImage package.
|
* (c) Olivier Poitrey <rs@dailymotion.com>
|
*
|
* For the full copyright and license information, please view the LICENSE
|
* file that was distributed with this source code.
|
*/
|
|
#import "SDAnimatedImageView.h"
|
|
#if SD_UIKIT || SD_MAC
|
|
#import "UIImage+Metadata.h"
|
#import "NSImage+Compatibility.h"
|
#import "SDInternalMacros.h"
|
#import "objc/runtime.h"
|
|
// A wrapper to implements the transformer on animated image, like tint color
|
@interface SDAnimatedImageFrameProvider : NSObject <SDAnimatedImageProvider>
|
@property (nonatomic, strong) id<SDAnimatedImageProvider> provider;
|
@property (nonatomic, strong) id<SDImageTransformer> transformer;
|
@end
|
|
@implementation SDAnimatedImageFrameProvider
|
|
- (instancetype)initWithProvider:(id<SDAnimatedImageProvider>)provider transformer:(id<SDImageTransformer>)transformer {
|
self = [super init];
|
if (self) {
|
_provider = provider;
|
_transformer = transformer;
|
}
|
return self;
|
}
|
|
- (NSUInteger)hash {
|
NSUInteger prime = 31;
|
NSUInteger result = 1;
|
NSUInteger providerHash = self.provider.hash;
|
NSUInteger transformerHash = self.transformer.transformerKey.hash;
|
result = prime * result + providerHash;
|
result = prime * result + transformerHash;
|
return result;
|
}
|
|
- (BOOL)isEqual:(id)object {
|
if (nil == object) {
|
return NO;
|
}
|
if (self == object) {
|
return YES;
|
}
|
if (![object isKindOfClass:[self class]]) {
|
return NO;
|
}
|
return self.provider == [object provider]
|
&& [self.transformer.transformerKey isEqualToString:[object transformer].transformerKey];
|
}
|
|
- (NSData *)animatedImageData {
|
return self.provider.animatedImageData;
|
}
|
|
- (NSUInteger)animatedImageFrameCount {
|
return self.provider.animatedImageFrameCount;
|
}
|
|
- (NSUInteger)animatedImageLoopCount {
|
return self.provider.animatedImageLoopCount;
|
}
|
|
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
|
return [self.provider animatedImageDurationAtIndex:index];
|
}
|
|
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
|
UIImage *frame = [self.provider animatedImageFrameAtIndex:index];
|
return [self.transformer transformedImageWithImage:frame forKey:@""];
|
}
|
|
@end
|
|
@interface UIImageView () <CALayerDelegate>
|
@end
|
|
@interface SDAnimatedImageView () {
|
BOOL _initFinished; // Extra flag to mark the `commonInit` is called
|
NSRunLoopMode _runLoopMode;
|
NSUInteger _maxBufferSize;
|
double _playbackRate;
|
SDAnimatedImagePlaybackMode _playbackMode;
|
}
|
|
@property (nonatomic, strong, readwrite) SDAnimatedImagePlayer *player;
|
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
|
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
|
@property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
|
@property (nonatomic, assign) BOOL shouldAnimate;
|
@property (nonatomic, assign) BOOL isProgressive;
|
@property (nonatomic) CALayer *imageViewLayer; // The actual rendering layer.
|
|
@end
|
|
@implementation SDAnimatedImageView
|
#if SD_UIKIT
|
@dynamic animationRepeatCount; // we re-use this property from `UIImageView` super class on iOS.
|
#endif
|
|
#pragma mark - Initializers
|
|
#if SD_MAC
|
+ (instancetype)imageViewWithImage:(NSImage *)image
|
{
|
NSRect frame = NSMakeRect(0, 0, image.size.width, image.size.height);
|
SDAnimatedImageView *imageView = [[SDAnimatedImageView alloc] initWithFrame:frame];
|
[imageView setImage:image];
|
return imageView;
|
}
|
#else
|
// -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;
|
}
|
#endif
|
|
- (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
|
{
|
// Pay attention that UIKit's `initWithImage:` will trigger a `setImage:` during initialization before this `commonInit`.
|
// So the properties which rely on this order, should using lazy-evaluation or do extra check in `setImage:`.
|
self.autoPlayAnimatedImage = YES;
|
self.shouldCustomLoopCount = NO;
|
self.shouldIncrementalLoad = YES;
|
self.playbackRate = 1.0;
|
#if SD_MAC
|
self.wantsLayer = YES;
|
#endif
|
// Mark commonInit finished
|
_initFinished = YES;
|
}
|
|
#pragma mark - Accessors
|
#pragma mark Public
|
|
- (void)setImage:(UIImage *)image
|
{
|
if (self.image == image) {
|
return;
|
}
|
|
// Check Progressive rendering
|
[self updateIsProgressiveWithImage:image];
|
|
if (!self.isProgressive) {
|
// Stop animating
|
self.player = nil;
|
self.currentFrame = nil;
|
self.currentFrameIndex = 0;
|
self.currentLoopCount = 0;
|
}
|
|
// 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.
|
super.image = image;
|
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)] && [(id<SDAnimatedImage>)image animatedImageFrameCount] > 1) {
|
if (!self.player) {
|
id<SDAnimatedImageProvider> provider;
|
// Check progressive loading
|
if (self.isProgressive) {
|
provider = [(id<SDAnimatedImage>)image animatedCoder];
|
} else {
|
provider = (id<SDAnimatedImage>)image;
|
}
|
// Create animated player
|
if (self.animationTransformer) {
|
// Check if post-transform animation available
|
provider = [[SDAnimatedImageFrameProvider alloc] initWithProvider:provider transformer:self.animationTransformer];
|
self.player = [SDAnimatedImagePlayer playerWithProvider:provider];
|
} else {
|
// Normal animation without post-transform
|
self.player = [SDAnimatedImagePlayer playerWithProvider:provider];
|
}
|
} else {
|
// Update Frame Count
|
self.player.totalFrameCount = [(id<SDAnimatedImage>)image animatedImageFrameCount];
|
}
|
|
if (!self.player) {
|
// animated player nil means the image format is not supported, or frame count <= 1
|
return;
|
}
|
|
// Custom Loop Count
|
if (self.shouldCustomLoopCount) {
|
self.player.totalLoopCount = self.animationRepeatCount;
|
}
|
|
// RunLoop Mode
|
self.player.runLoopMode = self.runLoopMode;
|
|
// Max Buffer Size
|
self.player.maxBufferSize = self.maxBufferSize;
|
|
// Play Rate
|
self.player.playbackRate = self.playbackRate;
|
|
// Play Mode
|
self.player.playbackMode = self.playbackMode;
|
|
// Setup handler
|
@weakify(self);
|
self.player.animationFrameHandler = ^(NSUInteger index, UIImage * frame) {
|
@strongify(self);
|
self.currentFrameIndex = index;
|
self.currentFrame = frame;
|
[self.imageViewLayer setNeedsDisplay];
|
};
|
self.player.animationLoopHandler = ^(NSUInteger loopCount) {
|
@strongify(self);
|
// Progressive image reach the current last frame index. Keep the state and pause animating. Wait for later restart
|
if (self.isProgressive) {
|
NSUInteger lastFrameIndex = self.player.totalFrameCount - 1;
|
[self.player seekToFrameAtIndex:lastFrameIndex loopCount:0];
|
[self.player pausePlaying];
|
} else {
|
self.currentLoopCount = loopCount;
|
}
|
};
|
|
// Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
|
super.highlighted = NO;
|
|
[self stopAnimating];
|
[self checkPlay];
|
}
|
[self.imageViewLayer setNeedsDisplay];
|
}
|
|
#pragma mark - Configuration
|
|
- (void)setRunLoopMode:(NSRunLoopMode)runLoopMode
|
{
|
_runLoopMode = [runLoopMode copy];
|
self.player.runLoopMode = runLoopMode;
|
}
|
|
- (NSRunLoopMode)runLoopMode
|
{
|
if (!_runLoopMode) {
|
_runLoopMode = [[self class] defaultRunLoopMode];
|
}
|
return _runLoopMode;
|
}
|
|
+ (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;
|
}
|
|
- (void)setMaxBufferSize:(NSUInteger)maxBufferSize
|
{
|
_maxBufferSize = maxBufferSize;
|
self.player.maxBufferSize = maxBufferSize;
|
}
|
|
- (NSUInteger)maxBufferSize {
|
return _maxBufferSize; // Defaults to 0
|
}
|
|
- (void)setPlaybackRate:(double)playbackRate
|
{
|
_playbackRate = playbackRate;
|
self.player.playbackRate = playbackRate;
|
}
|
|
- (double)playbackRate
|
{
|
if (!_initFinished) {
|
return 1.0; // Defaults to 1.0
|
}
|
return _playbackRate;
|
}
|
|
- (void)setPlaybackMode:(SDAnimatedImagePlaybackMode)playbackMode {
|
_playbackMode = playbackMode;
|
self.player.playbackMode = playbackMode;
|
}
|
|
- (SDAnimatedImagePlaybackMode)playbackMode {
|
if (!_initFinished) {
|
return SDAnimatedImagePlaybackModeNormal; // Default mode is normal
|
}
|
return _playbackMode;
|
}
|
|
|
- (BOOL)shouldIncrementalLoad
|
{
|
if (!_initFinished) {
|
return YES; // Defaults to YES
|
}
|
return _initFinished;
|
}
|
|
#pragma mark - UIView Method Overrides
|
#pragma mark Observing View-Related Changes
|
|
#if SD_MAC
|
- (void)viewDidMoveToSuperview
|
#else
|
- (void)didMoveToSuperview
|
#endif
|
{
|
#if SD_MAC
|
[super viewDidMoveToSuperview];
|
#else
|
[super didMoveToSuperview];
|
#endif
|
|
[self checkPlay];
|
}
|
|
#if SD_MAC
|
- (void)viewDidMoveToWindow
|
#else
|
- (void)didMoveToWindow
|
#endif
|
{
|
#if SD_MAC
|
[super viewDidMoveToWindow];
|
#else
|
[super didMoveToWindow];
|
#endif
|
|
[self checkPlay];
|
}
|
|
#if SD_MAC
|
- (void)setAlphaValue:(CGFloat)alphaValue
|
#else
|
- (void)setAlpha:(CGFloat)alpha
|
#endif
|
{
|
#if SD_MAC
|
[super setAlphaValue:alphaValue];
|
#else
|
[super setAlpha:alpha];
|
#endif
|
|
[self checkPlay];
|
}
|
|
- (void)setHidden:(BOOL)hidden
|
{
|
[super setHidden:hidden];
|
|
[self checkPlay];
|
}
|
|
#pragma mark - UIImageView Method Overrides
|
#pragma mark Image Data
|
|
- (void)setAnimationRepeatCount:(NSInteger)animationRepeatCount
|
{
|
#if SD_UIKIT
|
[super setAnimationRepeatCount:animationRepeatCount];
|
#else
|
_animationRepeatCount = animationRepeatCount;
|
#endif
|
|
if (self.shouldCustomLoopCount) {
|
self.player.totalLoopCount = animationRepeatCount;
|
}
|
}
|
|
- (void)startAnimating
|
{
|
if (self.player) {
|
[self updateShouldAnimate];
|
if (self.shouldAnimate) {
|
[self.player startPlaying];
|
}
|
} else {
|
#if SD_UIKIT
|
[super startAnimating];
|
#else
|
[super setAnimates:YES];
|
#endif
|
}
|
}
|
|
- (void)stopAnimating
|
{
|
if (self.player) {
|
if (self.resetFrameIndexWhenStopped) {
|
[self.player stopPlaying];
|
} else {
|
[self.player pausePlaying];
|
}
|
if (self.clearBufferWhenStopped) {
|
[self.player clearFrameBuffer];
|
}
|
} else {
|
#if SD_UIKIT
|
[super stopAnimating];
|
#else
|
[super setAnimates:NO];
|
#endif
|
}
|
}
|
|
#if SD_UIKIT
|
- (BOOL)isAnimating
|
{
|
if (self.player) {
|
return self.player.isPlaying;
|
} else {
|
return [super isAnimating];
|
}
|
}
|
#endif
|
|
#if SD_MAC
|
- (BOOL)animates
|
{
|
if (self.player) {
|
return self.player.isPlaying;
|
} else {
|
return [super animates];
|
}
|
}
|
|
- (void)setAnimates:(BOOL)animates
|
{
|
if (animates) {
|
[self startAnimating];
|
} else {
|
[self stopAnimating];
|
}
|
}
|
#endif
|
|
#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.player) {
|
[super setHighlighted:highlighted];
|
}
|
}
|
|
|
#pragma mark - Private Methods
|
#pragma mark Animation
|
|
/// Check if it should be played
|
- (void)checkPlay
|
{
|
// Only handle for SDAnimatedImage, leave UIAnimatedImage or animationImages for super implementation control
|
if (self.player && self.autoPlayAnimatedImage) {
|
[self updateShouldAnimate];
|
if (self.shouldAnimate) {
|
[self startAnimating];
|
} else {
|
[self stopAnimating];
|
}
|
}
|
}
|
|
// 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
|
{
|
#if SD_MAC
|
BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alphaValue > 0.0;
|
#else
|
BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
|
#endif
|
self.shouldAnimate = self.player && isVisible;
|
}
|
|
// Update progressive status only after `setImage:` call.
|
- (void)updateIsProgressiveWithImage:(UIImage *)image
|
{
|
self.isProgressive = NO;
|
if (!self.shouldIncrementalLoad) {
|
// Early return
|
return;
|
}
|
// We must use `image.class conformsToProtocol:` instead of `image conformsToProtocol:` here
|
// Because UIKit on macOS, using internal hard-coded override method, which returns NO
|
id<SDAnimatedImageCoder> currentAnimatedCoder = [self progressiveAnimatedCoderForImage:image];
|
if (currentAnimatedCoder) {
|
UIImage *previousImage = self.image;
|
if (!previousImage) {
|
// If current animated coder supports progressive, and no previous image to check, start progressive loading
|
self.isProgressive = YES;
|
} else {
|
id<SDAnimatedImageCoder> previousAnimatedCoder = [self progressiveAnimatedCoderForImage:previousImage];
|
if (previousAnimatedCoder == currentAnimatedCoder) {
|
// If current animated coder is the same as previous, start progressive loading
|
self.isProgressive = YES;
|
}
|
}
|
}
|
}
|
|
// Check if image can represent a `Progressive Animated Image` during loading
|
- (id<SDAnimatedImageCoder, SDProgressiveImageCoder>)progressiveAnimatedCoderForImage:(UIImage *)image
|
{
|
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)] && image.sd_isIncremental && [image respondsToSelector:@selector(animatedCoder)]) {
|
id<SDAnimatedImageCoder> animatedCoder = [(id<SDAnimatedImage>)image animatedCoder];
|
if ([animatedCoder respondsToSelector:@selector(initIncrementalWithOptions:)]) {
|
return (id<SDAnimatedImageCoder, SDProgressiveImageCoder>)animatedCoder;
|
}
|
}
|
return nil;
|
}
|
|
|
#pragma mark Providing the Layer's Content
|
#pragma mark - CALayerDelegate
|
|
- (void)displayLayer:(CALayer *)layer
|
{
|
UIImage *currentFrame = self.currentFrame;
|
if (currentFrame) {
|
layer.contentsScale = currentFrame.scale;
|
layer.contents = (__bridge id)currentFrame.CGImage;
|
} else {
|
// If we have no animation frames, call super implementation. iOS 14+ UIImageView use this delegate method for rendering.
|
if ([UIImageView instancesRespondToSelector:@selector(displayLayer:)]) {
|
[super displayLayer:layer];
|
} else {
|
// Fallback to implements the static image rendering by ourselves (like macOS or before iOS 14)
|
currentFrame = super.image;
|
layer.contentsScale = currentFrame.scale;
|
layer.contents = (__bridge id)currentFrame.CGImage;
|
}
|
}
|
}
|
|
#if SD_UIKIT
|
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
// See: #3635
|
// From iOS 17, when UIImageView entering the background, it will receive the trait collection changes, and modify the CALayer.contents by `self.image.CGImage`
|
// However, For animated image, `self.image.CGImge != self.currentFrame.CGImage`, right ?
|
// So this cause the render issue, we need to reset the CALayer.contents again
|
[super traitCollectionDidChange:previousTraitCollection];
|
[self.imageViewLayer setNeedsDisplay];
|
}
|
#endif
|
|
#if SD_MAC
|
// NSImageView use a subview. We need this subview's layer for actual rendering.
|
// 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)
|
- (NSView *)imageView {
|
NSImageView *imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageView));
|
if (!imageView) {
|
// macOS 10.14
|
imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageSubview));
|
}
|
return imageView;
|
}
|
|
// on macOS, it's the imageView subview's layer (we use layer-hosting view to let CALayerDelegate works)
|
- (CALayer *)imageViewLayer {
|
NSView *imageView = self.imageView;
|
if (!imageView) {
|
return nil;
|
}
|
if (!_imageViewLayer) {
|
_imageViewLayer = [CALayer new];
|
_imageViewLayer.delegate = self;
|
imageView.layer = _imageViewLayer;
|
imageView.wantsLayer = YES;
|
}
|
return _imageViewLayer;
|
}
|
#else
|
// on iOS, it's the imageView itself's layer
|
- (CALayer *)imageViewLayer {
|
return self.layer;
|
}
|
|
#endif
|
|
@end
|
|
#endif
|