react-native-fast-image 9.3.6 → 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 +362 -129
  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,59 +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).
784
- public void setResizeMode(ScaleType scaleType, boolean repeat, boolean inPixels) {
785
- if (scaleType == mResizeScaleType && repeat == mRepeat && inPixels == mInPixels) return;
786
- mResizeScaleType = scaleType;
787
- mInPixels = inPixels;
788
- mRepeat = repeat;
789
- mNeedsReload = true;
927
+ // new objectFit needs a reload to take effect (#762): the image that's
928
+ // showing, loaded again quietly (see reblur), as for a new blurRadius,
929
+ // without the load events, as on iOS.
930
+ public void setObjectFit(ScaleType scaleType) {
931
+ if (scaleType == mFitScaleType) return;
932
+ mFitScaleType = scaleType;
933
+ mFitChanged = true;
790
934
  // A loaded image on screen was cropped or fitted for the old scale
791
935
  // type: it keeps its layout until the image for the new one is ready
792
936
  // (onImageLoaded), or the load fails. Laid out for the new one, a
793
937
  // cover crop would be stretched to the image's own size.
794
- 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();
795
966
  }
796
967
 
797
- // Lays the view out for resizeMode.
798
- private void applyResizeMode() {
799
- mLayoutScaleType = mResizeScaleType;
800
- setScaleType(!mInPixels && (mResizeScaleType == ScaleType.CENTER_INSIDE || mResizeScaleType == ScaleType.CENTER)
801
- ? ScaleType.MATRIX
802
- : mResizeScaleType);
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);
1013
+ }
1014
+
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);
803
1025
  updateImageMatrix();
804
1026
  }
805
1027
 
806
- // Whether the view shows its defaultSource (or its tiled copy, which has
807
- // its bitmap): FastImage's own drawable, which Glide doesn't recycle (see
808
- // startFade).
1028
+ // Whether the view shows its defaultSource: FastImage's own drawable,
1029
+ // which Glide doesn't recycle (see startFade).
809
1030
  private boolean mShowsDefaultSource = false;
810
1031
 
811
- // Repeats what the view shows (the loaded image, and defaultSource).
812
1032
  @Override
813
1033
  public void setImageDrawable(@Nullable Drawable drawable) {
814
1034
  mShowsDefaultSource = drawable != null && drawable == mDefaultSource;
815
- super.setImageDrawable(mRepeat ? tiled(drawable) : drawable);
1035
+ super.setImageDrawable(drawable);
816
1036
  updateImageMatrix();
817
1037
  }
818
1038
 
@@ -855,12 +1075,16 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
855
1075
  return (float) (getResources().getDisplayMetrics().density / mLoadingScale);
856
1076
  }
857
1077
 
858
- // scale-down: the image at its own size, centered, or scaled down
859
- // uniformly to fit if that's larger than the view. Glide decodes it no
860
- // larger than that (CenterInside), and it's drawn at this size. none: the
861
- // image at its own size, centered, whatever its size (cropped by the view;
862
- // Glide decodes it at its size). defaultSource (a resource) is already at
863
- // 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).
864
1088
  private void updateImageMatrix() {
865
1089
  if (getScaleType() != ScaleType.MATRIX) return;
866
1090
  Drawable drawable = getDrawable();
@@ -878,7 +1102,27 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
878
1102
  }
879
1103
  float width;
880
1104
  float height;
881
- 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) {
882
1126
  width = drawableWidth;
883
1127
  height = drawableHeight;
884
1128
  } else if (mImageWidth > 0 && mImageHeight > 0) {
@@ -891,31 +1135,18 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
891
1135
  width = drawableWidth * imageDensity();
892
1136
  height = drawableHeight * imageDensity();
893
1137
  }
894
- float fit = mLayoutScaleType == ScaleType.CENTER
895
- ? 1
896
- : 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;
897
1141
  width *= fit;
898
1142
  height *= fit;
899
1143
  matrix.setScale(width / drawableWidth, height / drawableHeight);
