react-native-fast-image 9.2.2 → 9.3.1

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.
Files changed (28) hide show
  1. package/README.md +36 -7
  2. package/android/src/main/java/com/dylanvann/fastimage/FastImageBlur.java +18 -5
  3. package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +5 -1
  4. package/android/src/main/java/com/dylanvann/fastimage/FastImageSharedDownloads.java +64 -19
  5. package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +22 -0
  6. package/android/src/main/java/com/dylanvann/fastimage/FastImageSvg.java +27 -0
  7. package/android/src/main/java/com/dylanvann/fastimage/FastImageSvgRenderer.java +10 -0
  8. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +5 -0
  9. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +1 -1
  10. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +157 -14
  11. package/dist/index.cjs.js +50 -18
  12. package/dist/index.cjs.js.flow +3 -0
  13. package/dist/index.cjs.web.js +241 -130
  14. package/dist/index.cjs.web.js.flow +3 -0
  15. package/dist/index.d.ts +36 -6
  16. package/dist/index.js +50 -18
  17. package/dist/index.js.flow +3 -0
  18. package/dist/index.web.js +243 -132
  19. package/dist/index.web.js.flow +3 -0
  20. package/dist/objectFit.d.ts +3 -0
  21. package/ios/FastImage/FFFDownsampledImage.h +8 -0
  22. package/ios/FastImage/FFFDownsampledImage.m +16 -0
  23. package/ios/FastImage/FFFastImageView.h +7 -1
  24. package/ios/FastImage/FFFastImageView.m +10 -5
  25. package/ios/FastImage/FFFastImageViewManager.m +3 -1
  26. package/ios/FastImage/RCTConvert+FFFastImage.h +3 -0
  27. package/ios/FastImage/RCTConvert+FFFastImage.m +11 -0
  28. package/package.json +1 -1
@@ -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
package/dist/index.cjs.js CHANGED
@@ -46,30 +46,61 @@ const cacheControl = {
46
46
  cacheOnly: "cacheOnly"
47
47
  };
48
48
  //#endregion
