react-native-fast-image 8.20.0 → 8.22.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 CHANGED
@@ -212,6 +212,27 @@ Pauses an animated image (GIF, and animated WebP on iOS) on the frame it's showi
212
212
 
213
213
  ---
214
214
 
215
+ ### `transition?: boolean | number | Transition`
216
+
217
+ Fades the image in when it loads. Off by default. `true` uses the platform's usual fade, a number is the duration in milliseconds, or pass an object:
218
+
219
+ - `duration`: milliseconds; 0 is no fade. Defaults to the platform's usual length: 300 ms on Android, 250 ms on iOS.
220
+ - `betweenImages` (default `false`): also fade between images. By default an image fades in when it appears over nothing (or over `defaultSource`), and a new `source` replaces the image that's showing at once, once it has loaded. With `true`, the new image cross-dissolves from the one showing (for a gallery or an avatar that changes, say).
221
+ - `skipOnCacheHit`: which images show at once, without the fade:
222
+ - `'memory'` (default): images from the memory cache, so a list scrolled back up shows images it already loaded at once. Images from the disk cache fade in.
223
+ - `'all'`: images from the memory or disk cache: images that download fade in, and local files and bundled images usually only the first time they load (the disk cache usually keeps them too).
224
+ - `'none'`: every image fades in.
225
+
226
+ Downloads, local files (`file://`, `content://`) and bundled images (`require()`) fade in. In lists that reuse views (e.g. FlashList), set `recyclingKey`, so a reused view starts empty and its image fades in, instead of showing the previous item's image until it loads.
227
+
228
+ ```jsx
229
+ <FastImage source={{ uri }} transition />
230
+ <FastImage source={{ uri }} transition={500} />
231
+ <FastImage source={{ uri }} transition={{ betweenImages: true, skipOnCacheHit: 'none' }} />
232
+ ```
233
+
234
+ ---
235
+
215
236
  ### `downsample?: boolean`
216
237
 
217
238
  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.
@@ -222,6 +243,22 @@ Decoding a smaller copy can take a little longer, so use it where the memory mat
222
243
 
223
244
  ---
224
245
 
246
+ ### `blurRadius?: number`
247
+
248
+ Blurs the image by this radius, in points, like React Native's `Image` (the same radius looks about the same on iOS and Android). `0` **(Default)** is no blur.
249
+
250
+ It's for still images, or a radius that changes now and then (e.g. blurring a photo behind a sheet). Each change blurs the image again on the CPU, so don't animate it.
251
+
252
+ - Only the loaded image is blurred, not `defaultSource`.
253
+ - An animated image (GIF, animated WebP) shows its first frame, blurred, and doesn't animate.
254
+ - With `tintColor`, the blurred image is tinted.
255
+ - The image is blurred at about the size it's shown at, off the main thread. The cached file stays the original image, so `getCachePath` and other views of it aren't affected.
256
+ - Changing it blurs the image that's showing again, without sending the load events again.
257
+
258
+ To animate a blur, or to blur an animated image, use React Native's `filter` style instead, which the GPU draws: `style={{ filter: [{ blur: 6 }] }}`. It needs the New Architecture. React Native's docs list `blur` for Android 12+ only; on iOS it's behind an experimental React Native feature flag (`enableSwiftUIBasedFilters`, SwiftUI-based filters).
259
+
260
+ ---
261
+
225
262
  ### `onLoadStart?: () => void`
226
263
 
227
264
  Called when the image starts to load.
