react-native-fast-image 9.3.7 → 10.0.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.
Files changed (76) hide show
  1. package/README.md +87 -61
  2. package/RNFastImage.podspec +14 -9
  3. package/android/build.gradle +21 -47
  4. package/android/consumer-rules.pro +7 -7
  5. package/android/src/main/AndroidManifest.xml +1 -2
  6. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageAnimatable.java +1 -1
  7. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageAnimated.java +2 -2
  8. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageApng.java +2 -2
  9. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageBlur.java +4 -6
  10. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageCacheControl.java +1 -1
  11. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageCacheLimits.java +4 -1
  12. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageCacheWrite.java +1 -1
  13. package/android/src/main/java/com/fastimage/FastImageCrop.java +92 -0
  14. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageEvent.java +2 -3
  15. package/android/src/main/java/com/fastimage/FastImageEvents.java +74 -0
  16. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageGif.java +1 -1
  17. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageGlide.java +3 -3
  18. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageGlideModule.java +1 -1
  19. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageKeyedGlideUrl.java +1 -1
  20. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageOkHttpProgressGlideModule.java +36 -9
  21. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageRequestListener.java +6 -1
  22. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSharedDownloads.java +1 -1
  23. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSource.java +1 -1
  24. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSourceSize.java +12 -2
  25. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSvg.java +3 -3
  26. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSvgRenderer.java +1 -1
  27. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageUrl.java +1 -1
  28. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageViewConverter.java +7 -8
  29. package/android/src/main/java/com/fastimage/FastImageViewManager.java +591 -0
  30. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageViewModule.java +31 -28
  31. package/android/src/main/java/com/fastimage/FastImageViewPackage.java +51 -0
  32. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageViewWithUrl.java +352 -126
  33. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageWebGlideUrl.java +1 -1
  34. package/app.plugin.js +2 -7
  35. package/dist/index.cjs.js +276 -157
  36. package/dist/index.cjs.js.flow +33 -17
  37. package/dist/index.cjs.web.js +211 -61
  38. package/dist/index.cjs.web.js.flow +33 -17
  39. package/dist/index.d.ts +83 -58
  40. package/dist/index.js +278 -159
  41. package/dist/index.js.flow +33 -17
  42. package/dist/index.web.js +212 -62
  43. package/dist/index.web.js.flow +33 -17
  44. package/dist/objectFit.d.ts +2 -2
  45. package/dist/objectPosition.d.ts +42 -0
  46. package/dist/specs/FastImageViewNativeComponent.d.ts +48 -0
  47. package/dist/specs/NativeFastImageModule.d.ts +11 -0
  48. package/dist/warnings.d.ts +3 -0
  49. package/ios/FastImage/FFFDownsampledImage.h +4 -8
  50. package/ios/FastImage/FFFDownsampledImage.m +0 -34
  51. package/ios/FastImage/FFFastImageModule.h +8 -0
  52. package/ios/FastImage/{FFFastImageViewManager.m → FFFastImageModule.mm} +70 -59
  53. package/ios/FastImage/FFFastImageView.h +36 -7
  54. package/ios/FastImage/FFFastImageView.m +201 -164
  55. package/ios/FastImage/FFFastImageViewComponentView.h +11 -0
  56. package/ios/FastImage/FFFastImageViewComponentView.mm +325 -0
  57. package/ios/FastImage/RCTConvert+FFFastImage.h +1 -1
  58. package/ios/FastImage/RCTConvert+FFFastImage.m +7 -10
  59. package/package.json +28 -9
  60. package/src/constants.ts +28 -0
  61. package/src/index.js.flow +179 -0
  62. package/src/index.tsx +1103 -0
  63. package/src/index.web.tsx +548 -0
  64. package/src/objectFit.ts +48 -0
  65. package/src/objectPosition.ts +243 -0
  66. package/src/specs/FastImageViewNativeComponent.ts +83 -0
  67. package/src/specs/NativeFastImageModule.ts +35 -0
  68. package/src/warnings.ts +37 -0
  69. package/android/src/main/AndroidManifestNew.xml +0 -3
  70. package/android/src/main/java/com/dylanvann/fastimage/FastImageCookieHandler.java +0 -63
  71. package/android/src/main/java/com/dylanvann/fastimage/FastImageEvents.java +0 -49
  72. package/android/src/main/java/com/dylanvann/fastimage/FastImageProgressListener.java +0 -18
  73. package/android/src/main/java/com/dylanvann/fastimage/FastImageShadowNode.java +0 -21
  74. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +0 -379
  75. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java +0 -27
  76. package/ios/FastImage/FFFastImageViewManager.h +0 -5
@@ -1,6 +1,6 @@
1
- package com.dylanvann.fastimage;
1
+ package com.fastimage;
2
2
 
3
- import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT;
3
+ import static com.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT;
4
4
 
5
5
  import android.animation.Animator;
6
6
  import android.animation.AnimatorListenerAdapter;
@@ -14,8 +14,8 @@ import android.graphics.Paint;
14
14
  import android.graphics.PixelFormat;
15
15
  import android.graphics.PorterDuff;
16
16
  import android.graphics.PorterDuffXfermode;
17
+ import android.graphics.Rect;
17
18
  import android.graphics.RectF;
18
- import android.graphics.Shader;
19
19
  import android.graphics.drawable.AnimatedImageDrawable;
20
20
  import android.graphics.drawable.BitmapDrawable;
21
21
  import android.graphics.drawable.Drawable;
