react-native-fast-image 8.25.0 → 8.27.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 +49 -1
- package/android/build.gradle +3 -0
- package/android/consumer-rules.pro +5 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +6 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +5 -2
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java +17 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSvg.java +161 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSvgRenderer.java +86 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +6 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +112 -3
- package/dist/index.cjs.js +20 -5
- package/dist/index.cjs.js.flow +4 -1
- package/dist/index.cjs.web.js +78 -3
- package/dist/index.cjs.web.js.flow +4 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +20 -5
- package/dist/index.js.flow +4 -1
- package/dist/index.web.js +79 -4
- package/dist/index.web.js.flow +4 -1
- package/ios/FastImage/FFFastImageSource.h +7 -0
- package/ios/FastImage/FFFastImageSource.m +46 -0
- package/ios/FastImage/FFFastImageView.h +3 -0
- package/ios/FastImage/FFFastImageView.m +111 -12
- package/ios/FastImage/FFFastImageViewManager.m +3 -2
- package/ios/FastImage/RCTConvert+FFFastImage.h +1 -0
- package/ios/FastImage/RCTConvert+FFFastImage.m +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,6 +124,26 @@ With Expo, add it with [expo-build-properties](https://docs.expo.dev/versions/la
|
|
|
124
124
|
|
|
125
125
|
The app needs access to the photo library, which it has if it got the url from there. A photo library image is decoded at about the view's size, since photos are large and usually shown small; `onLoad` still reports the photo's own size. `FastImage.preload` of a `ph://` source loads the full-size photo (there's no view to size it for), which doesn't make a view's smaller copy load faster, so preloading photo library images usually isn't worth it. Without the plugin, a `ph://` source fails with `onError`, saying so. `assets-library://` urls aren't supported. On Android, photo pickers give `content://` urls, which load as they are.
|
|
126
126
|
|
|
127
|
+
#### SVG images
|
|
128
|
+
|
|
129
|
+
SVG images (remote, bundled with `require()`, or local files) load when the app has an SVG library: SDWebImageSVGCoder on iOS and AndroidSVG on Android. Add them to the app:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# ios/Podfile
|
|
133
|
+
pod 'SDWebImageSVGCoder'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```groovy
|
|
137
|
+
// android/app/build.gradle
|
|
138
|
+
dependencies {
|
|
139
|
+
implementation 'com.caverock:androidsvg-aar:1.4'
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
On Android either of AndroidSVG's packages works (`com.caverock:androidsvg-aar` or `com.caverock:androidsvg`), so an app that already has one (e.g. from another library) needs nothing more. With Expo, add the pod with [expo-build-properties](https://docs.expo.dev/versions/latest/sdk/build-properties/) (`{ "ios": { "extraPods": [{ "name": "SDWebImageSVGCoder" }] } }`); an app that uses `expo-image` already has both.
|
|
144
|
+
|
|
145
|
+
An SVG is drawn at the size it's shown at, so it's sharp at any size, and then works like any other image: `resizeMode`, `tintColor`, `blurRadius`, `transition` and caching. `onLoad` reports the SVG's own size: its `width` and `height`, or its `viewBox`'s (300x150 if it has neither). Animated SVGs (SMIL or CSS animations) show their first state. On iOS, SVG images need iOS 13 or later. Without the SVG library, an SVG image fails with `onError`, saying what to add (on iOS, for a url that ends in `.svg`).
|
|
146
|
+
|
|
127
147
|
---
|
|
128
148
|
|
|
129
149
|
### `source.headers?: object`
|
|
@@ -175,6 +195,33 @@ Whether the decoded image is kept in the memory cache. **Default: true.** With `
|
|
|
175
195
|
|
|
176
196
|
---
|
|
177
197
|
|
|
198
|
+
### Several sizes of an image
|
|
199
|
+
|
|
200
|
+
`source` can also be an array of the same image at different sizes, each with its `width` and `height` in pixels (times `scale`, if it has one). The view loads the one whose size is closest to its own, in pixels, so a small view downloads a small image:
|
|
201
|
+
|
|
202
|
+
```jsx
|
|
203
|
+
<FastImage
|
|
204
|
+
style={{ width: 120, height: 120 }}
|
|
205
|
+
source={[
|
|
206
|
+
{ uri: 'https://example.com/photo-200.jpg', width: 200, height: 200 },
|
|
207
|
+
{ uri: 'https://example.com/photo-800.jpg', width: 800, height: 800 },
|
|
208
|
+
{
|
|
209
|
+
uri: 'https://example.com/photo-2000.jpg',
|
|
210
|
+
width: 2000,
|
|
211
|
+
height: 2000,
|
|
212
|
+
},
|
|
213
|
+
]}
|
|
214
|
+
/>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- The view picks once it has been laid out. One that has no size (e.g. sized from `onLoad`) loads the largest.
|
|
218
|
+
- When the view's size changes so that another size fits better, it loads that one (with its load events), and keeps showing the current image until then, without a `transition`.
|
|
219
|
+
- Each size is cached separately. A `cacheKey` applies to its own entry: give each size its own key (or none), or the sizes would replace each other in the cache.
|
|
220
|
+
- `FastImage.preload`, `getCachePath` and `writeToCache` take one size: pass the one a view will show. An array fails with `{ ok: false, error }`.
|
|
221
|
+
- An array of one is the same as that source.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
178
225
|
### `defaultSource?: number`
|
|
179
226
|
|
|
180
227
|
- An asset loaded with `require(...)`.
|
|
@@ -512,8 +559,9 @@ With [react-native-web](https://necolas.github.io/react-native-web/), FastImage
|
|
|
512
559
|
|
|
513
560
|
Support is minimal:
|
|
514
561
|
|
|
515
|
-
- Props that work: `source` (a `uri`,
|
|
562
|
+
- Props that work: `source` (a `uri`, a `require()`d image, or several sizes), `defaultSource`, `resizeMode`, `tintColor`, `blurRadius`, `style`, children, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
|
|
516
563
|
- Ignored: `source.headers` (a browser can't send them for an image), `source.priority`, `source.cache`, `source.cacheKey`, `source.memoryCache`, `recyclingKey`, `loop`, `imageRendering`, `paused`, `transition`, `downsample`, `fallback` and `onProgress`.
|
|
564
|
+
- Several sizes are shown with a lazily loaded `<img>` whose `srcset` lists them, with `sizes="auto, 100vw"`: the browser loads the one for the width the image is shown at, in device pixels (usually the smallest that's at least as wide). Browsers that don't support `sizes="auto"` use the viewport's width. `tintColor`, `defaultSource` and `resizeMode="repeat"` don't apply to them.
|
|
517
565
|
- `FastImage.preload` loads the images into the browser's cache and resolves with a result per source. `clearMemoryCache`, `clearDiskCache` and `configureCache` resolve without doing anything (the browser manages its cache), and `getCachePath` and `writeToCache` resolve with `{ ok: false, error: 'Not supported on the web' }`.
|
|
518
566
|
|
|
519
567
|
## Troubleshooting
|
package/android/build.gradle
CHANGED
|
@@ -81,4 +81,7 @@ dependencies {
|
|
|
81
81
|
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
|
|
82
82
|
implementation "com.github.bumptech.glide:okhttp3-integration:${glideVersion}"
|
|
83
83
|
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
|
84
|
+
// SVG images, when the app has AndroidSVG (either package): not shipped,
|
|
85
|
+
// so there's never a second copy of it (see FastImageSvg).
|
|
86
|
+
compileOnly 'com.caverock:androidsvg-aar:1.4'
|
|
84
87
|
}
|
|
@@ -41,3 +41,8 @@
|
|
|
41
41
|
-keep class com.facebook.react.uimanager.UIManagerHelper {
|
|
42
42
|
public static *** getEventDispatcherForReactTag(com.facebook.react.bridge.ReactContext, int);
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
# AndroidSVG, which FastImage uses for SVG images when the app has it
|
|
46
|
+
# (FastImageSvgRenderer), and doesn't ship. Without it, R8 would stop at the
|
|
47
|
+
# missing classes.
|
|
48
|
+
-dontwarn com.caverock.androidsvg.**
|
package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java
CHANGED
|
@@ -120,6 +120,7 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
|
|
|
120
120
|
}
|
|
121
121
|
OkHttpClient client = builder.build();
|
|
122
122
|
registry.replace(GlideUrl.class, InputStream.class, new UrlLoaderFactory(client));
|
|
123
|
+
FastImageSvg.register(registry, glide.getBitmapPool());
|
|
123
124
|
|
|
124
125
|
// `cache: 'web'` skips Glide's caches and relies on HTTP caching, so
|
|
125
126
|
// those urls get a client with an HTTP cache (#280): one of their own,
|
|
@@ -202,7 +203,7 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
|
|
|
202
203
|
|
|
203
204
|
// Fails a successful response that's clearly not an image: its type isn't
|
|
204
205
|
// an image's (text, JSON, XML other than SVG) and its first bytes aren't a
|
|
205
|
-
// format Glide recognizes, e.g. a captive portal's or a proxy's HTML page
|
|
206
|
+
// format Glide recognizes or an SVG's, e.g. a captive portal's or a proxy's HTML page
|
|
206
207
|
// sent with status 200. Glide keeps the bytes it downloads in its disk
|
|
207
208
|
// cache before decoding them (and doesn't remove them when that fails), as
|
|
208
209
|
// does the HTTP cache of `web` images, so every later load of the url
|
|
@@ -227,6 +228,10 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
|
|
|
227
228
|
if (imageType != ImageHeaderParser.ImageType.UNKNOWN) {
|
|
228
229
|
return response;
|
|
229
230
|
}
|
|
231
|
+
byte[] head = response.peekBody(1024).bytes();
|
|
232
|
+
if (FastImageSvg.looksLikeSvg(head, head.length)) {
|
|
233
|
+
return response;
|
|
234
|
+
}
|
|
230
235
|
response.close();
|
|
231
236
|
throw new IOException("Not an image (Content-Type: " + type + ")");
|
|
232
237
|
}
|
|
@@ -54,11 +54,14 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
|
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// The first root cause's message, e.g. "Not Found, status code: 404".
|
|
58
57
|
// The first root cause's message, e.g. "Not Found, status code: 404" (also
|
|
59
|
-
// for preload results).
|
|
58
|
+
// for preload results). For an SVG without AndroidSVG, what to add: other
|
|
59
|
+
// decoders (e.g. Glide's video one) also tried it, and failed first.
|
|
60
60
|
static String errorMessage(@androidx.annotation.Nullable GlideException e) {
|
|
61
61
|
if (e != null) {
|
|
62
|
+
for (Throwable cause : e.getRootCauses()) {
|
|
63
|
+
if (FastImageSvg.MISSING.equals(cause.getMessage())) return FastImageSvg.MISSING;
|
|
64
|
+
}
|
|
62
65
|
for (Throwable cause : e.getRootCauses()) {
|
|
63
66
|
if (cause.getMessage() != null) return cause.getMessage();
|
|
64
67
|
}
|
|
@@ -57,6 +57,13 @@ public class FastImageSource extends ImageSource {
|
|
|
57
57
|
super(context, source, width, height);
|
|
58
58
|
mHeaders = headers == null ? Headers.DEFAULT : headers;
|
|
59
59
|
mUri = super.getUri();
|
|
60
|
+
if (mUri == null || TextUtils.isEmpty(mUri.toString())) {
|
|
61
|
+
// A bundled image that release builds package as a raw resource
|
|
62
|
+
// (e.g. an SVG, which isn't a drawable): React Native's
|
|
63
|
+
// ImageSource only looks for drawables.
|
|
64
|
+
Uri raw = rawResourceUri(context, source);
|
|
65
|
+
if (raw != null) mUri = raw;
|
|
66
|
+
}
|
|
60
67
|
|
|
61
68
|
if (isResource() && TextUtils.isEmpty(mUri.toString())) {
|
|
62
69
|
throw new Resources.NotFoundException("Local Resource Not Found. Resource: '" + getSource() + "'.");
|
|
@@ -76,6 +83,16 @@ public class FastImageSource extends ImageSource {
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
|
|
86
|
+
// The raw resource with the source's name (a bundled image's source is
|
|
87
|
+
// its resource name, e.g. src_images_logo), or null.
|
|
88
|
+
@Nullable
|
|
89
|
+
private static Uri rawResourceUri(Context context, @Nullable String source) {
|
|
90
|
+
if (source == null || source.isEmpty() || source.contains(":")) return null;
|
|
91
|
+
int id = context.getResources().getIdentifier(source, "raw", context.getPackageName());
|
|
92
|
+
if (id == 0) return null;
|
|
93
|
+
return Uri.parse(ANDROID_RESOURCE_SCHEME + "://" + context.getPackageName() + "/" + id);
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
public boolean isBase64Resource() {
|
|
80
97
|
return mUri != null && FastImageSource.isBase64Uri(mUri);
|
|
81
98
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
|
|
5
|
+
import androidx.annotation.NonNull;
|
|
6
|
+
import androidx.annotation.Nullable;
|
|
7
|
+
|
|
8
|
+
import com.bumptech.glide.Registry;
|
|
9
|
+
import com.bumptech.glide.load.Options;
|
|
10
|
+
import com.bumptech.glide.load.ResourceDecoder;
|
|
11
|
+
import com.bumptech.glide.load.engine.Resource;
|
|
12
|
+
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
|
|
13
|
+
import com.bumptech.glide.util.ByteBufferUtil;
|
|
14
|
+
|
|
15
|
+
import java.io.ByteArrayOutputStream;
|
|
16
|
+
import java.io.IOException;
|
|
17
|
+
import java.io.InputStream;
|
|
18
|
+
import java.nio.ByteBuffer;
|
|
19
|
+
import java.nio.charset.StandardCharsets;
|
|
20
|
+
import java.util.Locale;
|
|
21
|
+
|
|
22
|
+
// SVG images (static), drawn with AndroidSVG when the app has it: either of
|
|
23
|
+
// its packages (com.caverock:androidsvg or com.caverock:androidsvg-aar).
|
|
24
|
+
// FastImage compiles against it but doesn't ship it, so an app never gets a
|
|
25
|
+
// second copy of it (from another library, or its own). Without it, an SVG
|
|
26
|
+
// image fails with a message saying what to add.
|
|
27
|
+
//
|
|
28
|
+
// The decoders come after Glide's own, so they're only asked about data
|
|
29
|
+
// Glide couldn't decode, and only take data that starts like an SVG. This
|
|
30
|
+
// class doesn't use AndroidSVG itself (FastImageSvgRenderer does), so it
|
|
31
|
+
// loads without it.
|
|
32
|
+
final class FastImageSvg {
|
|
33
|
+
static final String MISSING =
|
|
34
|
+
"SVG images need AndroidSVG: add implementation 'com.caverock:androidsvg-aar:1.4' to the app's dependencies";
|
|
35
|
+
private static final int HEAD_LENGTH = 1024;
|
|
36
|
+
@Nullable
|
|
37
|
+
private static Boolean available;
|
|
38
|
+
|
|
39
|
+
private FastImageSvg() {
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static synchronized boolean available() {
|
|
43
|
+
if (available == null) {
|
|
44
|
+
try {
|
|
45
|
+
Class.forName("com.caverock.androidsvg.SVG");
|
|
46
|
+
available = true;
|
|
47
|
+
} catch (Throwable e) {
|
|
48
|
+
available = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return available;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static void register(@NonNull Registry registry, @NonNull BitmapPool pool) {
|
|
55
|
+
registry.append(InputStream.class, Bitmap.class, new StreamDecoder(pool))
|
|
56
|
+
// Data Glide holds in memory, e.g. from its disk cache.
|
|
57
|
+
.append(ByteBuffer.class, Bitmap.class, new BufferDecoder(pool));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Whether the data starts like an SVG document: its first element is
|
|
61
|
+
// <svg> (after a byte order mark, whitespace, an XML declaration, comments
|
|
62
|
+
// or a doctype), so e.g. an HTML page with an inline SVG isn't one.
|
|
63
|
+
static boolean looksLikeSvg(@NonNull byte[] head, int length) {
|
|
64
|
+
String text = new String(head, 0, length, StandardCharsets.UTF_8).toLowerCase(Locale.ROOT);
|
|
65
|
+
int i = text.startsWith("\uFEFF") ? 1 : 0;
|
|
66
|
+
while (i < text.length()) {
|
|
67
|
+
char c = text.charAt(i);
|
|
68
|
+
if (Character.isWhitespace(c)) {
|
|
69
|
+
i++;
|
|
70
|
+
} else if (text.startsWith("<?", i)) {
|
|
71
|
+
i = skipPast(text, i, "?>");
|
|
72
|
+
} else if (text.startsWith("<!--", i)) {
|
|
73
|
+
i = skipPast(text, i, "-->");
|
|
74
|
+
} else if (text.startsWith("<!", i)) {
|
|
75
|
+
// A doctype, whose internal subset ([...]) can have '>'s.
|
|
76
|
+
int subset = text.indexOf('[', i);
|
|
77
|
+
int close = text.indexOf('>', i);
|
|
78
|
+
if (subset >= 0 && subset < close) i = skipPast(text, subset, "]");
|
|
79
|
+
if (i >= 0) i = skipPast(text, i, ">");
|
|
80
|
+
} else {
|
|
81
|
+
return text.startsWith("<svg", i);
|
|
82
|
+
}
|
|
83
|
+
if (i < 0) return false;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The index after the end marker from i, or -1 if it isn't there.
|
|
89
|
+
private static int skipPast(String text, int i, String end) {
|
|
90
|
+
int found = text.indexOf(end, i);
|
|
91
|
+
return found < 0 ? -1 : found + end.length();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private static byte[] readAll(InputStream stream) throws IOException {
|
|
95
|
+
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
96
|
+
byte[] chunk = new byte[16 * 1024];
|
|
97
|
+
int read;
|
|
98
|
+
while ((read = stream.read(chunk)) != -1) {
|
|
99
|
+
out.write(chunk, 0, read);
|
|
100
|
+
}
|
|
101
|
+
return out.toByteArray();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private static Resource<Bitmap> decode(byte[] data, int width, int height, Options options, BitmapPool pool)
|
|
105
|
+
throws IOException {
|
|
106
|
+
if (!available()) {
|
|
107
|
+
throw new IOException(MISSING);
|
|
108
|
+
}
|
|
109
|
+
return FastImageSvgRenderer.render(data, width, height, options, pool);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private static final class StreamDecoder implements ResourceDecoder<InputStream, Bitmap> {
|
|
113
|
+
private final BitmapPool pool;
|
|
114
|
+
|
|
115
|
+
StreamDecoder(BitmapPool pool) {
|
|
116
|
+
this.pool = pool;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Override
|
|
120
|
+
public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException {
|
|
121
|
+
// Glide rewinds the stream before decoding.
|
|
122
|
+
byte[] head = new byte[HEAD_LENGTH];
|
|
123
|
+
int length = 0;
|
|
124
|
+
int read;
|
|
125
|
+
while (length < head.length && (read = source.read(head, length, head.length - length)) != -1) {
|
|
126
|
+
length += read;
|
|
127
|
+
}
|
|
128
|
+
return looksLikeSvg(head, length);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Nullable
|
|
132
|
+
@Override
|
|
133
|
+
public Resource<Bitmap> decode(@NonNull InputStream source, int width, int height, @NonNull Options options)
|
|
134
|
+
throws IOException {
|
|
135
|
+
return FastImageSvg.decode(readAll(source), width, height, options, pool);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static final class BufferDecoder implements ResourceDecoder<ByteBuffer, Bitmap> {
|
|
140
|
+
private final BitmapPool pool;
|
|
141
|
+
|
|
142
|
+
BufferDecoder(BitmapPool pool) {
|
|
143
|
+
this.pool = pool;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Override
|
|
147
|
+
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
|
|
148
|
+
ByteBuffer copy = source.duplicate();
|
|
149
|
+
byte[] head = new byte[Math.min(HEAD_LENGTH, copy.remaining())];
|
|
150
|
+
copy.get(head);
|
|
151
|
+
return looksLikeSvg(head, head.length);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Nullable
|
|
155
|
+
@Override
|
|
156
|
+
public Resource<Bitmap> decode(@NonNull ByteBuffer source, int width, int height, @NonNull Options options)
|
|
157
|
+
throws IOException {
|
|
158
|
+
return FastImageSvg.decode(readAll(ByteBufferUtil.toStream(source)), width, height, options, pool);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.graphics.Canvas;
|
|
5
|
+
import android.graphics.RectF;
|
|
6
|
+
|
|
7
|
+
import com.bumptech.glide.load.Options;
|
|
8
|
+
import com.bumptech.glide.load.engine.Resource;
|
|
9
|
+
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
|
|
10
|
+
import com.bumptech.glide.load.resource.bitmap.BitmapResource;
|
|
11
|
+
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
|
12
|
+
import com.caverock.androidsvg.SVG;
|
|
13
|
+
import com.caverock.androidsvg.SVGParseException;
|
|
14
|
+
|
|
15
|
+
import java.io.ByteArrayInputStream;
|
|
16
|
+
import java.io.IOException;
|
|
17
|
+
|
|
18
|
+
// Draws an SVG into a bitmap with AndroidSVG, at the size the request's
|
|
19
|
+
// downsample strategy gives for the view (as Glide decodes other images, but
|
|
20
|
+
// also larger than the SVG's own size, as it's drawn, not scaled up). Glide
|
|
21
|
+
// then applies the view's transformations (scale type, blur) as for any
|
|
22
|
+
// bitmap. Only used when the app has AndroidSVG (see FastImageSvg).
|
|
23
|
+
final class FastImageSvgRenderer {
|
|
24
|
+
// An SVG with neither a size nor a viewBox, as browsers size one.
|
|
25
|
+
private static final float DEFAULT_WIDTH = 300;
|
|
26
|
+
private static final float DEFAULT_HEIGHT = 150;
|
|
27
|
+
// At most this many pixels (a 4096 x 4096 bitmap), however large the view.
|
|
28
|
+
private static final float MAX_PIXELS = 4096f * 4096f;
|
|
29
|
+
|
|
30
|
+
private FastImageSvgRenderer() {
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static Resource<Bitmap> render(byte[] data, int width, int height, Options options, BitmapPool pool)
|
|
34
|
+
throws IOException {
|
|
35
|
+
SVG svg;
|
|
36
|
+
try {
|
|
37
|
+
svg = SVG.getFromInputStream(new ByteArrayInputStream(data));
|
|
38
|
+
} catch (SVGParseException e) {
|
|
39
|
+
throw new IOException("Cannot decode the SVG: " + e.getMessage(), e);
|
|
40
|
+
}
|
|
41
|
+
// The SVG's own size: its width and height, or its viewBox's.
|
|
42
|
+
RectF viewBox = svg.getDocumentViewBox();
|
|
43
|
+
float ownWidth = svg.getDocumentWidth();
|
|
44
|
+
float ownHeight = svg.getDocumentHeight();
|
|
45
|
+
if (viewBox != null && viewBox.width() > 0 && viewBox.height() > 0) {
|
|
46
|
+
if (ownWidth <= 0 && ownHeight <= 0) {
|
|
47
|
+
ownWidth = viewBox.width();
|
|
48
|
+
ownHeight = viewBox.height();
|
|
49
|
+
} else if (ownWidth <= 0) {
|
|
50
|
+
ownWidth = ownHeight * viewBox.width() / viewBox.height();
|
|
51
|
+
} else if (ownHeight <= 0) {
|
|
52
|
+
ownHeight = ownWidth * viewBox.height() / viewBox.width();
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (ownWidth <= 0 || ownHeight <= 0) {
|
|
56
|
+
ownWidth = DEFAULT_WIDTH;
|
|
57
|
+
ownHeight = DEFAULT_HEIGHT;
|
|
58
|
+
}
|
|
59
|
+
// Scaled to the bitmap through a viewBox of its own size.
|
|
60
|
+
svg.setDocumentViewBox(0, 0, ownWidth, ownHeight);
|
|
61
|
+
}
|
|
62
|
+
int sourceWidth = Math.max(1, Math.round(ownWidth));
|
|
63
|
+
int sourceHeight = Math.max(1, Math.round(ownHeight));
|
|
64
|
+
|
|
65
|
+
// The strategy also records the SVG's own size for onLoad
|
|
66
|
+
// (FastImageSourceSize.Capture).
|
|
67
|
+
DownsampleStrategy strategy = options.get(DownsampleStrategy.OPTION);
|
|
68
|
+
int targetWidth = width > 0 ? width : sourceWidth;
|
|
69
|
+
int targetHeight = height > 0 ? height : sourceHeight;
|
|
70
|
+
float scale = strategy != null
|
|
71
|
+
? strategy.getScaleFactor(sourceWidth, sourceHeight, targetWidth, targetHeight)
|
|
72
|
+
: 1f;
|
|
73
|
+
if (Float.isNaN(scale) || Float.isInfinite(scale) || scale <= 0) scale = 1f;
|
|
74
|
+
float pixels = sourceWidth * scale * sourceHeight * scale;
|
|
75
|
+
if (pixels > MAX_PIXELS) scale *= (float) Math.sqrt(MAX_PIXELS / pixels);
|
|
76
|
+
int bitmapWidth = Math.max(1, Math.round(sourceWidth * scale));
|
|
77
|
+
int bitmapHeight = Math.max(1, Math.round(sourceHeight * scale));
|
|
78
|
+
|
|
79
|
+
svg.setDocumentWidth(bitmapWidth);
|
|
80
|
+
svg.setDocumentHeight(bitmapHeight);
|
|
81
|
+
// Cleared (transparent), from Glide's pool.
|
|
82
|
+
Bitmap bitmap = pool.get(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
|
|
83
|
+
svg.renderToCanvas(new Canvas(bitmap));
|
|
84
|
+
return BitmapResource.obtain(bitmap, pool);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
|
|
|
14
14
|
|
|
15
15
|
import com.bumptech.glide.Glide;
|
|
16
16
|
import com.bumptech.glide.RequestManager;
|
|
17
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
17
18
|
import com.facebook.react.bridge.ReadableMap;
|
|
18
19
|
import com.facebook.react.bridge.WritableMap;
|
|
19
20
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
@@ -69,6 +70,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
69
70
|
view.setSource(source);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
@ReactProp(name = "sources")
|
|
74
|
+
public void setSources(FastImageViewWithUrl view, @Nullable ReadableArray sources) {
|
|
75
|
+
view.setSources(sources);
|
|
76
|
+
}
|
|
77
|
+
|
|
72
78
|
@ReactProp(name = "defaultSource")
|
|
73
79
|
public void setDefaultSource(FastImageViewWithUrl view, @Nullable String source) {
|
|
74
80
|
view.setDefaultSource(
|
|
@@ -35,6 +35,7 @@ import com.bumptech.glide.request.target.SizeReadyCallback;
|
|
|
35
35
|
import com.bumptech.glide.request.transition.NoTransition;
|
|
36
36
|
import com.bumptech.glide.request.transition.Transition;
|
|
37
37
|
import com.bumptech.glide.request.transition.TransitionFactory;
|
|
38
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
38
39
|
import com.facebook.react.bridge.ReadableMap;
|
|
39
40
|
import com.facebook.react.bridge.WritableMap;
|
|
40
41
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
@@ -63,7 +64,89 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
63
64
|
|
|
64
65
|
public void setSource(@Nullable ReadableMap source) {
|
|
65
66
|
mNeedsReload = true;
|
|
66
|
-
|
|
67
|
+
mPropSource = source;
|
|
68
|
+
if (mSources == null) mSource = source;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The `source` prop. With several `sources`, mSource is the one picked for
|
|
72
|
+
// the view's size instead.
|
|
73
|
+
@Nullable
|
|
74
|
+
private ReadableMap mPropSource = null;
|
|
75
|
+
// Several sources (2 or more) of the same image at different sizes, or
|
|
76
|
+
// null: the view loads the one whose size is closest to its own.
|
|
77
|
+
@Nullable
|
|
78
|
+
private ReadableArray mSources = null;
|
|
79
|
+
// Several sources: the index of the one picked (-1 before one is), and
|
|
80
|
+
// whether the load waits for the view's size (after layout), or has
|
|
81
|
+
// waited (then it loads the largest if the view still has no size).
|
|
82
|
+
private int mSourceIndex = -1;
|
|
83
|
+
private boolean mWaitsForSize = false;
|
|
84
|
+
private boolean mWaitedForSize = false;
|
|
85
|
+
// Several sources: the next load switches to another one for a new view
|
|
86
|
+
// size. It's the same picture at another size, so it doesn't fade in.
|
|
87
|
+
private boolean mSwitchesSource = false;
|
|
88
|
+
// What the last update was called with, to load once the view has a size.
|
|
89
|
+
@Nullable
|
|
90
|
+
private FastImageViewManager mManager;
|
|
91
|
+
@Nullable
|
|
92
|
+
private Map<String, List<FastImageViewWithUrl>> mViewsForUrlsMap;
|
|
93
|
+
|
|
94
|
+
public void setSources(@Nullable ReadableArray sources) {
|
|
95
|
+
mNeedsReload = true;
|
|
96
|
+
mSources = sources != null && sources.size() > 1 ? sources : null;
|
|
97
|
+
mSourceIndex = -1;
|
|
98
|
+
if (mSources == null) mSource = mPropSource;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Of several sources, the index of the one whose size in pixels (width ×
|
|
102
|
+
// height × scale²) is closest to the view's (by pixel count), or of the
|
|
103
|
+
// largest while the view has no size (e.g. one sized from onLoad).
|
|
104
|
+
private int sourceIndexForSize() {
|
|
105
|
+
ReadableArray sources = mSources;
|
|
106
|
+
if (sources == null) return -1;
|
|
107
|
+
double viewPixels = (double) getWidth() * getHeight();
|
|
108
|
+
int best = -1;
|
|
109
|
+
double bestFit = Double.MAX_VALUE;
|
|
110
|
+
for (int i = 0; i < sources.size(); i++) {
|
|
111
|
+
ReadableMap source = sources.getMap(i);
|
|
112
|
+
if (source == null) continue;
|
|
113
|
+
double scale = number(source, "scale", 1);
|
|
114
|
+
double pixels = number(source, "width", 0) * number(source, "height", 0) * scale * scale;
|
|
115
|
+
double fit = viewPixels > 0 ? Math.abs(1 - pixels / viewPixels) : -pixels;
|
|
116
|
+
if (best < 0 || fit < bestFit) {
|
|
117
|
+
best = i;
|
|
118
|
+
bestFit = fit;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return best;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Picks the source for the view's size (several sources).
|
|
125
|
+
private void pickSource() {
|
|
126
|
+
mSourceIndex = sourceIndexForSize();
|
|
127
|
+
mSource = mSourceIndex >= 0 && mSources != null ? mSources.getMap(mSourceIndex) : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private static double number(ReadableMap map, String key, double fallback) {
|
|
131
|
+
return map.hasKey(key) && !map.isNull(key) ? map.getDouble(key) : fallback;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Several sources: another one fits the view's new size better. Loads it
|
|
135
|
+
// (after layout), keeping the image showing until then, without a fade.
|
|
136
|
+
private boolean switchSourceIfResized() {
|
|
137
|
+
if (mSources == null || mNeedsReload || mWaitsForSize || getWidth() <= 0 || getHeight() <= 0
|
|
138
|
+
|| mManager == null || sourceIndexForSize() == mSourceIndex) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
mSwitchesSource = true;
|
|
142
|
+
mNeedsReload = true;
|
|
143
|
+
post(new Runnable() {
|
|
144
|
+
@Override
|
|
145
|
+
public void run() {
|
|
146
|
+
if (mNeedsReload && mManager != null) onAfterUpdate(mManager, requestManager, mViewsForUrlsMap);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return true;
|
|
67
150
|
}
|
|
68
151
|
|
|
69
152
|
public void setDefaultSource(@Nullable Drawable source) {
|
|
@@ -520,7 +603,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
520
603
|
mFadeFrom = null;
|
|
521
604
|
invalidate();
|
|
522
605
|
}
|
|
523
|
-
reloadIfResized();
|
|
606
|
+
if (!switchSourceIfResized()) reloadIfResized();
|
|
524
607
|
}
|
|
525
608
|
|
|
526
609
|
private void reloadIfResized() {
|
|
@@ -671,16 +754,42 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
671
754
|
@Nonnull FastImageViewManager manager,
|
|
672
755
|
@Nullable RequestManager requestManager,
|
|
673
756
|
@Nonnull Map<String, List<FastImageViewWithUrl>> viewsForUrlsMap) {
|
|
757
|
+
mManager = manager;
|
|
758
|
+
mViewsForUrlsMap = viewsForUrlsMap;
|
|
674
759
|
if (mBlurChanged && !mNeedsReload) {
|
|
675
760
|
mBlurChanged = false;
|
|
676
761
|
reblur();
|
|
677
762
|
}
|
|
678
763
|
if (!mNeedsReload)
|
|
679
764
|
return;
|
|
765
|
+
// Several sources: one is picked for the view's size, so a view that
|
|
766
|
+
// hasn't been laid out yet loads after layout (props and layout are
|
|
767
|
+
// applied in the same batch). A view that still has no size then
|
|
768
|
+
// (e.g. one sized from onLoad) loads the largest source.
|
|
769
|
+
if (mSources != null) {
|
|
770
|
+
if (mWaitsForSize) return;
|
|
771
|
+
if ((getWidth() <= 0 || getHeight() <= 0) && !mWaitedForSize) {
|
|
772
|
+
mWaitsForSize = true;
|
|
773
|
+
post(new Runnable() {
|
|
774
|
+
@Override
|
|
775
|
+
public void run() {
|
|
776
|
+
if (!mWaitsForSize) return;
|
|
777
|
+
mWaitsForSize = false;
|
|
778
|
+
mWaitedForSize = true;
|
|
779
|
+
onAfterUpdate(manager, requestManager, viewsForUrlsMap);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
mWaitedForSize = false;
|
|
785
|
+
pickSource();
|
|
786
|
+
}
|
|
680
787
|
// Only reload for changes that affect the request (source,
|
|
681
788
|
// defaultSource, resizeMode), not for every prop update.
|
|
682
789
|
mNeedsReload = false;
|
|
683
790
|
mBlurChanged = false;
|
|
791
|
+
boolean switching = mSwitchesSource;
|
|
792
|
+
mSwitchesSource = false;
|
|
684
793
|
boolean restarting = mRestarting;
|
|
685
794
|
mRestarting = false;
|
|
686
795
|
mLoadCount++;
|
|
@@ -791,7 +900,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
791
900
|
// replaces it at once, as Glide, Coil and Fresco fade an image in
|
|
792
901
|
// from nothing and never between images.
|
|
793
902
|
boolean replacing = shownRequest != null && model != null;
|
|
794
|
-
if (mTransitionDuration > 0 && (!replacing || mTransitionBetweenImages)) {
|
|
903
|
+
if (mTransitionDuration > 0 && !switching && (!replacing || mTransitionBetweenImages)) {
|
|
795
904
|
builder = builder.transition(DrawableTransitionOptions.with(
|
|
796
905
|
new FadeFactory(mTransitionDuration, mTransitionSkipOnCacheHit)));
|
|
797
906
|
}
|
package/dist/index.cjs.js
CHANGED
|
@@ -125,7 +125,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
125
125
|
};
|
|
126
126
|
const resolvedTintColor = tintColor != null ? tintColor : tintColorFromStyle(style);
|
|
127
127
|
if (fallback) {
|
|
128
|
-
const cleanedSource = typeof source === "number" ? source : withoutCache(source);
|
|
128
|
+
const cleanedSource = typeof source === "number" ? source : Array.isArray(source) ? source.map(withoutCache) : withoutCache(source);
|
|
129
129
|
const resolvedSource = react_native.Image.resolveAssetSource(cleanedSource);
|
|
130
130
|
return /* @__PURE__ */ react.default.createElement(react_native.View, {
|
|
131
131
|
style: [styles.imageContainer, style],
|
|
@@ -164,7 +164,9 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
164
164
|
resizeMode
|
|
165
165
|
}), children);
|
|
166
166
|
}
|
|
167
|
-
const
|
|
167
|
+
const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
|
|
168
|
+
const single = Array.isArray(source) ? source[0] : source;
|
|
169
|
+
const resolvedSource = sources ? void 0 : react_native.Image.resolveAssetSource(single);
|
|
168
170
|
const resolvedDefaultSource = resolveDefaultSource(defaultSource);
|
|
169
171
|
return /* @__PURE__ */ react.default.createElement(react_native.View, {
|
|
170
172
|
style: [styles.imageContainer, style],
|
|
@@ -177,6 +179,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
177
179
|
...transitionProps(transition),
|
|
178
180
|
style: react_native.StyleSheet.absoluteFill,
|
|
179
181
|
source: resolvedSource,
|
|
182
|
+
sources,
|
|
180
183
|
defaultSource: resolvedDefaultSource,
|
|
181
184
|
onFastImageLoadStart: onLoadStart,
|
|
182
185
|
onFastImageProgress: withProgress(onProgress),
|
|
@@ -200,8 +203,14 @@ const FastImage = FastImageComponent;
|
|
|
200
203
|
FastImage.resizeMode = resizeMode;
|
|
201
204
|
FastImage.cacheControl = cacheControl;
|
|
202
205
|
FastImage.priority = priority;
|
|
203
|
-
|
|
206
|
+
const ONE_SOURCE = "Takes one source, not an array of sizes: pass the size to use";
|
|
207
|
+
FastImage.preload = (sources) => Promise.resolve(react_native.NativeModules.FastImageView.preload(sources.map((s) => s && !Array.isArray(s) ? s : {}))).then((results) => sources.map((source, i) => {
|
|
204
208
|
var _results$i;
|
|
209
|
+
if (Array.isArray(source)) return {
|
|
210
|
+
ok: false,
|
|
211
|
+
error: ONE_SOURCE,
|
|
212
|
+
uri: void 0
|
|
213
|
+
};
|
|
205
214
|
const uri = source ? source.uri : void 0;
|
|
206
215
|
const result = (_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : noResult;
|
|
207
216
|
if (!result.ok) return {
|
|
@@ -220,8 +229,14 @@ FastImage.preload = (sources) => Promise.resolve(react_native.NativeModules.Fast
|
|
|
220
229
|
FastImage.clearMemoryCache = () => react_native.NativeModules.FastImageView.clearMemoryCache();
|
|
221
230
|
FastImage.clearDiskCache = () => react_native.NativeModules.FastImageView.clearDiskCache();
|
|
222
231
|
FastImage.configureCache = (limits = {}) => Promise.resolve(react_native.NativeModules.FastImageView.configureCache(limits));
|
|
223
|
-
FastImage.writeToCache = (source, file) =>
|
|
224
|
-
|
|
232
|
+
FastImage.writeToCache = (source, file) => Array.isArray(source) ? Promise.resolve({
|
|
233
|
+
ok: false,
|
|
234
|
+
error: ONE_SOURCE
|
|
235
|
+
}) : Promise.resolve(react_native.NativeModules.FastImageView.writeToCache(source || {}, file));
|
|
236
|
+
FastImage.getCachePath = (source) => Array.isArray(source) ? Promise.resolve({
|
|
237
|
+
ok: false,
|
|
238
|
+
error: ONE_SOURCE
|
|
239
|
+
}) : Promise.resolve(react_native.NativeModules.FastImageView.getCachePath(source || {}));
|
|
225
240
|
const styles = react_native.StyleSheet.create({
|
|
226
241
|
fallbackImage: {
|
|
227
242
|
position: "absolute",
|