@@ -0,0 +1,213 @@
1
+ package com.dylanvann.fastimage;
2
+
3
+ import android.graphics.Bitmap;
4
+ import android.graphics.Canvas;
5
+ import android.graphics.Paint;
6
+ import android.graphics.PorterDuff;
7
+ import android.graphics.PorterDuffXfermode;
8
+ import android.widget.ImageView;
9
+
10
+ import androidx.annotation.NonNull;
11
+
12
+ import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
13
+ import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
14
+ import com.bumptech.glide.request.target.Target;
15
+
16
+ import java.nio.IntBuffer;
17
+ import java.nio.charset.Charset;
18
+ import java.security.MessageDigest;
19
+
20
+ // Blurs the image (blurRadius), as React Native's Image does on iOS: three box
21
+ // blurs, close to a Gaussian blur, with the box size worked out from the radius
22
+ // (in pixels) as the SVG spec describes and halved, as React Native does. So a
23
+ // radius looks the same on both platforms. It runs after the scale type's crop
24
+ // or fit, on the image at about the size the view shows it (a blurred image
25
+ // needs no more detail), on Glide's threads. Part of the request's key, so
26
+ // blurred and unblurred views of one image don't share a cached image; the
27
+ // cached file stays the original.
28
+ final class FastImageBlur extends BitmapTransformation {
29
+ private static final String ID = "com.dylanvann.fastimage.FastImageBlur";
30
+ private static final byte[] ID_BYTES = ID.getBytes(Charset.forName("UTF-8"));
31
+
32
+ // In pixels on screen.
33
+ private final float radius;
34
+ private final ImageView.ScaleType scaleType;
35
+
36
+ FastImageBlur(float radius, ImageView.ScaleType scaleType) {
37
+ this.radius = radius;
38
+ this.scaleType = scaleType;
39
+ }
40
+
41
+ @Override
42
+ protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
43
+ int width = toTransform.getWidth();
44
+ int height = toTransform.getHeight();
45
+ if (outWidth == Target.SIZE_ORIGINAL) outWidth = width;
46
+ if (outHeight == Target.SIZE_ORIGINAL) outHeight = height;
47
+ // How many pixels on screen a pixel of the bitmap takes.
48
+ float shownScale = shownScale(width, height, outWidth, outHeight);
49
+ // Blurred at the size it's shown at, or at its own size if it's shown
50
+ // larger.
51
+ float scale = Math.min(shownScale, 1);
52
+ int blurredWidth = Math.max(1, Math.round(width * scale));
53
+ int blurredHeight = Math.max(1, Math.round(height * scale));
54
+ int boxSize = boxSize(radius * scale / shownScale, Math.max(blurredWidth, blurredHeight));
55
+ if (boxSize <= 1 && scale == 1) return toTransform;
56
+
57
+ Bitmap result = pool.get(blurredWidth, blurredHeight, Bitmap.Config.ARGB_8888);
58
+ result.setDensity(toTransform.getDensity());
59
+ Canvas canvas = new Canvas(result);
60
+ canvas.scale(blurredWidth / (float) width, blurredHeight / (float) height);
61
+ canvas.drawBitmap(toTransform, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));
62
+ canvas.setBitmap(null);
63
+ if (boxSize > 1) blur(pool, result, boxSize);
64
+ return result;
65
+ }
66
+
67
+ // Pixels on screen per pixel of the bitmap, for the view's scale type.
68
+ private float shownScale(int width, int height, int outWidth, int outHeight) {
69
+ if (width <= 0 || height <= 0 || outWidth <= 0 || outHeight <= 0) return 1;
70
+ float x = outWidth / (float) width;
71
+ float y = outHeight / (float) height;
72
+ if (scaleType == null) return 1;
73
+ switch (scaleType) {
74
+ case CENTER_CROP:
75
+ return Math.max(x, y);
76
+ case FIT_CENTER:
77
+ case FIT_START:
78
+ case FIT_END:
79
+ return Math.min(x, y);
80
+ case FIT_XY:
81
+ return (float) Math.sqrt(x * y);
82
+ case CENTER_INSIDE:
83
+ return Math.min(Math.min(x, y), 1);
84
+ default:
85
+ return 1;
86
+ }
87
+ }
88
+
89
+ // React Native's (RCTImageBlurUtils) for a radius in pixels: odd, so it's
90
+ // centered. At most the image's larger side (a larger box looks the same,
91
+ // and an absurd radius would overflow).
92
+ private static int boxSize(float radius, int max) {
93
+ double size = Math.floor((radius * 3 * Math.sqrt(2 * Math.PI) / 4 + 0.5) / 2);
94
+ return (int) Math.min(size, max) | 1;
95
+ }
96
+
97
+ // Blurs the bitmap, on its premultiplied pixels (as stored), each byte a
98
+ // channel. A large box blurs a copy 2, 4 or 8 times smaller instead (each
99
+ // pixel the average of a block), with a box that much smaller, of at least
100
+ // 11 pixels, drawn back at full size: away from the edges that matches
101
+ // blurring at full size within a few levels of 255 (about as much as a
102
+ // slightly different radius changes it), for a quarter to a sixty-fourth of
103
+ // the work.
104
+ private static void blur(BitmapPool pool, Bitmap bitmap, int boxSize) {
105
+ int width = bitmap.getWidth();
106
+ int height = bitmap.getHeight();
107
+ int[] pixels = new int[width * height];
108
+ IntBuffer buffer = IntBuffer.wrap(pixels);
109
+ bitmap.copyPixelsToBuffer(buffer);
110
+ int factor = boxSize >= 88 ? 8 : boxSize >= 44 ? 4 : boxSize >= 22 ? 2 : 1;
111
+ if (factor == 1) {
112
+ blur(pixels, width, height, boxSize);
113
+ buffer.rewind();
114
+ bitmap.copyPixelsFromBuffer(buffer);
115
+ return;
116
+ }
117
+ int smallWidth = (width + factor - 1) / factor;
118
+ int smallHeight = (height + factor - 1) / factor;
119
+ int[] small = shrink(pixels, width, height, factor, smallWidth, smallHeight);
120
+ blur(small, smallWidth, smallHeight, Math.round((boxSize / (float) factor - 1) / 2) * 2 + 1);
121
+ Bitmap copy = pool.get(smallWidth, smallHeight, Bitmap.Config.ARGB_8888);
122
+ copy.copyPixelsFromBuffer(IntBuffer.wrap(small));
123
+ Canvas canvas = new Canvas(bitmap);
124
+ canvas.scale(factor, factor);
125
+ Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
126
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
127
+ canvas.drawBitmap(copy, 0, 0, paint);
128
+ canvas.setBitmap(null);
129
+ pool.put(copy);
130
+ }
131
+
132
+ // Three box blurs, each across then down, with the edges extended.
133
+ private static void blur(int[] pixels, int width, int height, int boxSize) {
134
+ int[] other = new int[width * height];
135
+ int radius = boxSize / 2;
136
+ for (int i = 0; i < 3; i++) {
137
+ for (int y = 0; y < height; y++) boxBlur(pixels, other, y * width, 1, width, radius);
138
+ for (int x = 0; x < width; x++) boxBlur(other, pixels, x, width, height, radius);
139
+ }
140
+ }
141
+
142
+ // A copy factor times smaller: each pixel the average of a factor × factor
143
+ // block (smaller at the right and bottom edges).
144
+ private static int[] shrink(int[] pixels, int width, int height, int factor, int smallWidth, int smallHeight) {
145
+ int[] small = new int[smallWidth * smallHeight];
146
+ for (int sy = 0; sy < smallHeight; sy++) {
147
+ for (int sx = 0; sx < smallWidth; sx++) {
148
+ int a = 0, b = 0, c = 0, d = 0, n = 0;
149
+ for (int y = sy * factor; y < Math.min(height, (sy + 1) * factor); y++) {
150
+ for (int x = sx * factor; x < Math.min(width, (sx + 1) * factor); x++) {
151
+ int pixel = pixels[y * width + x];
152
+ a += pixel >>> 24;
153
+ b += (pixel >> 16) & 0xff;
154
+ c += (pixel >> 8) & 0xff;
155
+ d += pixel & 0xff;
156
+ n++;
157
+ }
158
+ }
159
+ int half = n / 2;
160
+ small[sy * smallWidth + sx] = ((a + half) / n) << 24 | ((b + half) / n) << 16
161
+ | ((c + half) / n) << 8 | ((d + half) / n);
162
+ }
163
+ }
164
+ return small;
165
+ }
166
+
167
+ // Averages each pixel of a row or column (count pixels from start, step
168
+ // apart) with the radius pixels on each side.
169
+ private static void boxBlur(int[] from, int[] to, int start, int step, int count, int radius) {
170
+ int size = 2 * radius + 1;
171
+ int half = size / 2;
172
+ int a = 0, b = 0, c = 0, d = 0;
173
+ for (int i = -radius; i <= radius; i++) {
174
+ int pixel = from[start + clamp(i, count) * step];
175
+ a += pixel >>> 24;
176
+ b += (pixel >> 16) & 0xff;
177
+ c += (pixel >> 8) & 0xff;
178
+ d += pixel & 0xff;
179
+ }
180
+ for (int i = 0; i < count; i++) {
181
+ to[start + i * step] = ((a + half) / size) << 24 | ((b + half) / size) << 16
182
+ | ((c + half) / size) << 8 | ((d + half) / size);
183
+ int added = from[start + clamp(i + radius + 1, count) * step];
184
+ int removed = from[start + clamp(i - radius, count) * step];
185
+ a += (added >>> 24) - (removed >>> 24);
186
+ b += ((added >> 16) & 0xff) - ((removed >> 16) & 0xff);
187
+ c += ((added >> 8) & 0xff) - ((removed >> 8) & 0xff);
188
+ d += (added & 0xff) - (removed & 0xff);
189
+ }
190
+ }
191
+
192
+ private static int clamp(int i, int count) {
193
+ return i < 0 ? 0 : i >= count ? count - 1 : i;
194
+ }
195
+
196
+ @Override
197
+ public boolean equals(Object o) {
198
+ return o instanceof FastImageBlur
199
+ && ((FastImageBlur) o).radius == radius
200
+ && ((FastImageBlur) o).scaleType == scaleType;
201
+ }
202
+
203
+ @Override
204
+ public int hashCode() {
205
+ return ID.hashCode() * 31 + Float.floatToIntBits(radius) * 31 + (scaleType == null ? 0 : scaleType.ordinal());
206
+ }
207
+
208
+ @Override
209
+ public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
210
+ messageDigest.update(ID_BYTES);
211
+ messageDigest.update((radius + "," + scaleType).getBytes(Charset.forName("UTF-8")));
212
+ }
213
+ }
@@ -34,7 +34,12 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
34
34
 
