From 41dc9bef7b85a08bf02bbc8f1f8af4a7ea9aa275 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 19 Oct 2024 23:45:26 +0800 Subject: [PATCH] Added `animationTransformer` on SDAnimatedImageView This allows the animated image to apply post-transform Currently we don't change the old `SDWebImageTransformAnimatedImage` behavior. This feature is opt-in --- .../SDWebImage Demo/DetailViewController.m | 34 +++++++++ SDWebImage/Core/SDAnimatedImageView.h | 14 ++++ SDWebImage/Core/SDAnimatedImageView.m | 73 ++++++++++++++++++- SDWebImage/Core/SDImageTransformer.h | 2 + SDWebImage/Private/SDImageFramePool.m | 3 +- 5 files changed, 124 insertions(+), 2 deletions(-) diff --git a/Examples/SDWebImage Demo/DetailViewController.m b/Examples/SDWebImage Demo/DetailViewController.m index 37a02f0a..e0966d26 100644 --- a/Examples/SDWebImage Demo/DetailViewController.m +++ b/Examples/SDWebImage Demo/DetailViewController.m @@ -12,6 +12,7 @@ @interface DetailViewController () @property (strong, nonatomic) IBOutlet SDAnimatedImageView *imageView; +@property (assign) BOOL tintApplied; @end @@ -37,6 +38,39 @@ style:UIBarButtonItemStylePlain target:self action:@selector(toggleAnimation:)]; + // Add a secret title click action to apply tint color + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + [button addTarget:self + action:@selector(toggleTint:) + forControlEvents:UIControlEventTouchUpInside]; + [button setTitle:@"Tint" forState:UIControlStateNormal]; + self.navigationItem.titleView = button; +} + +- (void)toggleTint:(UIResponder *)sender { + // tint for non-opaque animation + if (!self.imageView.isAnimating) { + return; + } + SDAnimatedImage *animatedImage = (SDAnimatedImage *)self.imageView.image; + if (animatedImage.sd_imageFormat == SDImageFormatGIF) { + // GIF is opaque + return; + } + BOOL containsAlpha = [SDImageCoderHelper CGImageContainsAlpha:animatedImage.CGImage]; + if (!containsAlpha) { + return; + } + if (self.tintApplied) { + self.imageView.animationTransformer = nil; + } else { + self.imageView.animationTransformer = [SDImageTintTransformer transformerWithColor:UIColor.blackColor]; + } + self.tintApplied = !self.tintApplied; + // refresh + UIImage *image = self.imageView.image; + self.imageView.image = nil; + self.imageView.image = image; } - (void)toggleAnimation:(UIResponder *)sender { diff --git a/SDWebImage/Core/SDAnimatedImageView.h b/SDWebImage/Core/SDAnimatedImageView.h index 431be921..73141e38 100644 --- a/SDWebImage/Core/SDAnimatedImageView.h +++ b/SDWebImage/Core/SDAnimatedImageView.h @@ -12,6 +12,7 @@ #import "SDAnimatedImage.h" #import "SDAnimatedImagePlayer.h" +#import "SDImageTransformer.h" /** A drop-in replacement for UIImageView/NSImageView, you can use this for animated image rendering. @@ -28,6 +29,19 @@ NS_SWIFT_UI_ACTOR */ @property (nonatomic, strong, readonly, nullable) SDAnimatedImagePlayer *player; +/** + The transformer for each decoded animated image frame. + We supports post-transform on animated image frame from version 5.20. + When you configure the transformer on `SDAnimatedImageView` and animation is playing, the `transformedImageWithImage:forKey:` will be called just after the frame is decoded. (The `key` arg is always empty for backward-compatible) + + Example to tint the animated image with alpha channel into template: + * @code + imageView.animationTransformer = [SDImageTintTransformer transformerWithColor:UIColor.blackColor]; + * @endcode + @note The `transformerKey` property is used to ensure the buffer cache available. So make sure it's correct value match the actual logic on transformer. + */ +@property (nonatomic, strong, nullable) id animationTransformer; + /** Current display frame image. This value is KVO Compliance. */ diff --git a/SDWebImage/Core/SDAnimatedImageView.m b/SDWebImage/Core/SDAnimatedImageView.m index 7e733672..193652df 100644 --- a/SDWebImage/Core/SDAnimatedImageView.m +++ b/SDWebImage/Core/SDAnimatedImageView.m @@ -15,6 +15,70 @@ #import "SDInternalMacros.h" #import "objc/runtime.h" +// A wrapper to implements the transformer on animated image, like tint color +@interface SDAnimatedImageFrameProvider : NSObject +@property (nonatomic, strong) id provider; +@property (nonatomic, strong) id transformer; +@end + +@implementation SDAnimatedImageFrameProvider + +- (instancetype)initWithProvider:(id)provider transformer:(id)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 () @end @@ -139,7 +203,14 @@ provider = (id)image; } // Create animated player - self.player = [SDAnimatedImagePlayer playerWithProvider:provider]; + 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)image animatedImageFrameCount]; diff --git a/SDWebImage/Core/SDImageTransformer.h b/SDWebImage/Core/SDImageTransformer.h index e01550a6..ed8175d0 100644 --- a/SDWebImage/Core/SDImageTransformer.h +++ b/SDWebImage/Core/SDImageTransformer.h @@ -31,6 +31,7 @@ FOUNDATION_EXPORT NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullab /** A transformer protocol to transform the image load from cache or from download. You can provide transformer to cache and manager (Through the `transformer` property or context option `SDWebImageContextImageTransformer`). + From v5.20, the transformer class also can be used on animated image frame post-transform logic, see `SDAnimatedImageView`. @note The transform process is called from a global queue in order to not to block the main queue. */ @@ -50,6 +51,7 @@ FOUNDATION_EXPORT NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullab @required /** For each transformer, it must contains its cache key to used to store the image cache or query from the cache. This key will be appened after the original cache key generated by URL or from user. + Which means, the cache should match what your transformer logic do. The same `input image` + `transformer key`, should always generate the same `output image`. @return The cache key to appended after the original cache key. Should not be nil. */ diff --git a/SDWebImage/Private/SDImageFramePool.m b/SDWebImage/Private/SDImageFramePool.m index 1eb2cdb3..8e133d22 100644 --- a/SDWebImage/Private/SDImageFramePool.m +++ b/SDWebImage/Private/SDImageFramePool.m @@ -31,7 +31,8 @@ SD_LOCK_DECLARE_STATIC(_providerFramePoolMapLock); static NSMapTable *providerFramePoolMap; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - providerFramePoolMap = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; + // Key use `hash` && `isEqual:` + providerFramePoolMap = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; }); return providerFramePoolMap; }