@@ -28,7 +28,6 @@ import androidx.annotation.NonNull;
28
28
  import androidx.annotation.Nullable;
29
29
  import androidx.annotation.RequiresApi;
30
30
  import androidx.appcompat.widget.AppCompatImageView;
31
- import androidx.core.view.ViewCompat;
32
31
 
33
32
  import com.bumptech.glide.GenericTransitionOptions;
34
33
  import com.bumptech.glide.RequestBuilder;
@@ -49,12 +48,17 @@ import com.facebook.react.bridge.ReadableArray;
49
48
  import com.facebook.react.bridge.ReadableMap;
50
49
  import com.facebook.react.bridge.WritableMap;
51
50
  import com.facebook.react.bridge.WritableNativeMap;
51
+ import com.facebook.react.modules.i18nmanager.I18nUtil;
52
+ import com.facebook.react.touch.ReactHitSlopView;
53
+ import com.facebook.react.uimanager.BackgroundStyleApplicator;
54
+ import com.facebook.react.uimanager.PixelUtil;
52
55
  import com.facebook.react.uimanager.PointerEvents;
53
56
  import com.facebook.react.uimanager.ReactPointerEventsView;
57
+ import com.facebook.react.uimanager.style.LogicalEdge;
54
58
 
55
59
  import javax.annotation.Nonnull;
56
60
 
57
- class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEventsView {
61
+ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEventsView, ReactHitSlopView {
58
62
  private boolean mNeedsReload = false;
59
63
  private ReadableMap mSource = null;
60
64
  private Drawable mDefaultSource = null;
@@ -66,16 +70,93 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
66
70
  public FastImageViewWithUrl(Context context, @Nullable RequestManager requestManager) {
67
71
  super(context);
68
72
  this.requestManager = requestManager;
69
- // An image larger than the view (objectFit none) is cropped to it,
70
- // also when the view doesn't clip its content (overflow visible).
73
+ // Borders inset the image: its padding is their widths (see
74
+ // updateBorderPadding). An image larger than that (objectFit none) is
75
+ // cropped to it, also when the view doesn't clip its content
76
+ // (overflow visible).
71
77
  setCropToPadding(true);
72
78
  }
73
79
 
74
- // FastImage sends "none" when it has pointerEvents="box-none" (the image
75
- // is part of the box, which doesn't take touches). React Native's touch
76
- // handling only reads pointerEvents from a ReactPointerEventsView.
77
- private PointerEvents mPointerEvents = PointerEvents.AUTO;
80
+ // The image fills the view inside its borders, and padding doesn't inset
81
+ // it, as on iOS: the view's padding is the border widths, resolved as
82
+ // React Native draws the border (its BorderInsets, which isn't public).
83
+ // React Native passes the layout's padding and borders together, so
84
+ // FastImageViewManager.setPadding doesn't use them. Called when a border
85
+ // width or the layout direction changes.
86
+ void updateBorderPadding() {
87
+ LogicalEdge left = LogicalEdge.START;
88
+ LogicalEdge physicalLeft = LogicalEdge.LEFT;
89
+ LogicalEdge right = LogicalEdge.END;
90
+ LogicalEdge physicalRight = LogicalEdge.RIGHT;
91
+ if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
92
+ left = LogicalEdge.END;
93
+ right = LogicalEdge.START;
94
+ if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(getContext())) {
95
+ physicalLeft = LogicalEdge.RIGHT;
96
+ physicalRight = LogicalEdge.LEFT;
97
+ }
98
+ }
99
+ int paddingLeft = borderWidth(left, physicalLeft, LogicalEdge.HORIZONTAL);
100
+ int paddingTop = borderWidth(LogicalEdge.BLOCK_START, LogicalEdge.TOP, LogicalEdge.BLOCK, LogicalEdge.VERTICAL);
101
+ int paddingRight = borderWidth(right, physicalRight, LogicalEdge.HORIZONTAL);
102
+ int paddingBottom = borderWidth(LogicalEdge.BLOCK_END, LogicalEdge.BOTTOM, LogicalEdge.BLOCK, LogicalEdge.VERTICAL);
103
+ // Compared with the padding last set here: getPaddingLeft() and the
104
+ // others resolve the padding, which calls onRtlPropertiesChanged.
105
+ if (paddingLeft == mBorderPaddingLeft && paddingTop == mBorderPaddingTop
106
+ && paddingRight == mBorderPaddingRight && paddingBottom == mBorderPaddingBottom) {
107
+ return;
108
+ }
109
+ mBorderPaddingLeft = paddingLeft;
110
+ mBorderPaddingTop = paddingTop;
111
+ mBorderPaddingRight = paddingRight;
112
+ mBorderPaddingBottom = paddingBottom;
113
+ setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
114
+ // The image was loaded for the box inside the borders: for the new
115
+ // one, switch to the source that fits it (several sources) or load it
116
+ // again, as when the view's size changes (the view's own size
117
+ // doesn't).
118
+ if (!switchSourceIfResized()) reloadIfResized();
119
+ }
120
+
121
+ // The padding updateBorderPadding last set. No initial values: it runs
122
+ // during View's constructor, before field initializers would.
123
+ private int mBorderPaddingLeft;
124
+ private int mBorderPaddingTop;
125
+ private int mBorderPaddingRight;
126
+ private int mBorderPaddingBottom;
127
+
128
+ // The first of these edges' border width that's set, else the one for
129
+ // every edge, in pixels (rounded down, as React Native's layout does).
130
+ private int borderWidth(LogicalEdge... edges) {
131
+ Float width = null;
132
+ for (LogicalEdge edge : edges) {
133
+ width = BackgroundStyleApplicator.getBorderWidth(this, edge);
134
+ if (width != null) break;
135
+ }
136
+ if (width == null) width = BackgroundStyleApplicator.getBorderWidth(this, LogicalEdge.ALL);
137
+ return width == null ? 0 : (int) Math.floor(PixelUtil.toPixelFromDIP(width));
138
+ }
78
139
 