35
35
  @Override
36
36
  public boolean onLoadFailed(@androidx.annotation.Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
37
- if (!events) return false;
37
+ if (!events) {
38
+ if (target instanceof ImageViewTarget) {
39
+ ((FastImageViewWithUrl) ((ImageViewTarget) target).getView()).onQuietLoadFailed();
40
+ }
41
+ return false;
42
+ }
38
43
  FastImageOkHttpProgressGlideModule.forget(key);
39
44
  if (!(target instanceof ImageViewTarget)) {
40
45
  return false;
@@ -14,7 +14,12 @@ import androidx.annotation.Nullable;
14
14
 
15
15
  import com.bumptech.glide.Glide;
16
16
  import com.bumptech.glide.load.ImageHeaderParserUtils;
17
+ import com.bumptech.glide.load.MultiTransformation;
18
+ import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
19
+ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
20
+ import com.bumptech.glide.load.resource.bitmap.CenterInside;
17
21
  import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
22
+ import com.bumptech.glide.load.resource.bitmap.FitCenter;
18
23
  import com.bumptech.glide.load.resource.gif.GifDrawable;
19
24
  import com.bumptech.glide.request.RequestOptions;
20
25
 
@@ -82,27 +87,37 @@ final class FastImageSourceSize {
82
87
 
83
88
  // What RequestBuilder.into(ImageView) applies for the view's scale type
84
89
  // (it leaves a transformation that's already set alone), with the
85
- // downsample strategy it implies wrapped by capture.
86
- static RequestOptions scaleTypeOptions(@Nullable ImageView.ScaleType scaleType, Capture capture) {
90
+ // downsample strategy it implies wrapped by capture, and then blur if it
91
+ // isn't null.
92
+ static RequestOptions scaleTypeOptions(@Nullable ImageView.ScaleType scaleType, Capture capture, @Nullable FastImageBlur blur) {
87
93
  RequestOptions options = new RequestOptions();
94
+ BitmapTransformation transformation = null;
88
95
  if (scaleType != null) {
89
96
  switch (scaleType) {
90
97
  case CENTER_CROP:
91
98
  options = options.optionalCenterCrop();
99
+ transformation = new CenterCrop();
92
100
  break;
93
101
  case CENTER_INSIDE:
94
102
  case FIT_XY:
95
103
  options = options.optionalCenterInside();
104
+ transformation = new CenterInside();
96
105
  break;
97
106
  case FIT_CENTER:
98
107
  case FIT_START:
99
108
  case FIT_END:
100
109
  options = options.optionalFitCenter();
110
+ transformation = new FitCenter();
101
111
  break;
102
112
  default:
103
113
  break;
104
114
  }
105
115
  }
116
+ if (blur != null) {
117
+ options = transformation == null
118
+ ? options.optionalTransform(blur)
119
+ : options.optionalTransform(new MultiTransformation<>(transformation, blur));
120
+ }
106
121
  return options.downsample(capture);
107
122
  }
108
123
 
@@ -22,6 +22,7 @@ import com.facebook.react.uimanager.LayoutShadowNode;
22
22
  import com.facebook.react.uimanager.SimpleViewManager;
23
23
  import com.facebook.react.bridge.ReactContext;
24
24
  import com.facebook.react.uimanager.ThemedReactContext;
25
+ import com.facebook.react.uimanager.PixelUtil;
25
26
  import com.facebook.react.uimanager.annotations.ReactProp;
26
27
  import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
27
28
 
@@ -99,6 +100,26 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
99
100
  view.setImageRendering(imageRendering);
100
101
  }
101
102
 
103
+ @ReactProp(name = "blurRadius")
104
+ public void setBlurRadius(FastImageViewWithUrl view, float blurRadius) {
105
+ view.setBlurRadius(PixelUtil.toPixelFromDIP(blurRadius));
106
+ }
107
+
108
+ @ReactProp(name = "transitionDuration")
109
+ public void setTransitionDuration(FastImageViewWithUrl view, int transitionDuration) {
110
+ view.setTransitionDuration(transitionDuration);
111
+ }
112
+
113
+ @ReactProp(name = "transitionBetweenImages")
114
+ public void setTransitionBetweenImages(FastImageViewWithUrl view, boolean betweenImages) {
115
+ view.setTransitionBetweenImages(betweenImages);
116
+ }
117
+
118
+ @ReactProp(name = "transitionSkipOnCacheHit")
119
+ public void setTransitionSkipOnCacheHit(FastImageViewWithUrl view, @Nullable String skipOnCacheHit) {
120
+ view.setTransitionSkipOnCacheHit(skipOnCacheHit);
121
+ }
122
+
102
123
  @ReactProp(name = "paused")
103
124
  public void setPaused(FastImageViewWithUrl view, boolean paused) {
104
125
  view.setPaused(paused);