react-native-fast-image 8.11.0 → 8.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageGif.java +37 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +5 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +35 -2
- package/dist/index.cjs.js.flow +2 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js.flow +2 -0
- package/ios/FastImage/FFFDownsampledImage.h +28 -0
- package/ios/FastImage/FFFDownsampledImage.m +169 -0
- package/ios/FastImage/FFFastImageView.h +4 -0
- package/ios/FastImage/FFFastImageView.m +218 -41
- package/ios/FastImage/FFFastImageViewManager.m +2 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -196,6 +196,22 @@ How the image is filtered when it's drawn smaller or larger than its size (like
|
|
|
196
196
|
|
|
197
197
|
---
|
|
198
198
|
|
|
199
|
+
### `paused?: boolean`
|
|
200
|
+
|
|
201
|
+
Pauses an animated image (GIF, and animated WebP on iOS) on the frame it's showing; `false` plays it again from there. Each image animates on its own, so pausing one doesn't pause others showing the same file.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### `downsample?: boolean`
|
|
206
|
+
|
|
207
|
+
iOS only. Decodes a large image at about the size it's shown at, instead of at full size, so it takes much less memory.
|
|
208
|
+
|
|
209
|
+
Use it when you show images much larger than their views and can't get them at the right size, e.g. user uploads or other people's URLs in a list. If you control the images, serve them at the size they're shown instead (resized on your server or by an image CDN), which also saves bandwidth.
|
|
210
|
+
|
|
211
|
+
Decoding a smaller copy can take a little longer, so use it where the memory matters. Needs SDWebImage 5.19.7 or later. On Android images are already decoded at about the view's size.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
199
215
|
### `onLoadStart?: () => void`
|
|
200
216
|
|
|
201
217
|
Called when the image starts to load.
|
|
@@ -18,6 +18,7 @@ import com.bumptech.glide.load.resource.bitmap.BitmapResource;
|
|
|
18
18
|
import com.bumptech.glide.load.resource.gif.GifBitmapProvider;
|
|
19
19
|
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
|
20
20
|
|
|
21
|
+
import java.lang.reflect.Field;
|
|
21
22
|
import java.nio.ByteBuffer;
|
|
22
23
|
|
|
23
24
|
// Gives a view its own animation of a GIF. Glide gives every view that shows
|
|
@@ -64,6 +65,42 @@ final class FastImageGif {
|
|
|
64
65
|
return copy;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// GifDrawable.start() also starts counting plays again, so a paused GIF
|
|
69
|
+
// would play its whole loop count again when resumed; iOS continues it.
|
|
70
|
+
// The count isn't public: keep it across start() by reflection. A GIF
|
|
71
|
+
// that already finished its plays starts over, as on iOS. Without the
|
|
72
|
+
// fields (another Glide version), it counts again, as start() does.
|
|
73
|
+
@Nullable
|
|
74
|
+
private static final Field LOOP_COUNT = field("loopCount");
|
|
75
|
+
@Nullable
|
|
76
|
+
private static final Field MAX_LOOP_COUNT = field("maxLoopCount");
|
|
77
|
+
|
|
78
|
+
static void resume(GifDrawable gif) {
|
|
79
|
+
if (LOOP_COUNT == null || MAX_LOOP_COUNT == null) {
|
|
80
|
+
gif.start();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
int played = LOOP_COUNT.getInt(gif);
|
|
85
|
+
int max = MAX_LOOP_COUNT.getInt(gif);
|
|
86
|
+
gif.start();
|
|
87
|
+
if (max == GifDrawable.LOOP_FOREVER || played < max) LOOP_COUNT.setInt(gif, played);
|
|
88
|
+
} catch (IllegalAccessException e) {
|
|
89
|
+
gif.start();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Nullable
|
|
94
|
+
private static Field field(String name) {
|
|
95
|
+
try {
|
|
96
|
+
Field field = GifDrawable.class.getDeclaredField(name);
|
|
97
|
+
field.setAccessible(true);
|
|
98
|
+
return field.getType() == int.class ? field : null;
|
|
99
|
+
} catch (Exception e) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
67
104
|
// ByteBufferGifDecoder's.
|
|
68
105
|
private static int sampleSize(GifHeader header, int width, int height) {
|
|
69
106
|
int exact = Math.min(header.getHeight() / height, header.getWidth() / width);
|
|
@@ -99,6 +99,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
99
99
|
view.setImageRendering(imageRendering);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
@ReactProp(name = "paused")
|
|
103
|
+
public void setPaused(FastImageViewWithUrl view, boolean paused) {
|
|
104
|
+
view.setPaused(paused);
|
|
105
|
+
}
|
|
106
|
+
|
|
102
107
|
@ReactProp(name = "resizeMode")
|
|
103
108
|
public void setResizeMode(FastImageViewWithUrl view, String resizeMode) {
|
|
104
109
|
final FastImageViewWithUrl.ScaleType scaleType = FastImageViewConverter.getScaleType(resizeMode);
|
|
@@ -79,8 +79,26 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
79
79
|
// another view plays it.
|
|
80
80
|
if (mOwnGif != null && getDrawable() == mOwnGif) {
|
|
81
81
|
applyLoopCount(mOwnGif);
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
if (!mPaused) {
|
|
83
|
+
mOwnGif.stop();
|
|
84
|
+
mOwnGif.startFromFirstFrame();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pauses GIFs on the frame they're showing (the view's own animation).
|
|
90
|
+
private boolean mPaused = false;
|
|
91
|
+
|
|
92
|
+
public void setPaused(boolean paused) {
|
|
93
|
+
if (paused == mPaused) return;
|
|
94
|
+
mPaused = paused;
|
|
95
|
+
if (mOwnGif != null && getDrawable() == mOwnGif) {
|
|
96
|
+
if (paused) {
|
|
97
|
+
mOwnGif.stop();
|
|
98
|
+
} else {
|
|
99
|
+
// Continues the loop count, as on iOS.
|
|
100
|
+
FastImageGif.resume(mOwnGif);
|
|
101
|
+
}
|
|
84
102
|
}
|
|
85
103
|
}
|
|
86
104
|
|
|
@@ -112,12 +130,27 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
112
130
|
applyLoopCount(own);
|
|
113
131
|
super.onResourceReady(own, transition);
|
|
114
132
|
mOwnGif = own;
|
|
133
|
+
// The target starts it; paused, it waits on its first frame.
|
|
134
|
+
if (mPaused) own.stop();
|
|
115
135
|
return;
|
|
116
136
|
}
|
|
117
137
|
}
|
|
118
138
|
super.onResourceReady(resource, transition);
|
|
119
139
|
}
|
|
120
140
|
|
|
141
|
+
// The target starts animations again when the Activity does: not a
|
|
142
|
+
// paused one, and the view's own GIF continues its loop count (start()
|
|
143
|
+
// would count again).
|
|
144
|
+
@Override
|
|
145
|
+
public void onStart() {
|
|
146
|
+
if (mPaused) return;
|
|
147
|
+
if (mOwnGif != null && getDrawable() == mOwnGif) {
|
|
148
|
+
FastImageGif.resume(mOwnGif);
|
|
149
|
+
} else {
|
|
150
|
+
super.onStart();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
121
154
|
@Override
|
|
122
155
|
protected void setResource(@Nullable Drawable resource) {
|
|
123
156
|
super.setResource(resource);
|
package/dist/index.cjs.js.flow
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -88,6 +88,19 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
88
88
|
* pixels, e.g. for pixel art).
|
|
89
89
|
*/
|
|
90
90
|
imageRendering?: 'auto' | 'smooth' | 'pixelated';
|
|
91
|
+
/**
|
|
92
|
+
* Pauses an animated image (GIF, and animated WebP on iOS) on the frame
|
|
93
|
+
* it's showing; `false` plays it again from there. Each image animates on
|
|
94
|
+
* its own, so pausing one doesn't pause others showing the same file.
|
|
95
|
+
*/
|
|
96
|
+
paused?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* iOS only. Decodes a large image at about the size it's shown at, so it
|
|
99
|
+
* takes much less memory. For images much larger than their views that
|
|
100
|
+
* you can't get at the right size (if you can, resize them on your server
|
|
101
|
+
* or with an image CDN instead). See the README.
|
|
102
|
+
*/
|
|
103
|
+
downsample?: boolean;
|
|
91
104
|
onLoadStart?(): void;
|
|
92
105
|
onProgress?(event: OnProgressEvent): void;
|
|
93
106
|
onLoad?(event: OnLoadEvent): void;
|
package/dist/index.js.flow
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#import <SDWebImage/SDAnimatedImage.h>
|
|
2
|
+
#import <SDWebImage/SDWebImageDefine.h>
|
|
3
|
+
|
|
4
|
+
// An image decoded at about the size it's shown at (the `downsample`
|
|
5
|
+
// prop), instead of at full size. The view asks for it with SDWebImage's
|
|
6
|
+
// thumbnail context: the box to fill (the view's size in pixels), and
|
|
7
|
+
// preserveAspectRatio NO when the image has to cover the box (resizeMode
|
|
8
|
+
// cover or stretch) rather than fit in it (contain or center). SDWebImage's
|
|
9
|
+
// own thumbnail decoding can only fit, so this class works out the size from
|
|
10
|
+
// the image's header before it decodes, as React Native's Image does.
|
|
11
|
+
@interface FFFDownsampledImage : SDAnimatedImage
|
|
12
|
+
|
|
13
|
+
// The url to load a source's url with, when it's downsampled.
|
|
14
|
+
+ (NSURL*) loadURLForURL: (NSURL*)url;
|
|
15
|
+
|
|
16
|
+
// Sets a load's context to decode the image for a box (in pixels) to cover
|
|
17
|
+
// or fit in.
|
|
18
|
+
+ (void) addToContext: (SDWebImageMutableContext*)context forURL: (NSURL*)url box: (CGSize)box cover: (BOOL)cover;
|
|
19
|
+
|
|
20
|
+
// The size of the full image (in points, like UIImage's size) that an image
|
|
21
|
+
// loaded this way was decoded from, for onLoad, or zero for other images.
|
|
22
|
+
+ (CGSize) sourceSizeOfImage: (UIImage*)image;
|
|
23
|
+
|
|
24
|
+
// Whether this version of SDWebImage can decode through this class: it
|
|
25
|
+
// decodes static images through the animated image class since 5.19.
|
|
26
|
+
+ (BOOL) isSupported;
|
|
27
|
+
|
|
28
|
+
@end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#import "FFFDownsampledImage.h"
|
|
2
|
+
#import <ImageIO/ImageIO.h>
|
|
3
|
+
#import <SDWebImage/SDImageCoder.h>
|
|
4
|
+
#import <SDWebImage/SDImageCacheDefine.h>
|
|
5
|
+
#import <SDWebImage/SDWebImageCacheKeyFilter.h>
|
|
6
|
+
#import <SDWebImage/UIImage+Metadata.h>
|
|
7
|
+
|
|
8
|
+
// The decode option that carries the full image's size to the view.
|
|
9
|
+
static SDImageCoderOption const FFFDecodeSourceSize = @"FFFDecodeSourceSize";
|
|
10
|
+
// Added to the url's fragment, which isn't sent to the server.
|
|
11
|
+
static NSString* const FFFDownsampledFragment = @"fastimage-downsampled";
|
|
12
|
+
|
|
13
|
+
// The image's size in pixels as it's shown (EXIF orientations 5 to 8 turn it
|
|
14
|
+
// sideways), from its header, or zero if ImageIO can't read it.
|
|
15
|
+
static CGSize FFFPixelSize(NSData* data) {
|
|
16
|
+
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef) data, NULL);
|
|
17
|
+
if (!source) {
|
|
18
|
+
return CGSizeZero;
|
|
19
|
+
}
|
|
20
|
+
NSDictionary* properties = (__bridge_transfer NSDictionary*) CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
|
|
21
|
+
CFRelease(source);
|
|
22
|
+
CGFloat width = [properties[(__bridge NSString*) kCGImagePropertyPixelWidth] doubleValue];
|
|
23
|
+
CGFloat height = [properties[(__bridge NSString*) kCGImagePropertyPixelHeight] doubleValue];
|
|
24
|
+
NSUInteger orientation = [properties[(__bridge NSString*) kCGImagePropertyOrientation] unsignedIntegerValue];
|
|
25
|
+
if (orientation >= kCGImagePropertyOrientationLeftMirrored) {
|
|
26
|
+
return CGSizeMake(height, width);
|
|
27
|
+
}
|
|
28
|
+
return CGSizeMake(width, height);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Where the decode puts the full image's size, passed in the load's decode
|
|
32
|
+
// options. SDWebImage attaches those options to the image it ends up with
|
|
33
|
+
// (sd_decodeOptions), also when its force decoding replaces the decoded image
|
|
34
|
+
// with another one, which drops anything kept on the image itself.
|
|
35
|
+
@interface FFFSourceSize : NSObject
|
|
36
|
+
@property (atomic, assign) CGSize size;
|
|
37
|
+
@end
|
|
38
|
+
|
|
39
|
+
@implementation FFFSourceSize
|
|
40
|
+
|
|
41
|
+
// Any two are equal: it's where the result goes, not part of the request, so
|
|
42
|
+
// the same request from several views is still decoded once (SDWebImage's
|
|
43
|
+
// downloader groups them by their decode options).
|
|
44
|
+
- (BOOL) isEqual: (id)object {
|
|
45
|
+
return [object isKindOfClass: [FFFSourceSize class]];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (NSUInteger) hash {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@end
|
|
53
|
+
|
|
54
|
+
@implementation FFFDownsampledImage
|
|
55
|
+
|
|
56
|
+
+ (BOOL) isSupported {
|
|
57
|
+
static BOOL supported = NO;
|
|
58
|
+
static dispatch_once_t once;
|
|
59
|
+
dispatch_once(&once, ^{
|
|
60
|
+
// No animated image coder takes JPEG, so before SDWebImage 5.19
|
|
61
|
+
// SDAnimatedImage returned nil for one, and SDWebImage decoded it with
|
|
62
|
+
// its thumbnail options as they are (stretched to the box).
|
|
63
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
64
|
+
CGContextRef context = CGBitmapContextCreate(NULL, 1, 1, 8, 4, colorSpace, kCGImageAlphaNoneSkipLast);
|
|
65
|
+
CGColorSpaceRelease(colorSpace);
|
|
66
|
+
CGImageRef pixel = context ? CGBitmapContextCreateImage(context) : NULL;
|
|
67
|
+
if (context) {
|
|
68
|
+
CGContextRelease(context);
|
|
69
|
+
}
|
|
70
|
+
if (!pixel) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
NSMutableData* jpeg = [NSMutableData data];
|
|
74
|
+
CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef) jpeg, CFSTR("public.jpeg"), 1, NULL);
|
|
75
|
+
if (destination) {
|
|
76
|
+
CGImageDestinationAddImage(destination, pixel, NULL);
|
|
77
|
+
CGImageDestinationFinalize(destination);
|
|
78
|
+
CFRelease(destination);
|
|
79
|
+
}
|
|
80
|
+
CGImageRelease(pixel);
|
|
81
|
+
supported = jpeg.length > 0 && [[SDAnimatedImage alloc] initWithData: jpeg scale: 1 options: nil] != nil;
|
|
82
|
+
});
|
|
83
|
+
return supported;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
+ (NSURL*) loadURLForURL: (NSURL*)url {
|
|
87
|
+
// SDWebImage's downloader shares a download between loads of the same
|
|
88
|
+
// url, and decodes each load's image with the first load's image class.
|
|
89
|
+
// After a load that isn't downsampled (e.g. a preload), this class
|
|
90
|
+
// wouldn't be used, and the box would be decoded as SDWebImage does (to
|
|
91
|
+
// fill it, stretched). With its own url, a downsampled load only shares
|
|
92
|
+
// downloads with other downsampled ones.
|
|
93
|
+
NSURLComponents* components = [NSURLComponents componentsWithURL: url resolvingAgainstBaseURL: NO];
|
|
94
|
+
if (!components) {
|
|
95
|
+
return url;
|
|
96
|
+
}
|
|
97
|
+
components.fragment = components.fragment.length > 0
|
|
98
|
+
? [components.fragment stringByAppendingFormat: @"-%@", FFFDownsampledFragment]
|
|
99
|
+
: FFFDownsampledFragment;
|
|
100
|
+
return components.URL ?: url;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
+ (void) addToContext: (SDWebImageMutableContext*)context forURL: (NSURL*)url box: (CGSize)box cover: (BOOL)cover {
|
|
104
|
+
context[SDWebImageContextAnimatedImageClass] = [FFFDownsampledImage class];
|
|
105
|
+
// Cached under the url itself (see loadURLForURL:), so the disk cache
|
|
106
|
+
// keeps one download for every size and for loads that aren't downsampled.
|
|
107
|
+
NSString* key = url.absoluteString;
|
|
108
|
+
context[SDWebImageContextCacheKeyFilter] = [SDWebImageCacheKeyFilter cacheKeyFilterWithBlock: ^NSString* (NSURL* _Nonnull loadURL) {
|
|
109
|
+
return key;
|
|
110
|
+
}];
|
|
111
|
+
// The memory cache key is the url with these (SDWebImage's thumbnail
|
|
112
|
+
// key), so views of other sizes don't get this image.
|
|
113
|
+
context[SDWebImageContextImageThumbnailPixelSize] = [NSValue valueWithCGSize: box];
|
|
114
|
+
context[SDWebImageContextImagePreserveAspectRatio] = @(!cover);
|
|
115
|
+
context[SDWebImageContextImageDecodeOptions] = @{FFFDecodeSourceSize: [FFFSourceSize new]};
|
|
116
|
+
// Only in memory: the disk cache has the downloaded file.
|
|
117
|
+
context[SDWebImageContextStoreCacheType] = @(SDImageCacheTypeMemory);
|
|
118
|
+
context[SDWebImageContextQueryCacheType] = @(SDImageCacheTypeMemory);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
+ (CGSize) sourceSizeOfImage: (UIImage*)image {
|
|
122
|
+
id sourceSize = image.sd_decodeOptions[FFFDecodeSourceSize];
|
|
123
|
+
return [sourceSize isKindOfClass: [FFFSourceSize class]] ? ((FFFSourceSize*) sourceSize).size : CGSizeZero;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
- (instancetype) initWithData: (NSData*)data scale: (CGFloat)scale options: (SDImageCoderOptions*)options {
|
|
127
|
+
NSValue* boxValue = options[SDImageCoderDecodeThumbnailPixelSize];
|
|
128
|
+
CGSize box = boxValue ? boxValue.CGSizeValue : CGSizeZero;
|
|
129
|
+
NSNumber* preserveAspectRatio = options[SDImageCoderDecodePreserveAspectRatio];
|
|
130
|
+
BOOL cover = preserveAspectRatio != nil && !preserveAspectRatio.boolValue;
|
|
131
|
+
CGSize pixelSize = FFFPixelSize(data);
|
|
132
|
+
|
|
133
|
+
// Decode at full size unless the image is at least twice as large as it
|
|
134
|
+
// needs to be (or its size can't be read). Decoding smaller is done in
|
|
135
|
+
// software, while newer iPhones decode JPEG and HEIC at full size in
|
|
136
|
+
// hardware: on an iPhone 15 Pro Max a 12 MP JPEG took about 20 ms at full
|
|
137
|
+
// size, and 120-130 ms with a peak of 100-125 MB decoded to 60-90% of its
|
|
138
|
+
// size (1-6 MB and 27-40 ms for a thumbnail).
|
|
139
|
+
NSMutableDictionary* decodeOptions = options ? [options mutableCopy] : [NSMutableDictionary dictionary];
|
|
140
|
+
[decodeOptions removeObjectForKey: SDImageCoderDecodeThumbnailPixelSize];
|
|
141
|
+
decodeOptions[SDImageCoderDecodePreserveAspectRatio] = @YES;
|
|
142
|
+
if (box.width > 0 && box.height > 0 && pixelSize.width > 0 && pixelSize.height > 0) {
|
|
143
|
+
CGFloat widthRatio = box.width / pixelSize.width;
|
|
144
|
+
CGFloat heightRatio = box.height / pixelSize.height;
|
|
145
|
+
CGFloat ratio = cover ? MAX(widthRatio, heightRatio) : MIN(widthRatio, heightRatio);
|
|
146
|
+
if (ratio <= 0.5) {
|
|
147
|
+
// ImageIO takes the longest side (kCGImageSourceThumbnailMaxPixelSize).
|
|
148
|
+
// SDWebImage works it out from the box with the image's stored
|
|
149
|
+
// width and height, before EXIF orientation, so a square box is
|
|
150
|
+
// the one that gives this side for any orientation.
|
|
151
|
+
CGFloat longest = ceil(MAX(pixelSize.width, pixelSize.height) * ratio);
|
|
152
|
+
decodeOptions[SDImageCoderDecodeThumbnailPixelSize] = [NSValue valueWithCGSize: CGSizeMake(longest, longest)];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
self = [super initWithData: data scale: scale options: decodeOptions];
|
|
157
|
+
if (self) {
|
|
158
|
+
CGFloat imageScale = MAX(scale, 1);
|
|
159
|
+
FFFSourceSize* sourceSize = options[FFFDecodeSourceSize];
|
|
160
|
+
if ([sourceSize isKindOfClass: [FFFSourceSize class]]) {
|
|
161
|
+
sourceSize.size = pixelSize.width > 0 && pixelSize.height > 0
|
|
162
|
+
? CGSizeMake(pixelSize.width / imageScale, pixelSize.height / imageScale)
|
|
163
|
+
: self.size;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return self;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@end
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
// How the image is filtered when drawn at another size: auto, smooth or
|
|
29
29
|
// pixelated.
|
|
30
30
|
@property (nonatomic, copy) NSString *imageRendering;
|
|
31
|
+
// Pauses animated images on the frame they're showing.
|
|
32
|
+
@property (nonatomic, assign) BOOL paused;
|
|
33
|
+
// Decodes images at about the view's size instead of at full size.
|
|
34
|
+
@property (nonatomic, assign) BOOL downsample;
|
|
31
35
|
|
|
32
36
|
@end
|
|
33
37
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#import <SDWebImage/UIView+WebCache.h>
|
|
4
4
|
#import <React/RCTUtils.h>
|
|
5
5
|
#import <SDWebImage/SDWebImageError.h>
|
|
6
|
+
#import "FFFDownsampledImage.h"
|
|
6
7
|
|
|
7
8
|
@interface FFFastImageView ()
|
|
8
9
|
|
|
@@ -25,6 +26,14 @@
|
|
|
25
26
|
// Whether the view shows an image that loaded (not defaultSource or nothing).
|
|
26
27
|
// A new source then keeps it until the new image has loaded (see reloadImage).
|
|
27
28
|
@property(nonatomic, assign) BOOL showsLoadedImage;
|
|
29
|
+
// downsample: a load waits for the view's size (see didSetProps).
|
|
30
|
+
@property(nonatomic, assign) BOOL waitsForSize;
|
|
31
|
+
// The size (in pixels) the image showing or loading was decoded for, and
|
|
32
|
+
// whether it covers it; zero for a full-size image.
|
|
33
|
+
@property(nonatomic, assign) CGSize decodedBox;
|
|
34
|
+
@property(nonatomic, assign) BOOL decodedCover;
|
|
35
|
+
// Counts loads, so a quiet reload's completion can tell if it's still current.
|
|
36
|
+
@property(nonatomic, assign) NSUInteger loadCount;
|
|
28
37
|
|
|
29
38
|
@end
|
|
30
39
|
|
|
@@ -85,9 +94,28 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
85
94
|
} else if (self.player && [self.image conformsToProtocol: @protocol(SDAnimatedImage)]) {
|
|
86
95
|
self.player.totalLoopCount = [(id<SDAnimatedImage>) self.image animatedImageLoopCount];
|
|
87
96
|
}
|
|
88
|
-
// Apply it to the image that's showing, and play it again
|
|
97
|
+
// Apply it to the image that's showing, and play it again (unless it's
|
|
98
|
+
// paused: then it waits on the first frame).
|
|
89
99
|
if (self.player) {
|
|
90
100
|
[self.player seekToFrameAtIndex: 0 loopCount: 0];
|
|
101
|
+
if (!_paused) {
|
|
102
|
+
[self startAnimating];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
- (void) setPaused: (BOOL)paused {
|
|
108
|
+
if (_paused == paused) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
_paused = paused;
|
|
112
|
+
// SDAnimatedImageView starts an animated image when it's shown, and again
|
|
113
|
+
// when the view comes back on screen, unless autoPlayAnimatedImage is off.
|
|
114
|
+
// Stopping keeps the frame it's on (resetFrameIndexWhenStopped is off).
|
|
115
|
+
self.autoPlayAnimatedImage = !paused;
|
|
116
|
+
if (paused) {
|
|
117
|
+
[self stopAnimating];
|
|
118
|
+
} else {
|
|
91
119
|
[self startAnimating];
|
|
92
120
|
}
|
|
93
121
|
}
|
|
@@ -121,6 +149,13 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
121
149
|
- (void) layoutSubviews {
|
|
122
150
|
[super layoutSubviews];
|
|
123
151
|
[self updateContentMode];
|
|
152
|
+
if (self.waitsForSize) {
|
|
153
|
+
if ([self hasSize]) {
|
|
154
|
+
[self reloadImage];
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
[self reloadIfResized];
|
|
158
|
+
}
|
|
124
159
|
}
|
|
125
160
|
|
|
126
161
|
- (void) setOnFastImageLoadEnd: (RCTDirectEventBlock)onFastImageLoadEnd {
|
|
@@ -235,9 +270,14 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
235
270
|
}
|
|
236
271
|
|
|
237
272
|
- (void) sendOnLoad: (UIImage*)image {
|
|
273
|
+
// The full image's size, also when it was decoded smaller.
|
|
274
|
+
CGSize size = [FFFDownsampledImage sourceSizeOfImage: image];
|
|
275
|
+
if (CGSizeEqualToSize(size, CGSizeZero)) {
|
|
276
|
+
size = image.size;
|
|
277
|
+
}
|
|
238
278
|
self.onLoadEvent = @{
|
|
239
|
-
@"width": [NSNumber numberWithDouble:
|
|
240
|
-
@"height": [NSNumber numberWithDouble:
|
|
279
|
+
@"width": [NSNumber numberWithDouble: size.width],
|
|
280
|
+
@"height": [NSNumber numberWithDouble: size.height]
|
|
241
281
|
};
|
|
242
282
|
if (self.onFastImageLoad) {
|
|
243
283
|
self.onFastImageLoad(self.onLoadEvent);
|
|
@@ -275,12 +315,95 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
275
315
|
|
|
276
316
|
- (void) didSetProps: (NSArray<NSString*>*)changedProps {
|
|
277
317
|
if (_needsReload) {
|
|
318
|
+
// With downsample on, the image is decoded for the view's size, so
|
|
319
|
+
// a view that hasn't been laid out yet loads once it has. Props and
|
|
320
|
+
// layout are applied in the same update, so that's before the next
|
|
321
|
+
// frame (in layoutSubviews). A view that still has no size then
|
|
322
|
+
// (e.g. one sized from onLoad) loads at full size.
|
|
323
|
+
if ([self downsamples] && ![self hasSize]) {
|
|
324
|
+
if (!self.waitsForSize) {
|
|
325
|
+
self.waitsForSize = YES;
|
|
326
|
+
__weak typeof(self) weakSelf = self;
|
|
327
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
328
|
+
if (weakSelf.waitsForSize) {
|
|
329
|
+
[weakSelf reloadImage];
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
278
335
|
[self reloadImage];
|
|
336
|
+
} else {
|
|
337
|
+
[self reloadIfResized];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Whether images are decoded at about the view's size (downsample).
|
|
342
|
+
// Not for `repeat`, which tiles the image at its own size, or SDWebImage
|
|
343
|
+
// before 5.19.
|
|
344
|
+
- (BOOL) downsamples {
|
|
345
|
+
return _downsample && _resizeMode != RCTResizeModeRepeat && [FFFDownsampledImage isSupported];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Whether the view has been laid out with an area. One that's 0 wide or tall
|
|
349
|
+
// (e.g. sized from onLoad) has no size to decode the image for.
|
|
350
|
+
- (BOOL) hasSize {
|
|
351
|
+
return self.bounds.size.width > 0 && self.bounds.size.height > 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// The size in pixels to decode the image for, or zero for full size.
|
|
355
|
+
- (CGSize) decodeBox {
|
|
356
|
+
if (![self downsamples] || ![self hasSize]) {
|
|
357
|
+
return CGSizeZero;
|
|
358
|
+
}
|
|
359
|
+
CGFloat scale = self.window.screen.scale ?: [UIScreen mainScreen].scale;
|
|
360
|
+
CGSize size = self.bounds.size;
|
|
361
|
+
return CGSizeMake(ceil(size.width * scale), ceil(size.height * scale));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Whether the image has to cover the box, rather than fit in it.
|
|
365
|
+
- (BOOL) decodeCovers {
|
|
366
|
+
return _resizeMode == RCTResizeModeCover || _resizeMode == RCTResizeModeStretch;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The view grew (by more than a fifth, as React Native's Image reloads), or
|
|
370
|
+
// now needs a covering or full-size image: loads the image again for its
|
|
371
|
+
// size, keeping the current one until then. It comes from the disk cache
|
|
372
|
+
// (the downloaded file is kept there whatever size it's decoded at).
|
|
373
|
+
- (void) reloadIfResized {
|
|
374
|
+
if (!_source || _needsReload || self.hasErrored || CGSizeEqualToSize(self.decodedBox, CGSizeZero)) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Already at full size (it's no larger than the view it was decoded for).
|
|
378
|
+
UIImage* image = self.untintedImage ?: super.image;
|
|
379
|
+
if (self.hasCompleted && CGSizeEqualToSize(image.size, [FFFDownsampledImage sourceSizeOfImage: image])) {
|
|
380
|
+
return;
|
|
279
381
|
}
|
|
382
|
+
CGSize box = [self decodeBox];
|
|
383
|
+
BOOL cover = [self decodeCovers];
|
|
384
|
+
if ([self downsamples]) {
|
|
385
|
+
if (![self hasSize]) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
BOOL grew = box.width > self.decodedBox.width * 1.2 || box.height > self.decodedBox.height * 1.2;
|
|
389
|
+
if (!grew && (self.decodedCover || !cover)) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
SDWebImageOptions options = [self loadOptions];
|
|
394
|
+
if (self.showsLoadedImage) {
|
|
395
|
+
options |= SDWebImageDelayPlaceholder;
|
|
396
|
+
}
|
|
397
|
+
// Without events once it has loaded: it's the same image. If it's still
|
|
398
|
+
// loading, it restarts for the new size, and sends its events as usual.
|
|
399
|
+
BOOL events = !self.hasCompleted;
|
|
400
|
+
[self downloadImage: _source options: options context: [self loadContext] events: events];
|
|
280
401
|
}
|
|
281
402
|
|
|
282
403
|
- (void) reloadImage {
|
|
283
404
|
_needsReload = NO;
|
|
405
|
+
self.waitsForSize = NO;
|
|
406
|
+
self.decodedBox = CGSizeZero;
|
|
284
407
|
|
|
285
408
|
if (_source) {
|
|
286
409
|
// Load base64 images.
|
|
@@ -321,33 +444,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
321
444
|
return;
|
|
322
445
|
}
|
|
323
446
|
|
|
324
|
-
|
|
325
|
-
SDWebImageContext* context = @{SDWebImageContextDownloadRequestModifier: _source.requestModifier};
|
|
326
|
-
|
|
327
|
-
// Set priority.
|
|
328
|
-
SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies;
|
|
329
|
-
switch (_source.priority) {
|
|
330
|
-
case FFFPriorityLow:
|
|
331
|
-
options |= SDWebImageLowPriority;
|
|
332
|
-
break;
|
|
333
|
-
case FFFPriorityNormal:
|
|
334
|
-
// Priority is normal by default.
|
|
335
|
-
break;
|
|
336
|
-
case FFFPriorityHigh:
|
|
337
|
-
options |= SDWebImageHighPriority;
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
switch (_source.cacheControl) {
|
|
342
|
-
case FFFCacheControlWeb:
|
|
343
|
-
options |= SDWebImageRefreshCached;
|
|
344
|
-
break;
|
|
345
|
-
case FFFCacheControlCacheOnly:
|
|
346
|
-
options |= SDWebImageFromCacheOnly;
|
|
347
|
-
break;
|
|
348
|
-
case FFFCacheControlImmutable:
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
447
|
+
SDWebImageOptions options = [self loadOptions];
|
|
351
448
|
|
|
352
449
|
// Keep showing the loaded image until the new one has loaded, instead
|
|
353
450
|
// of clearing it to defaultSource (or nothing) while it loads, which
|
|
@@ -368,20 +465,66 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
368
465
|
self.retriedAfterBackground = NO;
|
|
369
466
|
[self stopWaitingForActive];
|
|
370
467
|
|
|
371
|
-
[self downloadImage: _source options: options context:
|
|
468
|
+
[self downloadImage: _source options: options context: [self loadContext] events: YES];
|
|
372
469
|
} else if (_defaultSource) {
|
|
373
470
|
[self setImage: _defaultSource];
|
|
374
471
|
self.showsLoadedImage = NO;
|
|
375
472
|
}
|
|
376
473
|
}
|
|
377
474
|
|
|
378
|
-
- (
|
|
475
|
+
- (SDWebImageOptions) loadOptions {
|
|
476
|
+
SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies;
|
|
477
|
+
switch (_source.priority) {
|
|
478
|
+
case FFFPriorityLow:
|
|
479
|
+
options |= SDWebImageLowPriority;
|
|
480
|
+
break;
|
|
481
|
+
case FFFPriorityNormal:
|
|
482
|
+
// Priority is normal by default.
|
|
483
|
+
break;
|
|
484
|
+
case FFFPriorityHigh:
|
|
485
|
+
options |= SDWebImageHighPriority;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
switch (_source.cacheControl) {
|
|
490
|
+
case FFFCacheControlWeb:
|
|
491
|
+
options |= SDWebImageRefreshCached;
|
|
492
|
+
break;
|
|
493
|
+
case FFFCacheControlCacheOnly:
|
|
494
|
+
options |= SDWebImageFromCacheOnly;
|
|
495
|
+
break;
|
|
496
|
+
case FFFCacheControlImmutable:
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
return options;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Headers, and the size to decode at (see FFFDownsampledImage), which it
|
|
503
|
+
// records as decodedBox.
|
|
504
|
+
- (SDWebImageContext*) loadContext {
|
|
505
|
+
SDWebImageMutableContext* context = [NSMutableDictionary dictionary];
|
|
506
|
+
context[SDWebImageContextDownloadRequestModifier] = _source.requestModifier;
|
|
507
|
+
CGSize box = [self decodeBox];
|
|
508
|
+
self.decodedBox = box;
|
|
509
|
+
self.decodedCover = [self decodeCovers];
|
|
510
|
+
if (CGSizeEqualToSize(box, CGSizeZero)) {
|
|
511
|
+
context[SDWebImageContextAnimatedImageClass] = [SDAnimatedImage class];
|
|
512
|
+
return context;
|
|
513
|
+
}
|
|
514
|
+
[FFFDownsampledImage addToContext: context forURL: _source.url box: box cover: self.decodedCover];
|
|
515
|
+
return context;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// events: NO for a quiet reload at a new size (see reloadIfResized), which
|
|
519
|
+
// sends no events and keeps the current image if it fails.
|
|
520
|
+
- (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context events: (BOOL)events {
|
|
379
521
|
__weak typeof(self) weakSelf = self; // Always use a weak reference to self in blocks
|
|
522
|
+
NSUInteger load = ++self.loadCount;
|
|
380
523
|
// Most images have no onProgress, so only ask SDWebImage for progress when
|
|
381
524
|
// there's a handler as the load starts. A handler added while loading is
|
|
382
525
|
// used from the next load.
|
|
383
526
|
SDImageLoaderProgressBlock progress = nil;
|
|
384
|
-
if (self.onFastImageProgress) {
|
|
527
|
+
if (events && self.onFastImageProgress) {
|
|
385
528
|
progress = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) {
|
|
386
529
|
// Without a Content-Length the total is unknown (-1 or 0), and a
|
|
387
530
|
// percentage can't be worked out from it, so don't send those.
|
|
@@ -404,15 +547,49 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
404
547
|
};
|
|
405
548
|
}
|
|
406
549
|
CFTimeInterval startedAt = CACurrentMediaTime();
|
|
407
|
-
[
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
550
|
+
NSURL* url = context[SDWebImageContextImageThumbnailPixelSize] ? [FFFDownsampledImage loadURLForURL: source.url] : source.url;
|
|
551
|
+
if (!events) {
|
|
552
|
+
// Only this load's image: SDWebImage would clear the view (to the
|
|
553
|
+
// placeholder) if it fails, and a cancelled load can still complete.
|
|
554
|
+
SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
|
|
555
|
+
if (image && weakSelf.loadCount == load) {
|
|
556
|
+
weakSelf.image = image;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
[self sd_internalSetImageWithURL: url
|
|
560
|
+
placeholderImage: nil
|
|
561
|
+
options: options
|
|
562
|
+
context: context
|
|
563
|
+
setImageBlock: setImage
|
|
564
|
+
progress: nil
|
|
565
|
+
completed: ^(UIImage* _Nullable image, NSData* _Nullable data, NSError* _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL* _Nullable imageURL) {
|
|
566
|
+
if (error && weakSelf.loadCount == load) {
|
|
567
|
+
// Keeps the image it has, without trying again on every
|
|
568
|
+
// layout (e.g. while offline).
|
|
569
|
+
weakSelf.decodedBox = CGSizeZero;
|
|
570
|
+
}
|
|
571
|
+
}];
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// Like SDAnimatedImageView's sd_setImageWithURL, which sets the image
|
|
575
|
+
// class to SDAnimatedImage (loadContext sets it).
|
|
576
|
+
[self sd_internalSetImageWithURL: url
|
|
577
|
+
placeholderImage: _defaultSource
|
|
578
|
+
options: options
|
|
579
|
+
context: context
|
|
580
|
+
setImageBlock: nil
|
|
581
|
+
progress: progress
|
|
582
|
+
completed: ^(UIImage* _Nullable image,
|
|
583
|
+
NSData* _Nullable data,
|
|
413
584
|
NSError* _Nullable error,
|
|
414
585
|
SDImageCacheType cacheType,
|
|
586
|
+
BOOL finished,
|
|
415
587
|
NSURL* _Nullable imageURL) {
|
|
588
|
+
// Restarted for a new size (reloadIfResized): the new load
|
|
589
|
+
// sends the events.
|
|
590
|
+
if (weakSelf.loadCount != load && weakSelf.source == source) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
416
593
|
// The download was running when the app went to the
|
|
417
594
|
// background, and failed: iOS suspends it there, and its
|
|
418
595
|
// timeout keeps counting, so it times out as the app comes
|
|
@@ -449,7 +626,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
449
626
|
// nil in app extensions, which don't get these notifications.
|
|
450
627
|
UIApplication* application = RCTSharedApplication();
|
|
451
628
|
if (!application || application.applicationState == UIApplicationStateActive) {
|
|
452
|
-
[self downloadImage: source options: options context: context];
|
|
629
|
+
[self downloadImage: source options: options context: context events: YES];
|
|
453
630
|
return;
|
|
454
631
|
}
|
|
455
632
|
[self stopWaitingForActive];
|
|
@@ -460,7 +637,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
460
637
|
usingBlock: ^(NSNotification* notification) {
|
|
461
638
|
[weakSelf stopWaitingForActive];
|
|
462
639
|
if (weakSelf.source == source) {
|
|
463
|
-
[weakSelf downloadImage: source options: options context: context];
|
|
640
|
+
[weakSelf downloadImage: source options: options context: context events: YES];
|
|
464
641
|
}
|
|
465
642
|
}];
|
|
466
643
|
}
|
|
@@ -20,6 +20,8 @@ RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
|
|
|
20
20
|
RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
|
|
21
21
|
RCT_EXPORT_VIEW_PROPERTY(loopCount, NSInteger)
|
|
22
22
|
RCT_EXPORT_VIEW_PROPERTY(imageRendering, NSString)
|
|
23
|
+
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
|
|
24
|
+
RCT_EXPORT_VIEW_PROPERTY(downsample, BOOL)
|
|
23
25
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
|
|
24
26
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock)
|
|
25
27
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-fast-image",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.13.0",
|
|
4
4
|
"description": "🚩 FastImage, performant React Native image component.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@types/react": "^17.0.14",
|
|
52
52
|
"@types/react-native": "^0.69.5",
|
|
53
53
|
"@types/react-test-renderer": "^17.0.1",
|
|
54
|
+
"conventional-changelog-angular": "8.3.1",
|
|
54
55
|
"maestro-runner": "1.1.27",
|
|
55
56
|
"odiff-bin": "^4.5.0",
|
|
56
57
|
"oxfmt": "0.70.0",
|