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 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
- resizeMode="contain"
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'`: scales it uniformly (keeping its aspect ratio) so all of it fits in the view (minus padding).
243
- - `'cover'`: scales it uniformly (keeping its aspect ratio) so it covers the view (minus padding), cropping what doesn't fit.
244
- - `'stretch'`: scales its width and height separately to fill the view, which can change its aspect ratio.
245
- - `'center'`: centers it at its own size, scaled down uniformly to fit if it's larger than the view.
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, and loads it from the disk cache into memory, so a view shows it at once.
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 also kept decoded in memory, so they show at once.
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
- - 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.
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), 1);
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
- messageDigest.update((radius + "," + scaleType).getBytes(Charset.forName("UTF-8")));
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() == request) sendLoad(view, size);
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 == getScaleType() && repeat == mRepeat) return;
616
- setScaleType(scaleType);
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 ? new FastImageBlur(mBlurRadius, mRepeat ? null : getScaleType()) : null;
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
- getScaleType(), FastImageSourceSize.capture(getScaleType(), model), blur);
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 once it has loaded, and the one
712
- // loading. A new source starts with the shown one as a thumbnail, from the
713
- // cache only, so the image stays until the new one has loaded instead of
714
- // flashing blank (#747). This is Glide's safe way to do that: the previous
715
- // bitmap can't be kept in the view itself, since Glide reuses it once its
716
- // request is cleared.
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
- if (model == null) meanwhile = null;
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