49
- //#region src/index.tsx
50
- const resolveDefaultSource = (defaultSource) => {
51
- if (!defaultSource) return null;
52
- if (react_native.Platform.OS === "android") {
53
- const resolved = react_native.Image.resolveAssetSource(defaultSource);
54
- if (resolved) return resolved.uri;
55
- return null;
56
- }
57
- return defaultSource;
58
- };
59
- function tintColorFromStyle(style) {
49
+ //#region src/objectFit.ts
50
+ function fromStyle(style, key) {
60
51
  if (Array.isArray(style)) {
61
52
  for (let i = style.length - 1; i >= 0; i--) {
62
- const found = tintColorFromStyle(style[i]);
53
+ const found = fromStyle(style[i], key);
63
54
  if (found !== void 0) return found;
64
55
  }
65
56
  return;
66
57
  }
67
58
  if (typeof style === "number") {
68
59
  const flattened = react_native.StyleSheet.flatten(style);
69
- return flattened ? flattened.tintColor : void 0;
60
+ return flattened ? flattened[key] : void 0;
70
61
  }
71
- return style && typeof style === "object" ? style.tintColor : void 0;
62
+ return style && typeof style === "object" ? style[key] : void 0;
63
+ }
64
+ const RESIZE_MODE_FIT = {
65
+ contain: "contain",
66
+ cover: "cover",
67
+ stretch: "fill",
68
+ center: "center",
69
+ repeat: "repeat"
70
+ };
71
+ const FITS = {
72
+ fill: true,
73
+ contain: true,
74
+ cover: true,
75
+ none: true,
76
+ "scale-down": true
77
+ };
78
+ function resolveObjectFit(objectFit, style, resizeMode) {
79
+ var _RESIZE_MODE_FIT$resi;
80
+ const fit = objectFit != null ? objectFit : fromStyle(style, "objectFit");
81
+ if (fit && FITS[fit] === true) return fit;
82
+ return (_RESIZE_MODE_FIT$resi = RESIZE_MODE_FIT[resizeMode]) !== null && _RESIZE_MODE_FIT$resi !== void 0 ? _RESIZE_MODE_FIT$resi : "cover";
72
83
  }
84
+ //#endregion
85
+ //#region src/index.tsx
86
+ const resolveDefaultSource = (defaultSource) => {
87
+ if (!defaultSource) return null;
88
+ if (react_native.Platform.OS === "android") {
89
+ const resolved = react_native.Image.resolveAssetSource(defaultSource);
90
+ if (resolved) return resolved.uri;
91
+ return null;
92
+ }
93
+ return defaultSource;
94
+ };
95
+ const NATIVE_RESIZE_MODE = {
96
+ fill: "stretch",
97
+ contain: "contain",
98
+ cover: "cover",
99
+ none: "none",
100
+ "scale-down": "scale-down",
101
+ center: "center",
102
+ repeat: "repeat"
103
+ };
73
104
  function loopCount(loop) {
74
105
  if (loop === void 0) return -1;
75
106
  if (loop === true) return 0;
@@ -111,7 +142,7 @@ function withoutCache(source) {
111
142
  const { cache: _cache, ...rest } = source || {};
112
143
  return rest;
113
144
  }
114
- function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", loop, transition, forwardedRef, onLayout, pointerEvents, hitSlop, onStartShouldSetResponder, onStartShouldSetResponderCapture, onMoveShouldSetResponder, onMoveShouldSetResponderCapture, onResponderGrant, onResponderReject, onResponderStart, onResponderMove, onResponderEnd, onResponderRelease, onResponderTerminationRequest, onResponderTerminate, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, onTouchEndCapture, ...viewProps }) {
145
+ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, objectFit, resizeMode = "cover", loop, transition, forwardedRef, onLayout, pointerEvents, hitSlop, onStartShouldSetResponder, onStartShouldSetResponderCapture, onMoveShouldSetResponder, onMoveShouldSetResponderCapture, onResponderGrant, onResponderReject, onResponderStart, onResponderMove, onResponderEnd, onResponderRelease, onResponderTerminationRequest, onResponderTerminate, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, onTouchEndCapture, ...viewProps }) {
115
146
  const { onClick, ...props } = viewProps;
116
147
  const fallbackResult = (0, react.useRef)(void 0);
117
148
  const wrapperProps = {
@@ -141,7 +172,8 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
141
172
  ...props,
142
173
  pointerEvents: pointerEvents === "box-none" ? "none" : void 0
143
174
  };
144
- const resolvedTintColor = tintColor != null ? tintColor : tintColorFromStyle(style);
175
+ const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
176
+ const mode = NATIVE_RESIZE_MODE[resolveObjectFit(objectFit, style, resizeMode)];
145
177
  if (fallback) {
146
178
  const cleanedSource = typeof source === "number" ? source : Array.isArray(source) ? source.map(withoutCache) : withoutCache(source);
147
179
  const resolvedSource = react_native.Image.resolveAssetSource(cleanedSource);
@@ -179,7 +211,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
179
211
  error: "Failed to load the image"
180
212
  });
181
213
  }),
182
- resizeMode
214
+ resizeMode: mode === "none" || mode === "scale-down" ? "center" : mode
183
215
  }), children);
184
216
  }
185
217
  const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
@@ -205,7 +237,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
205
237
  onFastImageLoad: onLoad,
206
238
  onFastImageError: onError,
207
239
  onFastImageLoadEnd: onLoadEnd && ((event) => onLoadEnd(loadResult(event.nativeEvent))),
208
- resizeMode
240
+ resizeMode: mode
209
241
  }), children);
210
242
  }
211
243
  const FastImageMemo = (0, react.memo)(FastImageBase);
@@ -49,6 +49,7 @@ export type CacheControl = $ReadOnly<{|
49
49
  |}>
50
50
 
51
51
  export type ResizeModes = $Values<ResizeMode>
52
+ export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
52
53
  export type Priorities = $Values<Priority>
53
54
  export type CacheControls = $Values<CacheControl>
54
55
 
@@ -123,6 +124,8 @@ export type FastImageProps = $ReadOnly<{|
123
124
  defaultSource?: ?number,
124
125
 
125
126
  tintColor?: number | string,
127
+ objectFit?: ?ObjectFit,
128
+ // Deprecated: use objectFit.
126
129
  resizeMode?: ?ResizeModes,
127
130
  fallback?: ?boolean,
128
131
  recyclingKey?: ?string,