140
+ @Override
141
+ public void onRtlPropertiesChanged(int layoutDirection) {
142
+ super.onRtlPropertiesChanged(layoutDirection);
143
+ updateBorderPadding();
144
+ }
145
+
146
+ // The View style props FastImage handles itself (FastImageViewManager):
147
+ // it's a single native view, so they're the image's.
148
+ private PointerEvents mPointerEvents = PointerEvents.AUTO;
149
+ @Nullable
150
+ private Rect mHitSlopRect;
151
+ // overflow: hidden (FastImage's default) or scroll clip the image to the
152
+ // padding box, with the border radius; visible only to the view.
153
+ private boolean mOverflowVisible = false;
154
+ // backfaceVisibility, as React Native's View does it: a view turned away
155
+ // (rotateX or rotateY past 90°) is hidden with an opacity of 0.
156
+ private boolean mBackfaceVisible = true;
157
+ private float mBackfaceOpacity = 1f;
158
+
159
+ @NonNull
79
160
  @Override
80
161
  public PointerEvents getPointerEvents() {
81
162
  return mPointerEvents;
@@ -85,6 +166,50 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
85
166
  mPointerEvents = pointerEvents;
86
167
  }
87
168
 
169
+ @Nullable
170
+ @Override
171
+ public Rect getHitSlopRect() {
172
+ return mHitSlopRect;
173
+ }
174
+
175
+ void setHitSlopRect(@Nullable Rect hitSlopRect) {
176
+ mHitSlopRect = hitSlopRect;
177
+ }
178
+
179
+ void setOverflow(@Nullable String overflow) {
180
+ mOverflowVisible = "visible".equals(overflow);
181
+ invalidate();
182
+ }
183
+
184
+ void setOpacityIfPossible(float opacity) {
185
+ mBackfaceOpacity = opacity;
186
+ setBackfaceVisibilityDependantOpacity();
187
+ }
188
+
189
+ void setBackfaceVisibility(String backfaceVisibility) {
190
+ mBackfaceVisible = "visible".equals(backfaceVisibility);
191
+ setBackfaceVisibilityDependantOpacity();
192
+ }
193
+
194
+ void setBackfaceVisibilityDependantOpacity() {
195
+ if (mBackfaceVisible) {
196
+ setAlpha(mBackfaceOpacity);
197
+ return;
198
+ }
199
+ float rotationX = getRotationX();
200
+ float rotationY = getRotationY();
201
+ boolean frontFaceVisible =
202
+ rotationX >= -90f && rotationX < 90f && rotationY >= -90f && rotationY < 90f;
203
+ setAlpha(frontFaceVisible ? mBackfaceOpacity : 0f);
204
+ }
205
+
206
+ // As React Native's Image: opacity applies to the view and its border and
207
+ // background as one, without an offscreen layer.
208
+ @Override
209
+ public boolean hasOverlappingRendering() {
210
+ return false;
211
+ }
212
+
88
213
  public void setSource(@Nullable ReadableMap source) {
89
214
  mNeedsReload = true;
90
215
  mPropSource = source;
@@ -119,13 +244,24 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
119
244
  if (mSources == null) mSource = mPropSource;
120
245
  }
121
246
 
247
+ // The box the image is drawn in: the view inside its padding, which is the
248
+ // border widths (see updateBorderPadding).
249
+ private int contentWidth() {
250
+ return getWidth() - getPaddingLeft() - getPaddingRight();
251
+ }
252
+
253
+ private int contentHeight() {
254
+ return getHeight() - getPaddingTop() - getPaddingBottom();
255
+ }
256
+
122
257
  // Of several sources, the index of the one whose size in pixels (width ×
