Add support for imageNamed: in SDAnimatedImage with bundle files
This commit is contained in:
parent
7e83d78ca3
commit
5e09c6bf19
|
@ -66,7 +66,7 @@
|
|||
@note Normally we use `initWithData:scale:` to create custom animated image class. So you can implement your custom class without our built-in coder.
|
||||
|
||||
@param animatedCoder An animated coder which conform `SDWebImageAnimatedCoder` protocol
|
||||
@param scale The scale factor to assume when interpreting the image data. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property. (For `NSImage`, `scale` property can be calculated from `size`)
|
||||
@param scale The scale factor to assume when interpreting the image data. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property.
|
||||
@return An initialized object
|
||||
*/
|
||||
- (nullable instancetype)initWithAnimatedCoder:(nonnull id<SDWebImageAnimatedCoder>)animatedCoder scale:(CGFloat)scale;
|
||||
|
@ -76,7 +76,11 @@
|
|||
@interface SDAnimatedImage : UIImage <SDAnimatedImage>
|
||||
|
||||
// This class override these methods from UIImage(NSImage), and it supports NSSecureCoding.
|
||||
// You should use these methods to create a new animated image. Use other methods will just call super instead.
|
||||
// You should use these methods to create a new animated image. Use other methods just call super instead.
|
||||
+ (nullable instancetype)imageNamed:(nonnull NSString *)name; // Cache in memory, no Asset Catalog support
|
||||
#if __has_include(<UIKit/UITraitCollection.h>)
|
||||
+ (nullable instancetype)imageNamed:(nonnull NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection; // Cache in memory, no Asset Catalog support
|
||||
#endif
|
||||
+ (nullable instancetype)imageWithContentsOfFile:(nonnull NSString *)path;
|
||||
+ (nullable instancetype)imageWithData:(nonnull NSData *)data;
|
||||
+ (nullable instancetype)imageWithData:(nonnull NSData *)data scale:(CGFloat)scale;
|
||||
|
@ -94,12 +98,13 @@
|
|||
*/
|
||||
@property (nonatomic, copy, readonly, nullable) NSData *animatedImageData;
|
||||
|
||||
#if SD_MAC
|
||||
/**
|
||||
For AppKit, `NSImage` can contains multiple image representations with different scales. However, this class does not do that from the design. We processs the scale like UIKit and store it as a extra information for correctlly rendering in `SDAnimatedImageView`.
|
||||
The scale factor of the image.
|
||||
|
||||
@note For UIKit, this just call super instead.
|
||||
@note For AppKit, `NSImage` can contains multiple image representations with different scales. However, this class does not do that from the design. We processs the scale like UIKit and store it as a extra information for correctlly rendering in `SDAnimatedImageView`.
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat scale;
|
||||
#endif
|
||||
|
||||
/**
|
||||
Preload all frame image to memory. Then later request can directly return the frame for index without decoding.
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
#import "SDWebImageCodersManager.h"
|
||||
#import "SDWebImageFrame.h"
|
||||
|
||||
#define LOCK(...) dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER); \
|
||||
__VA_ARGS__; \
|
||||
dispatch_semaphore_signal(self->_lock);
|
||||
|
||||
static CGFloat SDImageScaleFromPath(NSString *string) {
|
||||
if (string.length == 0 || [string hasSuffix:@"/"]) return 1;
|
||||
NSString *name = string.stringByDeletingPathExtension;
|
||||
|
@ -28,6 +32,169 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
|
|||
return scale;
|
||||
}
|
||||
|
||||
static NSArray *SDBundlePreferredScales() {
|
||||
static NSArray *scales;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
#if SD_WATCH
|
||||
CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale;
|
||||
#elif SD_UIKIT
|
||||
CGFloat screenScale = [UIScreen mainScreen].scale;
|
||||
#elif SD_MAC
|
||||
CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor;
|
||||
#endif
|
||||
if (screenScale <= 1) {
|
||||
scales = @[@1,@2,@3];
|
||||
} else if (screenScale <= 2) {
|
||||
scales = @[@2,@3,@1];
|
||||
} else {
|
||||
scales = @[@3,@2,@1];
|
||||
}
|
||||
});
|
||||
return scales;
|
||||
}
|
||||
|
||||
#pragma mark - UIImage cache for bundle
|
||||
|
||||
// Apple parse the Asset Catalog compiled file(`Assets.car`) by CoreUI.framework, however it's a private framework and there are no other ways to directly get the data. So we just process the normal bundle files :)
|
||||
|
||||
@interface SDImageAssetManager : NSObject {
|
||||
dispatch_semaphore_t _lock;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) NSMapTable<NSString *, UIImage *> *imageTable;
|
||||
|
||||
+ (instancetype)sharedAssetManager;
|
||||
- (nullable NSString *)getPathForName:(nonnull NSString *)name bundle:(nonnull NSBundle *)bundle preferredScale:(CGFloat *)scale;
|
||||
- (nullable UIImage *)imageForName:(nonnull NSString *)name;
|
||||
- (void)storeImage:(nonnull UIImage *)image forName:(nonnull NSString *)name;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SDImageAssetManager
|
||||
|
||||
+ (instancetype)sharedAssetManager {
|
||||
static dispatch_once_t onceToken;
|
||||
static SDImageAssetManager *assetManager;
|
||||
dispatch_once(&onceToken, ^{
|
||||
assetManager = [[SDImageAssetManager alloc] init];
|
||||
});
|
||||
return assetManager;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
NSPointerFunctionsOptions valueOptions;
|
||||
#if SD_MAC
|
||||
// Apple says that NSImage use a weak reference to value
|
||||
valueOptions = NSPointerFunctionsWeakMemory;
|
||||
#else
|
||||
// Apple says that UIImage use a strong reference to value
|
||||
valueOptions = NSPointerFunctionsStrongMemory;
|
||||
#endif
|
||||
_imageTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsCopyIn valueOptions:valueOptions];
|
||||
_lock = dispatch_semaphore_create(1);
|
||||
#if SD_UIKIT
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
#if SD_UIKIT
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
|
||||
LOCK({
|
||||
[self.imageTable removeAllObjects];
|
||||
});
|
||||
}
|
||||
|
||||
- (NSString *)getPathForName:(NSString *)name bundle:(NSBundle *)bundle preferredScale:(CGFloat *)scale {
|
||||
NSParameterAssert(name);
|
||||
NSParameterAssert(bundle);
|
||||
NSString *path;
|
||||
if (name.length == 0) {
|
||||
return path;
|
||||
}
|
||||
if ([name hasSuffix:@"/"]) {
|
||||
return path;
|
||||
}
|
||||
NSString *extension = name.pathExtension;
|
||||
if (extension.length == 0) {
|
||||
// If no extension, follow Apple's doc, check PNG format
|
||||
extension = @"png";
|
||||
}
|
||||
name = [name stringByDeletingPathExtension];
|
||||
|
||||
CGFloat providedScale = *scale;
|
||||
NSArray *scales = SDBundlePreferredScales();
|
||||
|
||||
// Check if file name contains scale
|
||||
for (size_t i = 0; i < scales.count; i++) {
|
||||
NSNumber *scaleValue = scales[i];
|
||||
if ([name hasSuffix:[NSString stringWithFormat:@"@%@x", scaleValue]]) {
|
||||
path = [bundle pathForResource:name ofType:extension];
|
||||
if (path) {
|
||||
*scale = scaleValue.doubleValue; // override
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search with provided scale first
|
||||
if (providedScale != 0) {
|
||||
NSString *scaledName = [name stringByAppendingFormat:@"@%@x", @(providedScale)];
|
||||
path = [bundle pathForResource:scaledName ofType:extension];
|
||||
if (path) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Search with preferred scale
|
||||
for (size_t i = 0; i < scales.count; i++) {
|
||||
NSNumber *scaleValue = scales[i];
|
||||
if (scaleValue.doubleValue == providedScale) {
|
||||
// Ignore provided scale
|
||||
continue;
|
||||
}
|
||||
NSString *scaledName = [name stringByAppendingFormat:@"@%@x", scaleValue];
|
||||
path = [bundle pathForResource:scaledName ofType:extension];
|
||||
if (path) {
|
||||
*scale = scaleValue.doubleValue; // override
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Search without scale
|
||||
path = [bundle pathForResource:name ofType:extension];
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
- (UIImage *)imageForName:(NSString *)name {
|
||||
NSParameterAssert(name);
|
||||
UIImage *image;
|
||||
LOCK({
|
||||
image = [self.imageTable objectForKey:name];
|
||||
});
|
||||
return image;
|
||||
}
|
||||
|
||||
- (void)storeImage:(UIImage *)image forName:(NSString *)name {
|
||||
NSParameterAssert(image);
|
||||
NSParameterAssert(name);
|
||||
LOCK({
|
||||
[self.imageTable setObject:image forKey:name];
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface SDAnimatedImage ()
|
||||
|
||||
@property (nonatomic, strong) id<SDWebImageAnimatedCoder> coder;
|
||||
|
@ -38,6 +205,9 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
|
|||
@end
|
||||
|
||||
@implementation SDAnimatedImage
|
||||
#if SD_UIKIT || SD_WATCH
|
||||
@dynamic scale; // call super
|
||||
#endif
|
||||
|
||||
#pragma mark - Dealloc & Memory warning
|
||||
|
||||
|
@ -55,6 +225,53 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
|
|||
}
|
||||
|
||||
#pragma mark - UIImage override method
|
||||
+ (instancetype)imageNamed:(NSString *)name {
|
||||
#if __has_include(<UIKit/UITraitCollection.h>)
|
||||
return [self imageNamed:name inBundle:nil compatibleWithTraitCollection:nil];
|
||||
#else
|
||||
return [self imageNamed:name inBundle:nil scale:0];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if __has_include(<UIKit/UITraitCollection.h>)
|
||||
+ (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle compatibleWithTraitCollection:(UITraitCollection *)traitCollection {
|
||||
if (!traitCollection) {
|
||||
traitCollection = UIScreen.mainScreen.traitCollection;
|
||||
}
|
||||
CGFloat scale = traitCollection.displayScale;
|
||||
return [self imageNamed:name inBundle:bundle scale:scale];
|
||||
}
|
||||
#endif
|
||||
|
||||
// 0 scale means automatically check
|
||||
+ (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle scale:(CGFloat)scale {
|
||||
if (!name) {
|
||||
return nil;
|
||||
}
|
||||
if (!bundle) {
|
||||
bundle = [NSBundle mainBundle];
|
||||
}
|
||||
SDImageAssetManager *assetManager = [SDImageAssetManager sharedAssetManager];
|
||||
SDAnimatedImage *image = (SDAnimatedImage *)[assetManager imageForName:name];
|
||||
if ([image isKindOfClass:[SDAnimatedImage class]]) {
|
||||
return image;
|
||||
}
|
||||
NSString *path = [assetManager getPathForName:name bundle:bundle preferredScale:&scale];
|
||||
if (!path) {
|
||||
return image;
|
||||
}
|
||||
NSData *data = [NSData dataWithContentsOfFile:path];
|
||||
if (!data) {
|
||||
return image;
|
||||
}
|
||||
image = [[self alloc] initWithData:data scale:scale];
|
||||
if (image) {
|
||||
[assetManager storeImage:image forName:name];
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
+ (instancetype)imageWithContentsOfFile:(NSString *)path {
|
||||
return [[self alloc] initWithContentsOfFile:path];
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
|
||||
#if SD_UIKIT || SD_MAC
|
||||
|
||||
#import "SDAnimatedImage.h"
|
||||
|
||||
/**
|
||||
A drop-in replacement for UIImageView/NSImageView, you can use this for animated image rendering.
|
||||
Call `setImage:` with a `UIImage<SDAnimatedImage>` will start animated image rendering. Call with a UIImage(NSImage) will back to normal UIImageView(NSImageView) rendering
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
*/
|
||||
|
||||
#import "SDAnimatedImageView.h"
|
||||
|
||||
#if SD_UIKIT || SD_MAC
|
||||
|
||||
#import "UIImage+WebCache.h"
|
||||
#import "NSImage+Additions.h"
|
||||
#if SD_UIKIT || SD_MAC
|
||||
#import "SDAnimatedImage.h"
|
||||
#import <mach/mach.h>
|
||||
|
||||
#if SD_MAC
|
||||
|
|
|
@ -50,7 +50,7 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustom
|
|||
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomTransformer;
|
||||
|
||||
/**
|
||||
A Class object which the instance is a `UIImage/NSImage` subclass and adopt `SDAnimatedImage` protocol. And call `initWithData:scale:` to create the instance. If the instance create failed, fallback to normal `UIImage/NSImage`.
|
||||
A Class object which the instance is a `UIImage/NSImage` subclass and adopt `SDAnimatedImage` protocol. We will call `initWithData:scale:` to create the instance (or `initWithAnimatedCoder:sclae` when using progressive download) . If the instance create failed, fallback to normal `UIImage/NSImage`.
|
||||
This can be used to improve animated images rendering performance (especially memory usage on big animated images) with `SDAnimatedImageView` (Class).
|
||||
*/
|
||||
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextAnimatedImageClass;
|
||||
|
|
|
@ -61,7 +61,13 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
|
|||
// enough, other can be test with InitWithData
|
||||
}
|
||||
|
||||
- (void)test04AnimatedImagePreloadFrames {
|
||||
- (void)test04AnimatedImageImageNamed {
|
||||
SDAnimatedImage *image = [SDAnimatedImage imageNamed:@"TestImage.gif" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
|
||||
expect(image).notTo.beNil();
|
||||
expect([image.animatedImageData isEqualToData:[self testGIFData]]).beTruthy();
|
||||
}
|
||||
|
||||
- (void)test05AnimatedImagePreloadFrames {
|
||||
NSData *validData = [self testGIFData];
|
||||
SDAnimatedImage *image = [SDAnimatedImage imageWithData:validData];
|
||||
|
||||
|
@ -76,7 +82,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
|
|||
expect(frame).notTo.beNil();
|
||||
}
|
||||
|
||||
- (void)test05AnimatedImageViewSetImage {
|
||||
- (void)test06AnimatedImageViewSetImage {
|
||||
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
|
||||
UIImage *image = [UIImage imageWithData:[self testJPEGData]];
|
||||
imageView.image = image;
|
||||
|
@ -84,7 +90,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
|
|||
expect(imageView.currentFrame).beNil(); // current frame
|
||||
}
|
||||
|
||||
- (void)test06AnimatedImageViewSetAnimatedImage {
|
||||
- (void)test07AnimatedImageViewSetAnimatedImage {
|
||||
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
|
||||
SDAnimatedImage *image = [SDAnimatedImage imageWithData:[self testAnimatedWebPData]];
|
||||
imageView.image = image;
|
||||
|
@ -92,7 +98,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
|
|||
expect(imageView.currentFrame).notTo.beNil(); // current frame
|
||||
}
|
||||
|
||||
- (void)test07AnimatedImageViewRendering {
|
||||
- (void)test08AnimatedImageViewRendering {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView rendering"];
|
||||
SDAnimatedImageView *imageView = [[SDAnimatedImageView alloc] init];
|
||||
[self.window addSubview:imageView];
|
||||
|
@ -126,7 +132,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
|
|||
[self waitForExpectationsWithCommonTimeout];
|
||||
}
|
||||
|
||||
- (void)test08AnimatedImageViewSetProgressiveAnimatedImage {
|
||||
- (void)test09AnimatedImageViewSetProgressiveAnimatedImage {
|
||||
NSData *gifData = [self testGIFData];
|
||||
SDWebImageGIFCoder *progressiveCoder = [[SDWebImageGIFCoder alloc] initIncremental];
|
||||
// simulate progressive decode, pass partial data
|
||||
|
@ -153,7 +159,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
|
|||
expect(isProgressive).equal(NO);
|
||||
}
|
||||
|
||||
- (void)test09AnimatedImageViewCategory {
|
||||
- (void)test10AnimatedImageViewCategory {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView view category"];
|
||||
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
|
||||
NSURL *testURL = [NSURL URLWithString:kTestWebPURL];
|
||||
|
@ -166,7 +172,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
|
|||
[self waitForExpectationsWithCommonTimeout];
|
||||
}
|
||||
|
||||
- (void)test10AnimatedImageViewCategoryProgressive {
|
||||
- (void)test11AnimatedImageViewCategoryProgressive {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView view category"];
|
||||
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
|
||||
NSURL *testURL = [NSURL URLWithString:kTestGIFURL];
|
||||
|
|
Loading…
Reference in New Issue