react-native-fast-image 9.2.1 → 9.3.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 +40 -9
- package/android/src/main/java/com/dylanvann/fastimage/FastImageBlur.java +18 -5
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +5 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +22 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSvg.java +27 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSvgRenderer.java +10 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +5 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +1 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +157 -14
- package/dist/index.cjs.js +50 -18
- package/dist/index.cjs.js.flow +3 -0
- package/dist/index.cjs.web.js +241 -130
- package/dist/index.cjs.web.js.flow +3 -0
- package/dist/index.d.ts +38 -6
- package/dist/index.js +50 -18
- package/dist/index.js.flow +3 -0
- package/dist/index.web.js +243 -132
- package/dist/index.web.js.flow +3 -0
- package/dist/objectFit.d.ts +3 -0
- package/ios/FastImage/FFFDownsampledImage.h +8 -0
- package/ios/FastImage/FFFDownsampledImage.m +16 -0
- package/ios/FastImage/FFFastImageView.h +7 -1
- package/ios/FastImage/FFFastImageView.m +15 -5
- package/ios/FastImage/FFFastImageViewManager.m +3 -1
- package/ios/FastImage/RCTConvert+FFFastImage.h +3 -0
- package/ios/FastImage/RCTConvert+FFFastImage.m +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,8 @@ To set the cache's size when the app starts, see [Native config](#native-config)
|
|
|
76
76
|
|
|
77
77
|
If your Android app has its own Glide `AppGlideModule`, read [using FastImage with an AppGlideModule](docs/android-build-settings.md#if-your-app-has-its-own-appglidemodule) first, or FastImage may not work.
|
|
78
78
|
|
|
79
|
+
On iOS, FastImage works with SDWebImage 5.11.1 and later, but apps that run on iOS 26 or later need 5.21.4 or later: earlier versions can crash there while loading images ([SDWebImage#3849](https://github.com/SDWebImage/SDWebImage/issues/3849)). A new install gets the latest version. If your `Podfile.lock` has an older one, update it with `cd ios && pod update SDWebImage`.
|
|
80
|
+
|
|
79
81
|
## Usage
|
|
80
82
|
|
|
81
83
|
```jsx
|
|
@@ -89,7 +91,7 @@ const YourImage = () => (
|
|
|
89
91
|
headers: { Authorization: 'someAuthToken' },
|
|
90
92
|
priority: 'normal',
|
|
91
93
|
}}
|
|
92
|
-
|
|
94
|
+
objectFit="contain"
|
|
93
95
|
/>
|
|
94
96
|
)
|
|
95
97
|
```
|
|
@@ -233,16 +235,36 @@ On Android, `defaultSource` doesn't show in debug builds: there the dev server s
|
|
|
233
235
|
|
|
234
236
|
---
|
|
235
237
|
|
|
238
|
+
#### `objectFit`
|
|
239
|
+
|
|
240
|
+
**Type:** `ObjectFit` · **Default:** `'cover'`
|
|
241
|
+
|
|
242
|
+
How the image fits the view, as CSS's `object-fit` does.
|
|
243
|
+
|
|
244
|
+
- `'cover'`: scales it uniformly (keeping its aspect ratio) so it covers the view (minus padding), cropping what doesn't fit.
|
|
245
|
+
- `'contain'`: scales it uniformly (keeping its aspect ratio) so all of it fits in the view (minus padding).
|
|
246
|
+
- `'fill'`: scales its width and height separately to fill the view, which can change its aspect ratio.
|
|
247
|
+
- `'none'`: shows it at its own size, centered, cropped if it's larger than the view.
|
|
248
|
+
- `'scale-down'`: shows it at its own size, centered, or scaled down uniformly to fit if it's larger than the view (the smaller of `'none'` and `'contain'`).
|
|
249
|
+
|
|
250
|
+
An image's own size is its size in pixels, as points (dp on Android), as CSS counts an image's pixels: a 300 × 200 image is 300 × 200 points. A bundled image (`require()`) is its size in points.
|
|
251
|
+
|
|
252
|
+
It can also be set in `style`; the prop wins. Either one overrides `resizeMode`.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
236
256
|
#### `resizeMode`
|
|
237
257
|
|
|
238
258
|
**Type:** `ResizeMode` · **Default:** `'cover'`
|
|
239
259
|
|
|
260
|
+
**Deprecated:** Use `objectFit` instead, except for `'repeat'`, which has no `objectFit` value and isn't deprecated.
|
|
261
|
+
|
|
240
262
|
How the image fills the view.
|
|
241
263
|
|
|
242
|
-
- `'contain'`:
|
|
243
|
-
- `'cover'`:
|
|
244
|
-
- `'stretch'`:
|
|
245
|
-
- `'center'`:
|
|
264
|
+
- `'contain'`: as `objectFit="contain"`.
|
|
265
|
+
- `'cover'`: as `objectFit="cover"`.
|
|
266
|
+
- `'stretch'`: as `objectFit="fill"`.
|
|
267
|
+
- `'center'`: as `objectFit="scale-down"`, except that on Android an image smaller than the view is shown at its size in pixels on the screen, so smaller than on iOS and the web.
|
|
246
268
|
- `'repeat'`: repeats it to cover the view, from its top-left corner, at the image's own size in pixels (a bundled image at its size in points), scaled down to fit if it's larger than the view. An animated image repeats its first frame, and `defaultSource` repeats too.
|
|
247
269
|
|
|
248
270
|
---
|
|
@@ -529,9 +551,9 @@ for (const result of results) {
|
|
|
529
551
|
}
|
|
530
552
|
```
|
|
531
553
|
|
|
532
|
-
Each source's `cache` applies, as for a view: with `web` the preload follows the HTTP cache, and with `cacheOnly` it doesn't download. A `cacheOnly` preload resolves `ok` only if the image is cached
|
|
554
|
+
Each source's `cache` applies, as for a view: with `web` the preload follows the HTTP cache, and with `cacheOnly` it doesn't download. A `cacheOnly` preload resolves `ok` only if the image is cached.
|
|
533
555
|
|
|
534
|
-
A source with `memoryCache: false` is only downloaded to the disk cache, without being decoded into memory, and is decoded when it's shown. Use it to preload many images, or large ones, e.g. the next pages of a feed: a decoded photo can take tens of MB of memory. Other sources are
|
|
556
|
+
A source with `memoryCache: false` is only downloaded to the disk cache, without being decoded into memory, and is decoded when it's shown. Use it to preload many images, or large ones, e.g. the next pages of a feed: a decoded photo can take tens of MB of memory. Other sources are decoded too. A view showing a preloaded image loads it from the cache for its own size: it doesn't wait for the network, but it fades in as other images from the disk cache do (see `transition`'s `skipOnCacheHit`).
|
|
535
557
|
|
|
536
558
|
```js
|
|
537
559
|
await FastImage.preload(photos.map((uri) => ({ uri, memoryCache: false })))
|
|
@@ -684,6 +706,14 @@ A load's result, as `onLoadEnd` gets it: `ok` with the image's size, or not `ok`
|
|
|
684
706
|
|
|
685
707
|
---
|
|
686
708
|
|
|
709
|
+
### `ObjectFit`
|
|
710
|
+
|
|
711
|
+
**Type:** `'fill' | 'contain' | 'cover' | 'none' | 'scale-down'`
|
|
712
|
+
|
|
713
|
+
How the image fits the view: see [`objectFit`](#objectfit).
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
687
717
|
### `OnErrorEvent`
|
|
688
718
|
|
|
689
719
|
`onError`'s event: `nativeEvent.error` says what went wrong, e.g. an HTTP status code or an image that can't be decoded.
|
|
@@ -824,9 +854,10 @@ FastImage works on the web with [react-native-web](https://necolas.github.io/rea
|
|
|
824
854
|
|
|
825
855
|
Support is minimal:
|
|
826
856
|
|
|
827
|
-
- 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`.
|
|
857
|
+
- Props that work: `source` (a `uri`, a `require()`d image, or several sizes), `defaultSource`, `objectFit`, `resizeMode`, `tintColor`, `blurRadius`, `style`, children, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
|
|
828
858
|
- 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`.
|
|
829
|
-
-
|
|
859
|
+
- The image is an `<img>` element that fills the view inside its borders, with `objectFit` as its CSS `object-fit` (and `resizeMode`'s `stretch` as `fill`, `center` as `scale-down`). With `resizeMode="repeat"`, it's a repeated CSS background, with a transparent `<img>` over it. `tintColor` is an SVG filter and `blurRadius` a CSS `blur()`, and `defaultSource` is another `<img>`, under the image until it has loaded. A new `source` gets a new `<img>`, so the previous image doesn't stay while it loads (with several sizes, the browser shows it until the new one has loaded).
|
|
860
|
+
- 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. `resizeMode="repeat"` shows them as `cover`.
|
|
830
861
|
- `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' }`.
|
|
831
862
|
|
|
832
863
|
## Troubleshooting
|
|
@@ -32,10 +32,17 @@ final class FastImageBlur extends BitmapTransformation {
|
|
|
32
32
|
// In pixels on screen.
|
|
33
33
|
private final float radius;
|
|
34
34
|
private final ImageView.ScaleType scaleType;
|
|
35
|
+
// Pixels on screen per pixel of the image at its own size (scale-down and
|
|
36
|
+
// none show it at that size: see FastImageViewWithUrl.updateImageMatrix;
|
|
37
|
+
// 1 for resizeMode center).
|
|
38
|
+
private final float density;
|
|
35
39
|
|
|
36
|
-
FastImageBlur(float radius, ImageView.ScaleType scaleType) {
|
|
40
|
+
FastImageBlur(float radius, ImageView.ScaleType scaleType, float density) {
|
|
37
41
|
this.radius = radius;
|
|
38
42
|
this.scaleType = scaleType;
|
|
43
|
+
this.density = scaleType == ImageView.ScaleType.CENTER_INSIDE || scaleType == ImageView.ScaleType.CENTER
|
|
44
|
+
? density
|
|
45
|
+
: 1;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
@Override
|
|
@@ -80,7 +87,9 @@ final class FastImageBlur extends BitmapTransformation {
|
|
|
80
87
|
case FIT_XY:
|
|
81
88
|
return (float) Math.sqrt(x * y);
|
|
82
89
|
case CENTER_INSIDE:
|
|
83
|
-
return Math.min(Math.min(x, y),
|
|
90
|
+
return Math.min(Math.min(x, y), density);
|
|
91
|
+
case CENTER:
|
|
92
|
+
return density;
|
|
84
93
|
default:
|
|
85
94
|
return 1;
|
|
86
95
|
}
|
|
@@ -197,17 +206,21 @@ final class FastImageBlur extends BitmapTransformation {
|
|
|
197
206
|
public boolean equals(Object o) {
|
|
198
207
|
return o instanceof FastImageBlur
|
|
199
208
|
&& ((FastImageBlur) o).radius == radius
|
|
200
|
-
&& ((FastImageBlur) o).scaleType == scaleType
|
|
209
|
+
&& ((FastImageBlur) o).scaleType == scaleType
|
|
210
|
+
&& ((FastImageBlur) o).density == density;
|
|
201
211
|
}
|
|
202
212
|
|
|
203
213
|
@Override
|
|
204
214
|
public int hashCode() {
|
|
205
|
-
return ID.hashCode() * 31 + Float.floatToIntBits(radius) * 31 + (scaleType == null ? 0 : scaleType.ordinal())
|
|
215
|
+
return (ID.hashCode() * 31 + Float.floatToIntBits(radius) * 31 + (scaleType == null ? 0 : scaleType.ordinal())) * 31
|
|
216
|
+
+ Float.floatToIntBits(density);
|
|
206
217
|
}
|
|
207
218
|
|
|
208
219
|
@Override
|
|
209
220
|
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
|
210
221
|
messageDigest.update(ID_BYTES);
|
|
211
|
-
|
|
222
|
+
String key = radius + "," + scaleType;
|
|
223
|
+
if (density != 1) key += "," + density;
|
|
224
|
+
messageDigest.update(key.getBytes(Charset.forName("UTF-8")));
|
|
212
225
|
}
|
|
213
226
|
}
|
|
@@ -82,6 +82,8 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
|
|
|
82
82
|
boolean local = !(model instanceof FastImageUrl);
|
|
83
83
|
int[] size = FastImageSourceSize.get(resource, model, local,
|
|
84
84
|
dataSource == DataSource.RESOURCE_DISK_CACHE);
|
|
85
|
+
// Before the view shows it (the target does once this returns).
|
|
86
|
+
view.onImageSize(size);
|
|
85
87
|
if (size != null) {
|
|
86
88
|
sendLoad(view, size);
|
|
87
89
|
return false;
|
|
@@ -92,7 +94,9 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
|
|
|
92
94
|
FastImageSourceSize.readLocal(view.getContext(), source, resource, model, new FastImageSourceSize.Callback() {
|
|
93
95
|
@Override
|
|
94
96
|
public void onSize(int[] size) {
|
|
95
|
-
if (target.getRequest()
|
|
97
|
+
if (target.getRequest() != request) return;
|
|
98
|
+
view.onImageSize(size);
|
|
99
|
+
sendLoad(view, size);
|
|
96
100
|
}
|
|
97
101
|
});
|
|
98
102
|
return false;
|
|
@@ -128,6 +128,8 @@ final class FastImageSourceSize {
|
|
|
128
128
|
private static DownsampleStrategy strategy(@Nullable ImageView.ScaleType scaleType) {
|
|
129
129
|
if (scaleType == null) return DownsampleStrategy.DEFAULT;
|
|
130
130
|
switch (scaleType) {
|
|
131
|
+
case CENTER:
|
|
132
|
+
return FULL_SIZE;
|
|
131
133
|
case CENTER_CROP:
|
|
132
134
|
return DownsampleStrategy.CENTER_OUTSIDE;
|
|
133
135
|
case CENTER_INSIDE:
|
|
@@ -142,6 +144,26 @@ final class FastImageSourceSize {
|
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
// At the image's own size (objectFit none shows it at that size, whatever
|
|
148
|
+
// the view's), or scaled down to at most MAX_PIXELS pixels, which a
|
|
149
|
+
// bitmap can be drawn at (Android won't draw one over 100 MB).
|
|
150
|
+
static final DownsampleStrategy FULL_SIZE = new FullSize();
|
|
151
|
+
|
|
152
|
+
private static final float MAX_PIXELS = 4096f * 4096f;
|
|
153
|
+
|
|
154
|
+
private static final class FullSize extends DownsampleStrategy {
|
|
155
|
+
@Override
|
|
156
|
+
public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
|
|
157
|
+
float pixels = (float) sourceWidth * sourceHeight;
|
|
158
|
+
return pixels > MAX_PIXELS ? (float) Math.sqrt(MAX_PIXELS / pixels) : 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Override
|
|
162
|
+
public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
|
|
163
|
+
return SampleSizeRounding.QUALITY;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
145
167
|
// The image's size, or null when it has to be read from a local image's
|
|
146
168
|
// header first (see readLocal).
|
|
147
169
|
@Nullable
|
|
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
|
|
6
6
|
import androidx.annotation.Nullable;
|
|
7
7
|
|
|
8
8
|
import com.bumptech.glide.Registry;
|
|
9
|
+
import com.bumptech.glide.load.Option;
|
|
9
10
|
import com.bumptech.glide.load.Options;
|
|
10
11
|
import com.bumptech.glide.load.ResourceDecoder;
|
|
11
12
|
import com.bumptech.glide.load.engine.Resource;
|
|
@@ -17,6 +18,7 @@ import java.io.IOException;
|
|
|
17
18
|
import java.io.InputStream;
|
|
18
19
|
import java.nio.ByteBuffer;
|
|
19
20
|
import java.nio.charset.StandardCharsets;
|
|
21
|
+
import java.security.MessageDigest;
|
|
20
22
|
import java.util.Locale;
|
|
21
23
|
|
|
22
24
|
// SVG images (static), drawn with AndroidSVG, which FastImage includes
|
|
@@ -36,6 +38,31 @@ final class FastImageSvg {
|
|
|
36
38
|
@Nullable
|
|
37
39
|
private static Boolean available;
|
|
38
40
|
|
|
41
|
+
// objectFit scale-down and none show an image at its own size in dp
|
|
42
|
+
// (FastImageViewWithUrl.updateImageMatrix): an SVG is drawn at that size,
|
|
43
|
+
// the screen's pixels per dp (DENSITY) times its own, rather than at its
|
|
44
|
+
// size in pixels and then enlarged. With scale-down (FITS), no larger than
|
|
45
|
+
// the view. Part of the disk cache key, since the bitmap's size depends on
|
|
46
|
+
// them. Unset (0) for the other fits, which draw it for the view's size.
|
|
47
|
+
static final Option<Float> DENSITY = Option.disk(
|
|
48
|
+
"com.dylanvann.fastimage.FastImageSvg.Density", 0f,
|
|
49
|
+
new Option.CacheKeyUpdater<Float>() {
|
|
50
|
+
@Override
|
|
51
|
+
public void update(@NonNull byte[] keyBytes, @NonNull Float value, @NonNull MessageDigest messageDigest) {
|
|
52
|
+
messageDigest.update(keyBytes);
|
|
53
|
+
messageDigest.update(ByteBuffer.allocate(4).putFloat(value).array());
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
static final Option<Boolean> FITS = Option.disk(
|
|
57
|
+
"com.dylanvann.fastimage.FastImageSvg.Fits", false,
|
|
58
|
+
new Option.CacheKeyUpdater<Boolean>() {
|
|
59
|
+
@Override
|
|
60
|
+
public void update(@NonNull byte[] keyBytes, @NonNull Boolean value, @NonNull MessageDigest messageDigest) {
|
|
61
|
+
messageDigest.update(keyBytes);
|
|
62
|
+
messageDigest.update((byte) (value ? 1 : 0));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
39
66
|
private FastImageSvg() {
|
|
40
67
|
}
|
|
41
68
|
|
|
@@ -70,6 +70,16 @@ final class FastImageSvgRenderer {
|
|
|
70
70
|
float scale = strategy != null
|
|
71
71
|
? strategy.getScaleFactor(sourceWidth, sourceHeight, targetWidth, targetHeight)
|
|
72
72
|
: 1f;
|
|
73
|
+
// objectFit scale-down and none: at its own size in dp (see
|
|
74
|
+
// FastImageSvg.DENSITY), and for scale-down no larger than the view.
|
|
75
|
+
Float density = options.get(FastImageSvg.DENSITY);
|
|
76
|
+
if (density != null && density > 0) {
|
|
77
|
+
scale = density;
|
|
78
|
+
if (Boolean.TRUE.equals(options.get(FastImageSvg.FITS))) {
|
|
79
|
+
scale = Math.min(scale, Math.min(
|
|
80
|
+
targetWidth / (float) sourceWidth, targetHeight / (float) sourceHeight));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
73
83
|
if (Float.isNaN(scale) || Float.isInfinite(scale) || scale <= 0) scale = 1f;
|
|
74
84
|
float pixels = sourceWidth * scale * sourceHeight * scale;
|
|
75
85
|
if (pixels > MAX_PIXELS) scale *= (float) Math.sqrt(MAX_PIXELS / pixels);
|
|
@@ -49,6 +49,11 @@ class FastImageViewConverter {
|
|
|
49
49
|
put("cover", ScaleType.CENTER_CROP);
|
|
50
50
|
put("stretch", ScaleType.FIT_XY);
|
|
51
51
|
put("center", ScaleType.CENTER_INSIDE);
|
|
52
|
+
// objectFit="scale-down": as center, laid out in dp (see
|
|
53
|
+
// FastImageViewWithUrl.setResizeMode).
|
|
54
|
+
put("scale-down", ScaleType.CENTER_INSIDE);
|
|
55
|
+
// objectFit="none": the image at its own size, centered.
|
|
56
|
+
put("none", ScaleType.CENTER);
|
|
52
57
|
}};
|
|
53
58
|
|
|
54
59
|
// Whether the source has a uri to load (not null, missing or blank).
|
|
@@ -184,7 +184,7 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> {
|
|
|
184
184
|
boolean repeat = "repeat".equals(resizeMode);
|
|
185
185
|
final FastImageViewWithUrl.ScaleType scaleType =
|
|
186
186
|
repeat ? FastImageViewWithUrl.ScaleType.FIT_XY : FastImageViewConverter.getScaleType(resizeMode);
|
|
187
|
-
view.setResizeMode(scaleType, repeat);
|
|
187
|
+
view.setResizeMode(scaleType, repeat, "center".equals(resizeMode));
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
@Override
|
|
@@ -9,6 +9,7 @@ import android.annotation.SuppressLint;
|
|
|
9
9
|
import android.content.Context;
|
|
10
10
|
import android.graphics.Bitmap;
|
|
11
11
|
import android.graphics.Canvas;
|
|
12
|
+
import android.graphics.Matrix;
|
|
12
13
|
import android.graphics.Paint;
|
|
13
14
|
import android.graphics.PorterDuff;
|
|
14
15
|
import android.graphics.PorterDuffXfermode;
|
|
@@ -60,6 +61,9 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
60
61
|
public FastImageViewWithUrl(Context context, @Nullable RequestManager requestManager) {
|
|
61
62
|
super(context);
|
|
62
63
|
this.requestManager = requestManager;
|
|
64
|
+
// An image larger than the view (objectFit none) is cropped to it,
|
|
65
|
+
// also when the view doesn't clip its content (overflow visible).
|
|
66
|
+
setCropToPadding(true);
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
// FastImage sends "none" when it has pointerEvents="box-none" (the image
|
|
@@ -609,19 +613,134 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
609
613
|
// as React Native's Image does. The view fills with it (FIT_XY).
|
|
610
614
|
private boolean mRepeat = false;
|
|
611
615
|
|
|
616
|
+
// The scale type for resizeMode, which Glide decodes the image for and the
|
|
617
|
+
// blur scales with. For scale-down (CENTER_INSIDE) and none (CENTER) the
|
|
618
|
+
// view is MATRIX: it lays the image out itself (see updateImageMatrix).
|
|
619
|
+
// Starts as ImageView's.
|
|
620
|
+
private ScaleType mResizeScaleType = ScaleType.FIT_CENTER;
|
|
621
|
+
|
|
622
|
+
// resizeMode center: CENTER_INSIDE as ImageView draws it, an image smaller
|
|
623
|
+
// than the view at its size in pixels on the screen (scale-down: in dp).
|
|
624
|
+
private boolean mInPixels = false;
|
|
625
|
+
|
|
626
|
+
// The scale type the image on screen is laid out for (see
|
|
627
|
+
// applyResizeMode).
|
|
628
|
+
private ScaleType mLayoutScaleType = ScaleType.FIT_CENTER;
|
|
629
|
+
|
|
612
630
|
// Glide crops or fits the bitmap for the scale type when it loads, so a
|
|
613
631
|
// new resizeMode needs a reload to take effect (#762).
|
|
614
|
-
public void setResizeMode(ScaleType scaleType, boolean repeat) {
|
|
615
|
-
if (scaleType ==
|
|
616
|
-
|
|
632
|
+
public void setResizeMode(ScaleType scaleType, boolean repeat, boolean inPixels) {
|
|
633
|
+
if (scaleType == mResizeScaleType && repeat == mRepeat && inPixels == mInPixels) return;
|
|
634
|
+
mResizeScaleType = scaleType;
|
|
635
|
+
mInPixels = inPixels;
|
|
617
636
|
mRepeat = repeat;
|
|
618
637
|
mNeedsReload = true;
|
|
638
|
+
// A loaded image on screen was cropped or fitted for the old scale
|
|
639
|
+
// type: it keeps its layout until the image for the new one is ready
|
|
640
|
+
// (onImageLoaded), or the load fails. Laid out for the new one, a
|
|
641
|
+
// cover crop would be stretched to the image's own size.
|
|
642
|
+
if (mShownRequest == null) applyResizeMode();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Lays the view out for resizeMode.
|
|
646
|
+
private void applyResizeMode() {
|
|
647
|
+
mLayoutScaleType = mResizeScaleType;
|
|
648
|
+
setScaleType(!mInPixels && (mResizeScaleType == ScaleType.CENTER_INSIDE || mResizeScaleType == ScaleType.CENTER)
|
|
649
|
+
? ScaleType.MATRIX
|
|
650
|
+
: mResizeScaleType);
|
|
651
|
+
updateImageMatrix();
|
|
619
652
|
}
|
|
620
653
|
|
|
621
654
|
// Repeats what the view shows (the loaded image, and defaultSource).
|
|
622
655
|
@Override
|
|
623
656
|
public void setImageDrawable(@Nullable Drawable drawable) {
|
|
624
657
|
super.setImageDrawable(mRepeat ? tiled(drawable) : drawable);
|
|
658
|
+
updateImageMatrix();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
@Override
|
|
662
|
+
protected boolean setFrame(int l, int t, int r, int b) {
|
|
663
|
+
boolean changed = super.setFrame(l, t, r, b);
|
|
664
|
+
updateImageMatrix();
|
|
665
|
+
return changed;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// The loaded image's own size in pixels (as onLoad has it), and its scale
|
|
669
|
+
// (a bundled image's, so it's its size in points; 1 for others, as on
|
|
670
|
+
// iOS), for scale-down and none. 0 until it's known.
|
|
671
|
+
private int mImageWidth = 0;
|
|
672
|
+
private int mImageHeight = 0;
|
|
673
|
+
private double mImageScale = 1;
|
|
674
|
+
// The loading source's scale.
|
|
675
|
+
private double mLoadingScale = 1;
|
|
676
|
+
@Nullable
|
|
677
|
+
private Matrix mImageMatrix;
|
|
678
|
+
|
|
679
|
+
// The loading image's own size, from FastImageRequestListener as it's
|
|
680
|
+
// ready (null until a local image's has been read).
|
|
681
|
+
void onImageSize(@Nullable int[] size) {
|
|
682
|
+
mImageWidth = size != null ? size[0] : 0;
|
|
683
|
+
mImageHeight = size != null ? size[1] : 0;
|
|
684
|
+
mImageScale = mLoadingScale;
|
|
685
|
+
updateImageMatrix();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Pixels on screen per pixel of the loaded image at its own size: an image
|
|
689
|
+
// is its size in pixels in dp (a bundled one in points, its pixels divided
|
|
690
|
+
// by its scale), as on iOS.
|
|
691
|
+
private float imageDensity() {
|
|
692
|
+
return (float) (getResources().getDisplayMetrics().density / mImageScale);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// The same for the loading image.
|
|
696
|
+
private float loadingDensity() {
|
|
697
|
+
return (float) (getResources().getDisplayMetrics().density / mLoadingScale);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// scale-down: the image at its own size, centered, or scaled down
|
|
701
|
+
// uniformly to fit if that's larger than the view. Glide decodes it no
|
|
702
|
+
// larger than that (CenterInside), and it's drawn at this size. none: the
|
|
703
|
+
// image at its own size, centered, whatever its size (cropped by the view;
|
|
704
|
+
// Glide decodes it at its size). defaultSource (a resource) is already at
|
|
705
|
+
// its size on screen.
|
|
706
|
+
private void updateImageMatrix() {
|
|
707
|
+
if (getScaleType() != ScaleType.MATRIX) return;
|
|
708
|
+
Drawable drawable = getDrawable();
|
|
709
|
+
if (drawable == null) return;
|
|
710
|
+
int drawableWidth = drawable.getIntrinsicWidth();
|
|
711
|
+
int drawableHeight = drawable.getIntrinsicHeight();
|
|
712
|
+
float viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
|
|
713
|
+
float viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
|
|
714
|
+
if (mImageMatrix == null) mImageMatrix = new Matrix();
|
|
715
|
+
Matrix matrix = mImageMatrix;
|
|
716
|
+
if (drawableWidth <= 0 || drawableHeight <= 0 || viewWidth <= 0 || viewHeight <= 0) {
|
|
717
|
+
matrix.reset();
|
|
718
|
+
setImageMatrix(matrix);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
float width;
|
|
722
|
+
float height;
|
|
723
|
+
if (drawable == mDefaultSource) {
|
|
724
|
+
width = drawableWidth;
|
|
725
|
+
height = drawableHeight;
|
|
726
|
+
} else if (mImageWidth > 0 && mImageHeight > 0) {
|
|
727
|
+
width = mImageWidth * imageDensity();
|
|
728
|
+
height = mImageHeight * imageDensity();
|
|
729
|
+
} else {
|
|
730
|
+
// Its size isn't known yet (a local image's is read after it
|
|
731
|
+
// loads): Glide decoded it at its size unless it's larger than
|
|
732
|
+
// the view, which shows it scaled down to fit either way.
|
|
733
|
+
width = drawableWidth * imageDensity();
|
|
734
|
+
height = drawableHeight * imageDensity();
|
|
735
|
+
}
|
|
736
|
+
float fit = mLayoutScaleType == ScaleType.CENTER
|
|
737
|
+
? 1
|
|
738
|
+
: Math.min(1, Math.min(viewWidth / width, viewHeight / height));
|
|
739
|
+
width *= fit;
|
|
740
|
+
height *= fit;
|
|
741
|
+
matrix.setScale(width / drawableWidth, height / drawableHeight);
|
|
742
|
+
matrix.postTranslate(Math.round((viewWidth - width) / 2), Math.round((viewHeight - height) / 2));
|
|
743
|
+
setImageMatrix(matrix);
|
|
625
744
|
}
|
|
626
745
|
|
|
627
746
|
@Nullable
|
|
@@ -668,7 +787,9 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
668
787
|
// pixelated, and the blur. A blurred animated image shows its first frame,
|
|
669
788
|
// as on iOS.
|
|
670
789
|
private RequestOptions renderingOptions(Object model) {
|
|
671
|
-
FastImageBlur blur = mBlurRadius > 0
|
|
790
|
+
FastImageBlur blur = mBlurRadius > 0
|
|
791
|
+
? new FastImageBlur(mBlurRadius, mRepeat ? null : mResizeScaleType, mInPixels ? 1 : loadingDensity())
|
|
792
|
+
: null;
|
|
672
793
|
RequestOptions options;
|
|
673
794
|
if (mRepeat) {
|
|
674
795
|
// Decoded at its own size, or scaled down to fit the view if it's
|
|
@@ -690,7 +811,15 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
690
811
|
if (blur != null) options = options.optionalTransform(blur);
|
|
691
812
|
} else {
|
|
692
813
|
options = FastImageSourceSize.scaleTypeOptions(
|
|
693
|
-
|
|
814
|
+
mResizeScaleType, FastImageSourceSize.capture(mResizeScaleType, model), blur);
|
|
815
|
+
boolean ownSize = mResizeScaleType == ScaleType.CENTER_INSIDE || mResizeScaleType == ScaleType.CENTER;
|
|
816
|
+
// scale-down and none show it at its own size in dp: an SVG is
|
|
817
|
+
// drawn at that size, so it's sharp (not with a blur, which hides
|
|
818
|
+
// the difference).
|
|
819
|
+
if (ownSize && !mInPixels && blur == null) {
|
|
820
|
+
options = options.set(FastImageSvg.DENSITY, loadingDensity())
|
|
821
|
+
.set(FastImageSvg.FITS, mResizeScaleType == ScaleType.CENTER_INSIDE);
|
|
822
|
+
}
|
|
694
823
|
}
|
|
695
824
|
return blur != null ? options.dontAnimate() : options;
|
|
696
825
|
}
|
|
@@ -708,12 +837,13 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
708
837
|
// have).
|
|
709
838
|
private boolean mRestarting = false;
|
|
710
839
|
|
|
711
|
-
// The request for the image the view shows
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
//
|
|
840
|
+
// The request for the image the view shows (from when it has loaded until
|
|
841
|
+
// another image or defaultSource replaces it), and the one loading. A new
|
|
842
|
+
// source starts with the shown one as a thumbnail, from the cache only, so
|
|
843
|
+
// the image stays until the new one has loaded instead of flashing blank
|
|
844
|
+
// (#747), also if the source changes again meanwhile. This is Glide's safe
|
|
845
|
+
// way to do that: the previous bitmap can't be kept in the view itself,
|
|
846
|
+
// since Glide reuses it once its request is cleared.
|
|
717
847
|
@Nullable
|
|
718
848
|
private RequestBuilder<Drawable> mShownRequest;
|
|
719
849
|
@Nullable
|
|
@@ -751,6 +881,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
751
881
|
void onImageLoaded() {
|
|
752
882
|
mLoadEnded = true;
|
|
753
883
|
endProgress();
|
|
884
|
+
// Before the view shows it (the target does once this returns).
|
|
885
|
+
applyResizeMode();
|
|
754
886
|
mShownRequest = mLoadingRequest;
|
|
755
887
|
mShownWidth = mLoadingWidth;
|
|
756
888
|
mShownHeight = mLoadingHeight;
|
|
@@ -905,6 +1037,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
905
1037
|
void onImageFailed(boolean hadThumbnail) {
|
|
906
1038
|
mLoadEnded = true;
|
|
907
1039
|
endProgress();
|
|
1040
|
+
applyResizeMode();
|
|
908
1041
|
mShownRequest = null;
|
|
909
1042
|
if (!hadThumbnail) return;
|
|
910
1043
|
final int load = mLoadCount;
|
|
@@ -961,15 +1094,17 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
961
1094
|
mRestarting = false;
|
|
962
1095
|
mLoadCount++;
|
|
963
1096
|
// A fade in progress carries on, as on iOS: into the next image if it
|
|
964
|
-
// shows at once, and a next image that fades starts from it.
|
|
1097
|
+
// shows at once, and a next image that fades starts from it. The
|
|
1098
|
+
// image on screen stays the shown one while the next loads (as its
|
|
1099
|
+
// thumbnail, or a copy), so a change before then keeps it too.
|
|
965
1100
|
RequestBuilder<Drawable> shownRequest = mShownRequest;
|
|
966
|
-
mShownRequest = null;
|
|
967
1101
|
mLoadingRequest = null;
|
|
968
1102
|
mBaseRequest = null;
|
|
969
1103
|
mModel = null;
|
|
970
1104
|
|
|
971
1105
|
// Nothing to show.
|
|
972
1106
|
if (mSource == null && mDefaultSource == null) {
|
|
1107
|
+
mShownRequest = null;
|
|
973
1108
|
// Cancel existing requests.
|
|
974
1109
|
clearView(requestManager);
|
|
975
1110
|
|
|
@@ -980,6 +1115,9 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
980
1115
|
return;
|
|
981
1116
|
}
|
|
982
1117
|
|
|
1118
|
+
mLoadingScale = mSource != null && mSource.hasKey("__packager_asset")
|
|
1119
|
+
? Math.max(number(mSource, "scale", 1), 0.01)
|
|
1120
|
+
: 1;
|
|
983
1121
|
//final GlideUrl glideUrl = FastImageViewConverter.getGlideUrl(view.getContext(), mSource);
|
|
984
1122
|
final FastImageSource imageSource = FastImageViewConverter.hasUri(mSource)
|
|
985
1123
|
? FastImageViewConverter.getImageSource(getContext(), mSource)
|
|
@@ -995,6 +1133,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
995
1133
|
FastImageEvents.send(this, REACT_ON_ERROR_EVENT, event);
|
|
996
1134
|
FastImageEvents.sendLoadEnd(this, error);
|
|
997
1135
|
|
|
1136
|
+
mShownRequest = null;
|
|
998
1137
|
// Cancel existing requests.
|
|
999
1138
|
clearView(requestManager);
|
|
1000
1139
|
|
|
@@ -1053,7 +1192,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1053
1192
|
RequestBuilder<Drawable> builder = base.clone().apply(renderingOptions(model));
|
|
1054
1193
|
RequestBuilder<Drawable> request = builder.clone();
|
|
1055
1194
|
|
|
1056
|
-
|
|
1195
|
+
// Only defaultSource: it replaces the image.
|
|
1196
|
+
if (model == null) {
|
|
1197
|
+
meanwhile = null;
|
|
1198
|
+
mShownRequest = null;
|
|
1199
|
+
}
|
|
1057
1200
|
boolean thumbnail = shownRequest != null && model != null && meanwhile == null;
|
|
1058
1201
|
// After the clone: loading the image that's showing again (at a
|
|
1059
1202
|
// new size, or with another blur) doesn't fade it in again. Over a
|