123
- // height × scale²) is closest to the view's (by pixel count), or of the
124
- // largest while the view has no size (e.g. one sized from onLoad).
258
+ // height × scale²) is closest to the box the image is drawn in (by pixel
259
+ // count), or of the largest while it has no size (e.g. a view sized from
260
+ // onLoad).
125
261
  private int sourceIndexForSize() {
126
262
  ReadableArray sources = mSources;
127
263
  if (sources == null) return -1;
128
- double viewPixels = (double) getWidth() * getHeight();
264
+ double viewPixels = (double) Math.max(0, contentWidth()) * Math.max(0, contentHeight());
129
265
  int best = -1;
130
266
  double bestFit = Double.MAX_VALUE;
131
267
  for (int i = 0; i < sources.size(); i++) {
@@ -152,10 +288,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
152
288
  return map.hasKey(key) && !map.isNull(key) ? map.getDouble(key) : fallback;
153
289
  }
154
290
 
155
- // Several sources: another one fits the view's new size better. Loads it
156
- // (after layout), keeping the image showing until then, without a fade.
291
+ // Several sources: another one fits the view's new size (or the box inside
292
+ // its new borders) better. Loads it (after layout), keeping the image
293
+ // showing until then, without a fade.
157
294
  private boolean switchSourceIfResized() {
158
- if (mSources == null || mNeedsReload || mWaitsForSize || getWidth() <= 0 || getHeight() <= 0
295
+ if (mSources == null || mNeedsReload || mWaitsForSize || contentWidth() <= 0 || contentHeight() <= 0
159
296
  || mManager == null || sourceIndexForSize() == mSourceIndex) {
160
297
  return false;
161
298
  }
@@ -175,12 +312,12 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
175
312
  mDefaultSource = source;
176
313
  }
177
314
 
178
- // Legacy architecture only (see FastImageShadowNode): the view's layout is
179
- // 0×0 at its parent's origin, which React Native never applies. Lay it out
180
- // at that size, as the New Architecture does, so Glide stops waiting for a
181
- // size and loads it (#865).
182
- void onZeroLayout() {
183
- if (!ViewCompat.isLaidOut(this)) layout(0, 0, 0, 0);
315
+ // The events JS has a handler for (the handledEvents prop): the view only
316
+ // sends those (see FastImageEvents).
317
+ int handledEvents;
318
+
319
+ boolean handles(int events) {
320
+ return (handledEvents & events) != 0;
184
321
  }
185
322
 
186
323
  // How many times GIFs play: -1 for the file's own loop count (the `loop`
@@ -215,13 +352,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
215
352
 
216
353
  // Pauses GIFs on the frame they're showing (the view's own animation).
217
354
  private boolean mPaused = false;
218
- // Progress (see FastImageViewManager.onDownloadProgress): whether the image
219
- // has an onProgress, the url its current load downloads (null for a local
220
- // image, and once the load has ended), the progress key the view gets
221
- // progress for (the loading url's, while it has an onProgress), the last
222
- // step downloads reported before the load started, and the fraction the
223
- // load has had.
224
- private boolean mTrackProgress = false;
355
+ // Progress (see FastImageViewManager.onDownloadProgress): the url its
356
+ // current load downloads (null for a local image, and once the load has
357
+ // ended), the progress key the view gets progress for (the loading url's,
358
+ // while it has an onProgress), the last step downloads reported before the
359
+ // load started, and the fraction the load has had.
225
360
  @Nullable
226
361
  private GlideUrl mLoadingUrl;
227
362
  @Nullable
@@ -232,8 +367,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
232
367
  // posted to itself don't load anything, or track progress, any more.
233
368
  private boolean mDropped = false;
234
369
 
235
- void setTrackProgress(boolean trackProgress) {
236
- mTrackProgress = trackProgress;
370
+ void setHandledEvents(int events) {
371
+ handledEvents = events;
237
372
  updateProgressTracking();
238
373
  }
239
374
 
@@ -253,7 +388,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
253
388
 
254
389
  // Whether a step a download reported is for the current load, and doesn't
255
390
  // go back: one reported before it started is for an earlier load, e.g. of
256
- // the same url before resizeMode changed, posted while that load still
391
+ // the same url before objectFit changed, posted while that load still
257
392
  // ran, and one below what the load has had is from another download of
258
393
  // the key (e.g. one that started after the first had ended).
259
394
  boolean takesProgress(long step, long loaded, long total) {
@@ -265,7 +400,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
265
400
  }
266
401
 
267
402
  private void updateProgressTracking() {
268
- String key = mTrackProgress && mLoadingUrl != null && !mDropped
403
+ String key = handles(FastImageEvents.PROGRESS) && mLoadingUrl != null && !mDropped
269
404
  ? FastImageSharedDownloads.key(mLoadingUrl)
270
405
  : null;
271
406
  if (key == null ? mTrackedKey == null : key.equals(mTrackedKey)) return;
@@ -462,9 +597,26 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
462
597
  canvas.restoreToCount(count);
463
598
  }
464
599
 
465
- @SuppressWarnings("deprecation")
466
600
  @Override
467
601
  protected void onDraw(Canvas canvas) {
602
+ // Clipped as React Native's Image (and the View it used to be in): to
603
+ // the padding box, with the border radius, unless overflow is visible.
604
+ int saved = canvas.save();
605
+ if (mOverflowVisible) {
606
+ Rect bounds = new Rect();
607
+ getDrawingRect(bounds);
608
+ canvas.clipRect(bounds);
609
+ } else {
610
+ BackgroundStyleApplicator.clipToPaddingBox(this, canvas);
611
+ }
612
+ try {
613
+ drawImage(canvas);
614
+ } finally {
615
+ canvas.restoreToCount(saved);
616
+ }
617
+ }
618
+
619
+ private void drawImage(Canvas canvas) {
468
620
  if (mFade == null) {
469
621
  super.onDraw(canvas);
470
622
  return;
@@ -475,15 +627,14 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
475
627
  if (drawFadeWithoutLayers(canvas, alpha)) return;
476
628
  if (mFadeFrom == null && mFadeFromDrawable == null) {
477
629
  // Fading in over nothing: one layer.
478
- int count = canvas.saveLayerAlpha(0, 0, width, height, alpha, Canvas.ALL_SAVE_FLAG);
630
+ int count = canvas.saveLayerAlpha(0, 0, width, height, alpha);
479
631
  super.onDraw(canvas);
480
632
  canvas.restoreToCount(count);
481
633
  return;
482
634
  }
483
635
  // Two layers, which add up to the cross-dissolve whatever the image's
484
636
  // transparency.
485
- // Canvas.ALL_SAVE_FLAG: the saveLayer without flags is API 21+.
486
- int count = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
637
+ int count = canvas.saveLayer(0, 0, width, height, null);
487
638
  if (mFadeFromDrawable != null) {
488
639
  drawFadeFrom(canvas, mFadeFromDrawable, 255 - alpha);
489
640
  } else {
@@ -491,7 +642,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
491
642
  canvas.drawBitmap(mFadeFrom, 0, 0, mFadeFromPaint);
492
643
  }
493
644
  mFadeInPaint.setAlpha(alpha);
494
- canvas.saveLayer(0, 0, width, height, mFadeInPaint, Canvas.ALL_SAVE_FLAG);
645
+ canvas.saveLayer(0, 0, width, height, mFadeInPaint);
495
646
  super.onDraw(canvas);
496
647
  canvas.restoreToCount(count);
497
648
  }
@@ -543,9 +694,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
543
694
  // an opacity can change during it.
544
695
  // Not the transition alpha, which only Android's Transition fades
545
696
  // (android.transition.Fade) set: getTransitionAlpha is public from API 29
546
- // only (hidden on Android 5 to 9, and the library compiles against 28
547
- // when an app doesn't set compileSdkVersion). React Native's opacity,
548
- // Animated, LayoutAnimation and Reanimated set the view's alpha, and
697
+ // only (hidden on Android 7 to 9). React Native's opacity, Animated,
698
+ // LayoutAnimation and Reanimated set the view's alpha, and
549
699
  // react-native-screens' fragment animations run on a container with a
550
700
  // layer of its own.
551
701
  private boolean drawnWithAlpha() {
@@ -760,61 +910,129 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
760
910
  animated.setRepeatCount(mLoopCount == 0 ? AnimatedImageDrawable.REPEAT_INFINITE : mLoopCount - 1);
761
911
  }
762
912
 
763
- // resizeMode repeat: the image (a GIF's first frame) repeated from the
764
- // top-left at its own size, scaled down to fit the view if it's larger,
765
- // as React Native's Image does. The view fills with it (FIT_XY).
766
- private boolean mRepeat = false;
767
-
768
- // The scale type for resizeMode, which Glide decodes the image for and the
769
- // blur scales with. For scale-down (CENTER_INSIDE) and none (CENTER) the
913
+ // The scale type for objectFit, which Glide decodes the image for and the
914
+ // blur scales with. For scale-down (CENTER_INSIDE) and none (CENTER), and
915
+ // contain and cover with an objectPosition other than the center, the
770
916
  // view is MATRIX: it lays the image out itself (see updateImageMatrix).
771
917
  // Starts as ImageView's.
772
- private ScaleType mResizeScaleType = ScaleType.FIT_CENTER;
773
-
774
- // resizeMode center: CENTER_INSIDE as ImageView draws it, an image smaller
775
- // than the view at its size in pixels on the screen (scale-down: in dp).
776
- private boolean mInPixels = false;
918
+ private ScaleType mFitScaleType = ScaleType.FIT_CENTER;
777
919
 
778
- // The scale type the image on screen is laid out for (see
779
- // applyResizeMode).
920
+ // The scale type the image on screen is laid out for, and whether it was
921
+ // downsampled or pixelated (see applyObjectFit).
780
922
  private ScaleType mLayoutScaleType = ScaleType.FIT_CENTER;
923
+ private boolean mLayoutDownsampled = true;
924
+ private boolean mLayoutPixelated = false;
781
925
 
782
926
  // Glide crops or fits the bitmap for the scale type when it loads, so a
783
- // new resizeMode needs a reload to take effect (#762): the image that's
927
+ // new objectFit needs a reload to take effect (#762): the image that's
784
928
  // showing, loaded again quietly (see reblur), as for a new blurRadius,
785
929
  // without the load events, as on iOS.
786
- public void setResizeMode(ScaleType scaleType, boolean repeat, boolean inPixels) {
787
- if (scaleType == mResizeScaleType && repeat == mRepeat && inPixels == mInPixels) return;
788
- mResizeScaleType = scaleType;
789
- mInPixels = inPixels;
790
- mRepeat = repeat;
930
+ public void setObjectFit(ScaleType scaleType) {
931
+ if (scaleType == mFitScaleType) return;
932
+ mFitScaleType = scaleType;
791
933
  mFitChanged = true;
792
934
  // A loaded image on screen was cropped or fitted for the old scale
793
935
  // type: it keeps its layout until the image for the new one is ready
794
936
  // (onImageLoaded), or the load fails. Laid out for the new one, a
795
937
  // cover crop would be stretched to the image's own size.
796
- if (mShownRequest == null) applyResizeMode();
938
+ if (mShownRequest == null) applyObjectFit();
939
+ }
940
+
941
+ // Lays the view out for objectFit.
942
+ private void applyObjectFit() {
943
+ mLayoutScaleType = mFitScaleType;
944
+ mLayoutDownsampled = mDownsample;
945
+ mLayoutPixelated = mPixelated;
946
+ updateScaleType();
947
+ }
948
+
949
+ // objectPosition: on each axis the image's left or top edge is at
950
+ // (view - image) * fraction + offset (offsets in pixels).
951
+ private float mPositionX = 0.5f;
952
+ private float mPositionXOffset = 0;
953
+ private float mPositionY = 0.5f;
954
+ private float mPositionYOffset = 0;
955
+
956
+ public void setObjectPositionX(float x) {
957
+ if (x == mPositionX) return;
958
+ mPositionX = x;
959
+ onPositionChanged();
960
+ }
961
+
962
+ public void setObjectPositionXOffset(float xOffset) {
963
+ if (xOffset == mPositionXOffset) return;
964
+ mPositionXOffset = xOffset;
965
+ onPositionChanged();
966
+ }
967
+
968
+ public void setObjectPositionY(float y) {
969
+ if (y == mPositionY) return;
970
+ mPositionY = y;
971
+ onPositionChanged();
972
+ }
973
+
974
+ public void setObjectPositionYOffset(float yOffset) {
975
+ if (yOffset == mPositionYOffset) return;
976
+ mPositionYOffset = yOffset;
977
+ onPositionChanged();
978
+ }
979
+
980
+ // Glide crops a cover image at the position (FastImageCrop): the image on
981
+ // screen is loaded again for it, quietly (see reblur), and until that's
982
+ // ready it stays as it was cropped (see shownCropped). Others, and cover
983
+ // images Glide doesn't crop, are only laid out at it, right away.
984
+ private void onPositionChanged() {
985
+ if (mFitScaleType == ScaleType.CENTER_CROP && !mPixelated && mDownsample) {
986
+ mCropChanged = true;
987
+ }
988
+ updateScaleType();
989
+ }
990
+
991
+ private boolean isCentered() {
992
+ return mPositionX == 0.5f && mPositionY == 0.5f && mPositionXOffset == 0 && mPositionYOffset == 0;
993
+ }
994
+
995
+ // Where Glide crops a cover image: CenterCrop at the center (as without
996
+ // objectPosition), FastImageCrop elsewhere.
997
+ @Nullable
998
+ private FastImageCrop positionedCrop() {
999
+ return mFitScaleType == ScaleType.CENTER_CROP && !isCentered() && !mPixelated && mDownsample
1000
+ ? new FastImageCrop(mPositionX, mPositionXOffset, mPositionY, mPositionYOffset)
1001
+ : null;
1002
+ }
1003
+
1004
+ // Whether the image on screen is a cover one Glide cropped to what the
1005
+ // view shows (CenterCrop or FastImageCrop, at the position it was loaded
1006
+ // for). Glide can't crop animated WebPs and AVIFs, APNGs and
1007
+ // defaultSource, and doesn't crop pixelated images or ones decoded at full
1008
+ // size (downsample false).
1009
+ private boolean shownCropped(Drawable drawable) {
1010
+ return mLayoutScaleType == ScaleType.CENTER_CROP && !mLayoutPixelated && mLayoutDownsampled
1011
+ && drawable != mDefaultSource
1012
+ && (drawable instanceof BitmapDrawable || drawable instanceof GifDrawable);
797
1013
  }
798
1014
 
799
- // Lays the view out for resizeMode.
800
- private void applyResizeMode() {
801
- mLayoutScaleType = mResizeScaleType;
802
- setScaleType(!mInPixels && (mResizeScaleType == ScaleType.CENTER_INSIDE || mResizeScaleType == ScaleType.CENTER)
803
- ? ScaleType.MATRIX
804
- : mResizeScaleType);
1015
+ // ImageView's own scale type for the layout, or MATRIX where the view lays
1016
+ // the image out itself (updateImageMatrix): scale-down and none at their
1017
+ // size in dp, and contain and cover at a position other than the center.
1018
+ private void updateScaleType() {
1019
+ ScaleType type = mLayoutScaleType;
1020
+ boolean ownSize = type == ScaleType.CENTER_INSIDE || type == ScaleType.CENTER;
1021
+ boolean positioned = !isCentered()
1022
+ && (ownSize || type == ScaleType.FIT_CENTER || type == ScaleType.CENTER_CROP);
1023
+ boolean laidOut = ownSize || positioned;
1024
+ setScaleType(laidOut ? ScaleType.MATRIX : type);
805
1025
  updateImageMatrix();
806
1026
  }
807
1027
 
808
- // Whether the view shows its defaultSource (or its tiled copy, which has
809
- // its bitmap): FastImage's own drawable, which Glide doesn't recycle (see
810
- // startFade).
1028
+ // Whether the view shows its defaultSource: FastImage's own drawable,
1029
+ // which Glide doesn't recycle (see startFade).
811
1030
  private boolean mShowsDefaultSource = false;
812
1031
 
813
- // Repeats what the view shows (the loaded image, and defaultSource).
814
1032
  @Override
815
1033
  public void setImageDrawable(@Nullable Drawable drawable) {
816
1034
  mShowsDefaultSource = drawable != null && drawable == mDefaultSource;
817
- super.setImageDrawable(mRepeat ? tiled(drawable) : drawable);
1035
+ super.setImageDrawable(drawable);
818
1036
  updateImageMatrix();
819
1037
  }
820
1038
 
@@ -857,12 +1075,16 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
857
1075
  return (float) (getResources().getDisplayMetrics().density / mLoadingScale);
858
1076
  }
859
1077
 
860
- // scale-down: the image at its own size, centered, or scaled down
861
- // uniformly to fit if that's larger than the view. Glide decodes it no
862
- // larger than that (CenterInside), and it's drawn at this size. none: the
863
- // image at its own size, centered, whatever its size (cropped by the view;
864
- // Glide decodes it at its size). defaultSource (a resource) is already at
865
- // its size on screen.
1078
+ // The image laid out as CSS's object-fit and object-position do: its size
1079
+ // for the fit (cover and contain from its shape; none at its own size,
1080
+ // cropped by the view; scale-down at its own size, or scaled down
1081
+ // uniformly to fit if that's larger than the view), with its left and top
1082
+ // edges at (view - image) * fraction + offset. Glide decodes a scale-down
1083
+ // image no larger than that (CenterInside), a none one at its size, and
1084
+ // crops a cover one to what the view shows (FastImageCrop), so it's drawn
1085
+ // at this size. defaultSource (a resource) is already at its size on
1086
+ // screen, and so is a center image (its size in pixels, as ImageView's
1087
+ // CENTER_INSIDE draws it).
866
1088
  private void updateImageMatrix() {
867
1089
  if (getScaleType() != ScaleType.MATRIX) return;
868
1090
  Drawable drawable = getDrawable();
@@ -880,7 +1102,27 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
880
1102
  }
881
1103
  float width;
882
1104
  float height;
883
- if (drawable == mDefaultSource) {
1105
+ if (shownCropped(drawable)) {
1106
+ // Glide cropped it to what the view shows (FastImageCrop), or for
1107
+ // the previous position or size until the image has loaded again
1108
+ // for this one: it covers the view, scaled uniformly (a crop for
1109
+ // another size isn't stretched), at the position's fractions (its
1110
+ // offsets are already in the crop).
1111
+ float cover = Math.max(viewWidth / drawableWidth, viewHeight / drawableHeight);
1112
+ matrix.setScale(cover, cover);
1113
+ matrix.postTranslate(
1114
+ Math.round((viewWidth - drawableWidth * cover) * mPositionX),
1115
+ Math.round((viewHeight - drawableHeight * cover) * mPositionY));
1116
+ setImageMatrix(matrix);
1117
+ return;
1118
+ }
1119
+ if (mLayoutScaleType == ScaleType.CENTER_CROP || mLayoutScaleType == ScaleType.FIT_CENTER) {
1120
+ float x = viewWidth / drawableWidth;
1121
+ float y = viewHeight / drawableHeight;
1122
+ float scale = mLayoutScaleType == ScaleType.CENTER_CROP ? Math.max(x, y) : Math.min(x, y);
1123
+ width = drawableWidth * scale;
1124
+ height = drawableHeight * scale;
1125
+ } else if (drawable == mDefaultSource) {
884
1126
  width = drawableWidth;
885
1127
  height = drawableHeight;
886
1128
  } else if (mImageWidth > 0 && mImageHeight > 0) {
@@ -893,31 +1135,18 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
893
1135
  width = drawableWidth * imageDensity();
894
1136
  height = drawableHeight * imageDensity();
895
1137
  }
896
- float fit = mLayoutScaleType == ScaleType.CENTER
897
- ? 1
898
- : Math.min(1, Math.min(viewWidth / width, viewHeight / height));
1138
+ float fit = mLayoutScaleType == ScaleType.CENTER_INSIDE
1139
+ ? Math.min(1, Math.min(viewWidth / width, viewHeight / height))
1140
+ : 1;
899
1141
  width *= fit;
900
1142
  height *= fit;
901
1143
  matrix.setScale(width / drawableWidth, height / drawableHeight);
902
- matrix.postTranslate(Math.round((viewWidth - width) / 2), Math.round((viewHeight - height) / 2));
1144
+ matrix.postTranslate(
1145
+ Math.round((viewWidth - width) * mPositionX + mPositionXOffset),
1146
+ Math.round((viewHeight - height) * mPositionY + mPositionYOffset));
903
1147
  setImageMatrix(matrix);
904
1148
  }
905
1149
 
906
- @Nullable
907
- private Drawable tiled(@Nullable Drawable drawable) {
908
- if (!(drawable instanceof BitmapDrawable)) return drawable;
909
- BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
910
- if (bitmapDrawable.getTileModeX() == Shader.TileMode.REPEAT) return drawable;
911
- Bitmap bitmap = bitmapDrawable.getBitmap();
912
- if (bitmap == null) return drawable;
913
- // A copy of the drawable (not of the bitmap): Glide's may be shown by
914
- // other views too.
915
- BitmapDrawable tiled = new BitmapDrawable(getResources(), bitmap);
916
- tiled.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
917
- if (mPixelated) tiled.setFilterBitmap(false);
918
- return tiled;
919
- }
920
-
921
1150
  // imageRendering="pixelated": no resampling by Glide, and drawn without
922
1151
  // filtering (sharp pixels). "smooth" is iOS only; here it's the same as
923
1152
  // "auto": Glide decodes a large image at about the view's size (unless
@@ -942,14 +1171,19 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
942
1171
  if (downsample == mDownsample) return;
943
1172
  mDownsample = downsample;
944
1173
  mNeedsReload = true;
1174
+ // As for a new objectFit: an image on screen keeps the layout it was
1175
+ // loaded for until the new one is ready.
1176
+ if (mShownRequest == null) applyObjectFit();
945
1177
  }
946
1178
 
947
1179
  // blurRadius, in pixels (0 is no blur). Part of the request (see
948
1180
  // reblur for a change).
949
1181
  private float mBlurRadius = 0;
950
1182
  private boolean mBlurChanged = false;
951
- // resizeMode or objectFit changed (setResizeMode).
1183
+ // objectFit changed (setObjectFit).
952
1184
  private boolean mFitChanged = false;
1185
+ // objectPosition changed for an image Glide crops (onPositionChanged).
1186
+ private boolean mCropChanged = false;
953
1187
 
954
1188
  public void setBlurRadius(float blurRadius) {
955
1189
  if (blurRadius == mBlurRadius) return;
@@ -962,21 +1196,10 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
962
1196
  // as on iOS.
963
1197
  private RequestOptions renderingOptions(Object model) {
964
1198
  FastImageBlur blur = mBlurRadius > 0
965
- ? new FastImageBlur(mBlurRadius, mRepeat ? null : mResizeScaleType, mInPixels ? 1 : loadingDensity())
1199
+ ? new FastImageBlur(mBlurRadius, mFitScaleType, loadingDensity())
966
1200
  : null;
967
1201
  RequestOptions options;
968
- if (mRepeat) {
969
- // Decoded at its own size, or scaled down to fit the view if it's
970
- // larger (not cropped or fitted for the scale type), and a GIF as
971
- // its first frame (tiled by setImageDrawable).
972
- options = new RequestOptions()
973
- .downsample(new FastImageSourceSize.Capture(
974
- mPixelated ? DownsampleStrategy.NONE : DownsampleStrategy.CENTER_INSIDE,
975
- String.valueOf(model)))
976
- .dontTransform()
977
- .dontAnimate();
978
- if (blur != null) options = options.optionalTransform(blur);
979
- } else if (mPixelated) {
1202
+ if (mPixelated) {
980
1203
  // Decoded as it is, then scaled by the view without filtering (see
981
1204
  // OwnGifTarget).
982
1205
  options = new RequestOptions()
@@ -984,15 +1207,16 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
984
1207
  .dontTransform();
985
1208
  if (blur != null) options = options.optionalTransform(blur);
986
1209
  } else {
987
- options = FastImageSourceSize.scaleTypeOptions(mResizeScaleType,
988
- FastImageSourceSize.capture(mResizeScaleType, model, mDownsample), blur, mDownsample);
989
- boolean ownSize = mResizeScaleType == ScaleType.CENTER_INSIDE || mResizeScaleType == ScaleType.CENTER;
1210
+ options = FastImageSourceSize.scaleTypeOptions(mFitScaleType,
1211
+ FastImageSourceSize.capture(mFitScaleType, model, mDownsample), positionedCrop(), blur,
1212
+ mDownsample);
1213
+ boolean ownSize = mFitScaleType == ScaleType.CENTER_INSIDE || mFitScaleType == ScaleType.CENTER;
990
1214
  // scale-down and none show it at its own size in dp: an SVG is
991
1215
  // drawn at that size, so it's sharp (not with a blur, which hides
992
1216
  // the difference).
993
- if (ownSize && !mInPixels && blur == null) {
1217
+ if (ownSize && blur == null) {
994
1218
  options = options.set(FastImageSvg.DENSITY, loadingDensity())
995
- .set(FastImageSvg.FITS, mResizeScaleType == ScaleType.CENTER_INSIDE);
1219
+ .set(FastImageSvg.FITS, mFitScaleType == ScaleType.CENTER_INSIDE);
996
1220
  }
997
1221
  }
998
1222
  return blur != null ? options.dontAnimate() : options;
@@ -1059,7 +1283,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1059
1283
  mLoadEnded = true;
1060
1284
  endProgress();
1061
1285
  // Before the view shows it (the target does once this returns).
1062
- applyResizeMode();
1286
+ applyObjectFit();
1063
1287
  mShownRequest = mLoadingRequest;
1064
1288
  mShownWidth = mLoadingWidth;
1065
1289
  mShownHeight = mLoadingHeight;
@@ -1102,8 +1326,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1102
1326
 
1103
1327
  // Whether the image loaded, at a size the view doesn't have now.
1104
1328
  private boolean isResized() {
1105
- int width = getWidth() - getPaddingLeft() - getPaddingRight();
1106
- int height = getHeight() - getPaddingTop() - getPaddingBottom();
1329
+ int width = contentWidth();
1330
+ int height = contentHeight();
1107
1331
  return requestManager != null && !mDropped && !mNeedsReload
1108
1332
  && mShownRequest != null && mShownRequest == mLoadingRequest
1109
1333
  && mShownWidth > 0 && mShownHeight > 0 && width > 0 && height > 0
@@ -1127,7 +1351,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1127
1351
  into(shown, builder, meanwhile, true);
1128
1352
  }
1129
1353
 
1130
- // blurRadius or resizeMode changed, and nothing else that needs a reload:
1354
+ // blurRadius or objectFit changed, and nothing else that needs a reload:
1131
1355
  // the image that's showing, blurred or fitted again, with no events, as
1132
1356
  // reloadForSize does: it's decoded again from the cache and blurred or
1133
1357
  // fitted on Glide's threads, and the view keeps the current image (and its
@@ -1226,7 +1450,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1226
1450
  void onImageFailed(boolean hadThumbnail) {
1227
1451
  mLoadEnded = true;
1228
1452
  endProgress();
1229
- applyResizeMode();
1453
+ applyObjectFit();
1230
1454
  mShownRequest = null;
1231
1455
  if (!hadThumbnail) return;
1232
1456
  final int load = mLoadCount;
@@ -1245,9 +1469,10 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1245
1469
  @Nullable RequestManager requestManager) {
1246
1470
  if (mDropped) return;
1247
1471
  mManager = manager;
1248
- if ((mBlurChanged || mFitChanged) && !mNeedsReload) {
1472
+ if ((mBlurChanged || mFitChanged || mCropChanged) && !mNeedsReload) {
1249
1473
  mBlurChanged = false;
1250
1474
  mFitChanged = false;
1475
+ mCropChanged = false;
1251
1476
  reblur();
1252
1477
  }
1253
1478
  if (!mNeedsReload)
@@ -1275,10 +1500,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1275
1500
  pickSource();
1276
1501
  }
1277
1502
  // Only reload for changes that affect the request (source,
1278
- // defaultSource, resizeMode), not for every prop update.
1503
+ // defaultSource, objectFit), not for every prop update.
1279
1504
  mNeedsReload = false;
1280
1505
  mBlurChanged = false;
1281
1506
  mFitChanged = false;
1507
+ mCropChanged = false;
1282
1508
  boolean switching = mSwitchesSource;
1283
1509
  mSwitchesSource = false;
1284
1510
  boolean restarting = mRestarting;