900
- 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));
901
1147
  setImageMatrix(matrix);
902
1148
  }
903
1149
 
904
- @Nullable
905
- private Drawable tiled(@Nullable Drawable drawable) {
906
- if (!(drawable instanceof BitmapDrawable)) return drawable;
907
- BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
908
- if (bitmapDrawable.getTileModeX() == Shader.TileMode.REPEAT) return drawable;
909
- Bitmap bitmap = bitmapDrawable.getBitmap();
910
- if (bitmap == null) return drawable;
911
- // A copy of the drawable (not of the bitmap): Glide's may be shown by
912
- // other views too.
913
- BitmapDrawable tiled = new BitmapDrawable(getResources(), bitmap);
914
- tiled.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
915
- if (mPixelated) tiled.setFilterBitmap(false);
916
- return tiled;
917
- }
918
-
919
1150
  // imageRendering="pixelated": no resampling by Glide, and drawn without
920
1151
  // filtering (sharp pixels). "smooth" is iOS only; here it's the same as
921
1152
  // "auto": Glide decodes a large image at about the view's size (unless
@@ -940,12 +1171,19 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
940
1171
  if (downsample == mDownsample) return;
941
1172
  mDownsample = downsample;
942
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();
943
1177
  }
944
1178
 
945
1179
  // blurRadius, in pixels (0 is no blur). Part of the request (see
946
1180
  // reblur for a change).
947
1181
  private float mBlurRadius = 0;
948
1182
  private boolean mBlurChanged = false;
1183
+ // objectFit changed (setObjectFit).
1184
+ private boolean mFitChanged = false;
1185
+ // objectPosition changed for an image Glide crops (onPositionChanged).
1186
+ private boolean mCropChanged = false;
949
1187
 
950
1188
  public void setBlurRadius(float blurRadius) {
951
1189
  if (blurRadius == mBlurRadius) return;
@@ -958,21 +1196,10 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
958
1196
  // as on iOS.
959
1197
  private RequestOptions renderingOptions(Object model) {
960
1198
  FastImageBlur blur = mBlurRadius > 0
961
- ? new FastImageBlur(mBlurRadius, mRepeat ? null : mResizeScaleType, mInPixels ? 1 : loadingDensity())
1199
+ ? new FastImageBlur(mBlurRadius, mFitScaleType, loadingDensity())
962
1200
  : null;
963
1201
  RequestOptions options;
964
- if (mRepeat) {
965
- // Decoded at its own size, or scaled down to fit the view if it's
966
- // larger (not cropped or fitted for the scale type), and a GIF as
967
- // its first frame (tiled by setImageDrawable).
968
- options = new RequestOptions()
969
- .downsample(new FastImageSourceSize.Capture(
970
- mPixelated ? DownsampleStrategy.NONE : DownsampleStrategy.CENTER_INSIDE,
971
- String.valueOf(model)))
972
- .dontTransform()
973
- .dontAnimate();
974
- if (blur != null) options = options.optionalTransform(blur);
975
- } else if (mPixelated) {
1202
+ if (mPixelated) {
976
1203
  // Decoded as it is, then scaled by the view without filtering (see
977
1204
  // OwnGifTarget).
978
1205
  options = new RequestOptions()
@@ -980,15 +1207,16 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
980
1207
  .dontTransform();
981
1208
  if (blur != null) options = options.optionalTransform(blur);
982
1209
  } else {
983
- options = FastImageSourceSize.scaleTypeOptions(mResizeScaleType,
984
- FastImageSourceSize.capture(mResizeScaleType, model, mDownsample), blur, mDownsample);
985
- 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;
986
1214
  // scale-down and none show it at its own size in dp: an SVG is
987
1215
  // drawn at that size, so it's sharp (not with a blur, which hides
988
1216
  // the difference).
989
- if (ownSize && !mInPixels && blur == null) {
1217
+ if (ownSize && blur == null) {
990
1218
  options = options.set(FastImageSvg.DENSITY, loadingDensity())
991
- .set(FastImageSvg.FITS, mResizeScaleType == ScaleType.CENTER_INSIDE);
1219
+ .set(FastImageSvg.FITS, mFitScaleType == ScaleType.CENTER_INSIDE);
992
1220
  }
993
1221
  }
994
1222
  return blur != null ? options.dontAnimate() : options;
@@ -1055,7 +1283,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1055
1283
  mLoadEnded = true;
1056
1284
  endProgress();
1057
1285
  // Before the view shows it (the target does once this returns).
1058
- applyResizeMode();
1286
+ applyObjectFit();
1059
1287
  mShownRequest = mLoadingRequest;
1060
1288
  mShownWidth = mLoadingWidth;
1061
1289
  mShownHeight = mLoadingHeight;
@@ -1098,8 +1326,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1098
1326
 
1099
1327
  // Whether the image loaded, at a size the view doesn't have now.
1100
1328
  private boolean isResized() {
1101
- int width = getWidth() - getPaddingLeft() - getPaddingRight();
1102
- int height = getHeight() - getPaddingTop() - getPaddingBottom();
1329
+ int width = contentWidth();
1330
+ int height = contentHeight();
1103
1331
  return requestManager != null && !mDropped && !mNeedsReload
1104
1332
  && mShownRequest != null && mShownRequest == mLoadingRequest
1105
1333
  && mShownWidth > 0 && mShownHeight > 0 && width > 0 && height > 0
@@ -1123,10 +1351,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1123
1351
  into(shown, builder, meanwhile, true);
1124
1352
  }
1125
1353
 
1126
- // blurRadius changed, and nothing else that needs a reload: the image
1127
- // that's showing, blurred again, with no events, as reloadForSize does: it's
1128
- // decoded again from the cache and blurred on Glide's threads, and the view
1129
- // keeps the current image until then. If that fails, it shows the image
1354
+ // blurRadius or objectFit changed, and nothing else that needs a reload:
1355
+ // the image that's showing, blurred or fitted again, with no events, as
1356
+ // reloadForSize does: it's decoded again from the cache and blurred or
1357
+ // fitted on Glide's threads, and the view keeps the current image (and its
1358
+ // layout, see onImageLoaded) until then. If that fails, it shows the image
1130
1359
  // from Glide's caches; cache: 'web' skips them, so the view shows
1131
1360
  // defaultSource (or nothing) then. Also while it's loading again that way
1132
1361
  // (e.g. the blur changes every frame): that load is replaced. A load that
@@ -1221,7 +1450,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1221
1450
  void onImageFailed(boolean hadThumbnail) {
1222
1451
  mLoadEnded = true;
1223
1452
  endProgress();
1224
- applyResizeMode();
1453
+ applyObjectFit();
1225
1454
  mShownRequest = null;
1226
1455
  if (!hadThumbnail) return;
1227
1456
  final int load = mLoadCount;
@@ -1240,8 +1469,10 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1240
1469
  @Nullable RequestManager requestManager) {
1241
1470
  if (mDropped) return;
1242
1471
  mManager = manager;
1243
- if (mBlurChanged && !mNeedsReload) {
1472
+ if ((mBlurChanged || mFitChanged || mCropChanged) && !mNeedsReload) {
1244
1473
  mBlurChanged = false;
1474
+ mFitChanged = false;
1475
+ mCropChanged = false;
1245
1476
  reblur();
1246
1477
  }
1247
1478
  if (!mNeedsReload)
@@ -1269,9 +1500,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1269
1500
  pickSource();
1270
1501
  }
1271
1502
  // Only reload for changes that affect the request (source,
1272
- // defaultSource, resizeMode), not for every prop update.
1503
+ // defaultSource, objectFit), not for every prop update.
1273
1504
  mNeedsReload = false;
1274
1505
  mBlurChanged = false;
1506
+ mFitChanged = false;
1507
+ mCropChanged = false;
1275
1508
  boolean switching = mSwitchesSource;
1276
1509
  mSwitchesSource = false;
1277
1510
  boolean restarting = mRestarting;