react-native-fast-image 8.20.0 → 8.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,16 @@ package com.dylanvann.fastimage;
2
2
 
3
3
  import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT;
4
4
 
5
+ import android.animation.Animator;
6
+ import android.animation.AnimatorListenerAdapter;
7
+ import android.animation.ValueAnimator;
5
8
  import android.annotation.SuppressLint;
6
9
  import android.content.Context;
7
10
  import android.graphics.Bitmap;
11
+ import android.graphics.Canvas;
12
+ import android.graphics.Paint;
13
+ import android.graphics.PorterDuff;
14
+ import android.graphics.PorterDuffXfermode;
8
15
  import android.graphics.drawable.BitmapDrawable;
9
16
  import android.graphics.drawable.Drawable;
10
17
 
@@ -13,15 +20,20 @@ import androidx.annotation.Nullable;
13
20
  import androidx.appcompat.widget.AppCompatImageView;
14
21
  import androidx.core.view.ViewCompat;
15
22
 
23
+ import com.bumptech.glide.GenericTransitionOptions;
16
24
  import com.bumptech.glide.RequestBuilder;
17
25
  import com.bumptech.glide.RequestManager;
26
+ import com.bumptech.glide.load.DataSource;
18
27
  import com.bumptech.glide.load.model.GlideUrl;
19
28
  import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
29
+ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
20
30
  import com.bumptech.glide.load.resource.gif.GifDrawable;
21
31
  import com.bumptech.glide.request.RequestOptions;
22
32
  import com.bumptech.glide.request.target.DrawableImageViewTarget;
23
33
  import com.bumptech.glide.request.target.SizeReadyCallback;
34
+ import com.bumptech.glide.request.transition.NoTransition;
24
35
  import com.bumptech.glide.request.transition.Transition;
36
+ import com.bumptech.glide.request.transition.TransitionFactory;
25
37
  import com.facebook.react.bridge.ReadableMap;
26
38
  import com.facebook.react.bridge.WritableMap;
27
39
  import com.facebook.react.bridge.WritableNativeMap;
@@ -88,6 +100,168 @@ class FastImageViewWithUrl extends AppCompatImageView {
88
100
  // Pauses GIFs on the frame they're showing (the view's own animation).
89
101
  private boolean mPaused = false;
90
102
 
103
+ // The `transition` prop (from the next load): how long a loaded image
104
+ // takes to fade in, in milliseconds (0 for no fade), whether it also fades
105
+ // over a loaded image (a new source), and which cache hits show at once
106
+ // (none, memory or all).
107
+ private int mTransitionDuration = 0;
108
+ private boolean mTransitionBetweenImages = false;
109
+ private String mTransitionSkipOnCacheHit = "memory";
110
+
111
+ public void setTransitionDuration(int transitionDuration) {
112
+ mTransitionDuration = Math.max(0, transitionDuration);
113
+ }
114
+
115
+ public void setTransitionBetweenImages(boolean betweenImages) {
116
+ mTransitionBetweenImages = betweenImages;
117
+ }
118
+
119
+ public void setTransitionSkipOnCacheHit(@Nullable String skipOnCacheHit) {
120
+ mTransitionSkipOnCacheHit = skipOnCacheHit == null ? "memory" : skipOnCacheHit;
121
+ }
122
+
123
+ // Fades a loaded image in, unless it's from a cache that skipOnCacheHit
124
+ // skips, as Glide's and Coil's cross-fades and Fresco decide: from the
125
+ // memory cache it shows at once (by default), and from the disk cache
126
+ // too with 'all'. Downloads, local files and bundled images fade.
127
+ private final class FadeFactory implements TransitionFactory<Drawable> {
128
+ private final int duration;
129
+ private final String skipOnCacheHit;
130
+
131
+ FadeFactory(int duration, String skipOnCacheHit) {
132
+ this.duration = duration;
133
+ this.skipOnCacheHit = skipOnCacheHit;
134
+ }
135
+
136
+ @Override
137
+ public Transition<Drawable> build(DataSource dataSource, boolean isFirstResource) {
138
+ if (skips(dataSource)) return NoTransition.get();
139
+ return new Transition<Drawable>() {
140
+ @Override
141
+ public boolean transition(Drawable current, ViewAdapter adapter) {
142
+ startFade(duration);
143
+ // The target shows the image as usual.
144
+ return false;
145
+ }
146
+ };
147
+ }
148
+
149
+ private boolean skips(DataSource dataSource) {
150
+ if (skipOnCacheHit.equals("none")) return false;
151
+ if (dataSource == DataSource.MEMORY_CACHE) return true;
152
+ // Glide also keeps a local file's or bundled image's decoded
153
+ // image in its disk cache (at the size it was decoded at, and not
154
+ // for GIFs), so with 'all' they usually only fade the first time.
155
+ boolean disk = dataSource == DataSource.DATA_DISK_CACHE || dataSource == DataSource.RESOURCE_DISK_CACHE;
156
+ return disk && skipOnCacheHit.equals("all");
157
+ }
158
+ }
159
+
160
+ // A fade in progress: what the view showed (null if nothing), drawn
161
+ // fading out while the view's image fades in, as a cross-dissolve on iOS.
162
+ // Drawn here rather than with Glide's DrawableCrossFadeTransition, whose
163
+ // TransitionDrawable stretches both images to one size and stays in the
164
+ // view after the fade, still drawing (and animating) the previous image.
165
+ @Nullable
166
+ private ValueAnimator mFade;
167
+ @Nullable
168
+ private Bitmap mFadeFrom;
169
+ private float mFadeProgress = 1f;
170
+ private final Paint mFadeFromPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
171
+ private final Paint mFadeInPaint = new Paint();
172
+
173
+ {
174
+ // The two add up to a cross-dissolve: from * (1 - t) + image * t.
175
+ mFadeInPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
176
+ }
177
+
178
+ private void startFade(int duration) {
179
+ int width = getWidth();
180
+ int height = getHeight();
181
+ if (width <= 0 || height <= 0) return;
182
+ // What the view shows now, including a fade in progress.
183
+ Bitmap from = null;
184
+ if (getDrawable() != null) {
185
+ try {
186
+ from = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
187
+ onDraw(new Canvas(from));
188
+ } catch (OutOfMemoryError | RuntimeException e) {
189
+ // Out of memory, or a hardware bitmap (Android 8+; an app's
190
+ // Glide module can enable them), which a software canvas
191
+ // can't draw: fade in over nothing instead.
192
+ if (from != null) from.recycle();
193
+ from = null;
194
+ }
195
+ }
196
+ endFade();
197
+ mFadeFrom = from;
198
+ mFadeProgress = 0f;
199
+ final ValueAnimator fade = ValueAnimator.ofFloat(0f, 1f);
200
+ fade.setDuration(duration);
201
+ fade.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
202
+ @Override
203
+ public void onAnimationUpdate(ValueAnimator animation) {
204
+ mFadeProgress = (float) animation.getAnimatedValue();
205
+ invalidate();
206
+ }
207
+ });
208
+ fade.addListener(new AnimatorListenerAdapter() {
209
+ @Override
210
+ public void onAnimationEnd(Animator animation) {
211
+ if (mFade == fade) endFade();
212
+ }
213
+ });
214
+ mFade = fade;
215
+ fade.start();
216
+ }
217
+
218
+ // Shows the image on its own (the fade ends, another starts, or the view
219
+ // is detached).
220
+ private void endFade() {
221
+ ValueAnimator fade = mFade;
222
+ mFade = null;
223
+ if (fade != null) fade.cancel();
224
+ if (mFadeFrom != null) {
225
+ mFadeFrom.recycle();
226
+ mFadeFrom = null;
227
+ }
228
+ mFadeProgress = 1f;
229
+ invalidate();
230
+ }
231
+
232
+ @SuppressWarnings("deprecation")
233
+ @Override
234
+ protected void onDraw(Canvas canvas) {
235
+ if (mFade == null) {
236
+ super.onDraw(canvas);
237
+ return;
238
+ }
239
+ int width = getWidth();
240
+ int height = getHeight();
241
+ int alpha = Math.round(255 * mFadeProgress);
242
+ if (mFadeFrom == null) {
243
+ // Fading in over nothing: one layer.
244
+ int count = canvas.saveLayerAlpha(0, 0, width, height, alpha, Canvas.ALL_SAVE_FLAG);
245
+ super.onDraw(canvas);
246
+ canvas.restoreToCount(count);
247
+ return;
248
+ }
249
+ // Canvas.ALL_SAVE_FLAG: the saveLayer without flags is API 21+.
250
+ int count = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
251
+ mFadeFromPaint.setAlpha(255 - alpha);
252
+ canvas.drawBitmap(mFadeFrom, 0, 0, mFadeFromPaint);
253
+ mFadeInPaint.setAlpha(alpha);
254
+ canvas.saveLayer(0, 0, width, height, mFadeInPaint, Canvas.ALL_SAVE_FLAG);
255
+ super.onDraw(canvas);
256
+ canvas.restoreToCount(count);
257
+ }
258
+
259
+ @Override
260
+ protected void onDetachedFromWindow() {
261
+ endFade();
262
+ super.onDetachedFromWindow();
263
+ }
264
+
91
265
  public void setPaused(boolean paused) {
92
266
  if (paused == mPaused) return;
93
267
  mPaused = paused;
@@ -202,19 +376,50 @@ class FastImageViewWithUrl extends AppCompatImageView {
202
376
  mNeedsReload = true;
203
377
  }
204
378
 
379
+ // blurRadius, in pixels (0 is no blur). Part of the request (see
380
+ // reblur for a change).
381
+ private float mBlurRadius = 0;
382
+ private boolean mBlurChanged = false;
383
+
384
+ public void setBlurRadius(float blurRadius) {
385
+ if (blurRadius == mBlurRadius) return;
386
+ mBlurRadius = blurRadius;
387
+ mBlurChanged = true;
388
+ }
389
+
205
390
  // The scale type's options (what into() would apply), or none when
206
- // pixelated.
391
+ // pixelated, and the blur. A blurred animated image shows its first frame,
392
+ // as on iOS.
207
393
  private RequestOptions renderingOptions(Object model) {
394
+ FastImageBlur blur = mBlurRadius > 0 ? new FastImageBlur(mBlurRadius, getScaleType()) : null;
395
+ RequestOptions options;
208
396
  if (mPixelated) {
209
397
  // Decoded as it is, then scaled by the view without filtering (see
210
398
  // OwnGifTarget).
211
- return new RequestOptions()
399
+ options = new RequestOptions()
212
400
  .downsample(new FastImageSourceSize.Capture(DownsampleStrategy.NONE, String.valueOf(model)))
213
401
  .dontTransform();
402
+ if (blur != null) options = options.optionalTransform(blur);
403
+ } else {
404
+ options = FastImageSourceSize.scaleTypeOptions(
405
+ getScaleType(), FastImageSourceSize.capture(getScaleType(), model), blur);
214
406
  }
215
- return FastImageSourceSize.scaleTypeOptions(getScaleType(), FastImageSourceSize.capture(getScaleType(), model));
407
+ return blur != null ? options.dontAnimate() : options;
216
408
  }
217
409
 
410
+ // The request the loading one was made from, before renderingOptions, and
411
+ // its model, to make it again with another blur (see reblur).
412
+ @Nullable
413
+ private RequestBuilder<Drawable> mBaseRequest;
414
+ @Nullable
415
+ private Object mModel;
416
+ // Whether the loading image has loaded or failed.
417
+ private boolean mLoadEnded = false;
418
+ // Set by reblur: the next load is the same one again, so it doesn't send
419
+ // onLoadStart (it sends its other events, as the load it replaces would
420
+ // have).
421
+ private boolean mRestarting = false;
422
+
218
423
  // The request for the image the view shows once it has loaded, and the one
219
424
  // loading. A new source starts with the shown one as a thumbnail, from the
220
425
  // cache only, so the image stays until the new one has loaded instead of
@@ -246,15 +451,17 @@ class FastImageViewWithUrl extends AppCompatImageView {
246
451
  mRecyclingKey = recyclingKey;
247
452
  if (changed) {
248
453
  // No thumbnail of the current image, so Glide clears the view to
249
- // defaultSource (or nothing) while the next one loads. Reload even
250
- // if the source is the same.
454
+ // defaultSource (or nothing) while the next one loads, nor a fade
455
+ // from it. Reload even if the source is the same.
251
456
  mShownRequest = null;
457
+ endFade();
252
458
  mNeedsReload = true;
253
459
  }
254
460
  }
255
461
 
256
462
  // The loading image loaded (FastImageRequestListener).
257
463
  void onImageLoaded() {
464
+ mLoadEnded = true;
258
465
  mShownRequest = mLoadingRequest;
259
466
  mShownWidth = mLoadingWidth;
260
467
  mShownHeight = mLoadingHeight;
@@ -267,6 +474,13 @@ class FastImageViewWithUrl extends AppCompatImageView {
267
474
  @Override
268
475
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
269
476
  super.onSizeChanged(w, h, oldw, oldh);
477
+ // During a fade, what the view showed was drawn at the old size: the
478
+ // image fades in over nothing instead.
479
+ if (mFadeFrom != null && (w != mFadeFrom.getWidth() || h != mFadeFrom.getHeight())) {
480
+ mFadeFrom.recycle();
481
+ mFadeFrom = null;
482
+ invalidate();
483
+ }
270
484
  reloadIfResized();
271
485
  }
272
486
 
@@ -310,6 +524,34 @@ class FastImageViewWithUrl extends AppCompatImageView {
310
524
  into(shown, builder, meanwhile);
311
525
  }
312
526
 
527
+ // blurRadius changed, and nothing else that needs a reload: the image
528
+ // that's showing, blurred again, with no events, as reloadForSize does: it's
529
+ // decoded again from the cache and blurred on Glide's threads, and the view
530
+ // keeps the current image until then (or if that fails). A load that
531
+ // hasn't finished starts again with the new blur, without a second
532
+ // onLoadStart. Nothing loaded (defaultSource, which isn't blurred, or
533
+ // nothing): the next load has the new blur.
534
+ @SuppressLint("CheckResult")
535
+ private void reblur() {
536
+ if (requestManager == null || mBaseRequest == null) return;
537
+ RequestBuilder<Drawable> shown = mShownRequest;
538
+ if (shown != null && shown == mLoadingRequest) {
539
+ RequestBuilder<Drawable> request = mBaseRequest.clone().apply(renderingOptions(mModel));
540
+ RequestBuilder<Drawable> current = fromCache(shown);
541
+ Drawable meanwhile = shownCopy(shown);
542
+ mLoadCount++;
543
+ clearView(requestManager);
544
+ RequestBuilder<Drawable> builder = request.clone()
545
+ .error(current.clone())
546
+ .listener(new FastImageRequestListener(null, null, true, false));
547
+ if (meanwhile == null) builder = builder.thumbnail(current);
548
+ into(request, builder, meanwhile);
549
+ } else if (mLoadingRequest != null && !mLoadEnded) {
550
+ mRestarting = true;
551
+ mNeedsReload = true;
552
+ }
553
+ }
554
+
313
555
  // A copy of the image showing, for the view to show while the next one
314
556
  // loads, when it isn't in Glide's memory cache (source.memoryCache false):
315
557
  // a thumbnail of it would come from the disk, after the view has cleared,
@@ -335,7 +577,9 @@ class FastImageViewWithUrl extends AppCompatImageView {
335
577
  // it was loaded at: the key it's in Glide's memory cache under.
336
578
  @SuppressLint("CheckResult")
337
579
  private RequestBuilder<Drawable> fromCache(RequestBuilder<Drawable> shown) {
338
- RequestBuilder<Drawable> request = shown.clone().onlyRetrieveFromCache(true);
580
+ // Never fades: it's the image the view showed.
581
+ RequestBuilder<Drawable> request = shown.clone().onlyRetrieveFromCache(true)
582
+ .transition(GenericTransitionOptions.<Drawable>withNoTransition());
339
583
  if (mShownWidth > 0 && mShownHeight > 0) request = request.override(mShownWidth, mShownHeight);
340
584
  return request;
341
585
  }
@@ -346,6 +590,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
346
590
  private void into(RequestBuilder<Drawable> request, RequestBuilder<Drawable> builder, @Nullable Drawable meanwhile) {
347
591
  final int load = mLoadCount;
348
592
  mLoadingRequest = request;
593
+ mLoadEnded = false;
349
594
  mLoadingWidth = 0;
350
595
  mLoadingHeight = 0;
351
596
  OwnGifTarget target = new OwnGifTarget(meanwhile);
@@ -360,9 +605,16 @@ class FastImageViewWithUrl extends AppCompatImageView {
360
605
  builder.into(target);
361
606
  }
362
607
 
608
+ // Loading the image that's showing again (at a new size, or with another
609
+ // blur) failed: the view keeps showing it (FastImageRequestListener).
610
+ void onQuietLoadFailed() {
611
+ mLoadEnded = true;
612
+ }
613
+
363
614
  // The loading image failed (FastImageRequestListener). Glide shows
364
615
  // defaultSource then, but not over a thumbnail: show it here, as iOS does.
365
616
  void onImageFailed(boolean hadThumbnail) {
617
+ mLoadEnded = true;
366
618
  mShownRequest = null;
367
619
  if (!hadThumbnail) return;
368
620
  final int load = mLoadCount;
@@ -380,15 +632,26 @@ class FastImageViewWithUrl extends AppCompatImageView {
380
632
  @Nonnull FastImageViewManager manager,
381
633
  @Nullable RequestManager requestManager,
382
634
  @Nonnull Map<String, List<FastImageViewWithUrl>> viewsForUrlsMap) {
635
+ if (mBlurChanged && !mNeedsReload) {
636
+ mBlurChanged = false;
637
+ reblur();
638
+ }
383
639
  if (!mNeedsReload)
384
640
  return;
385
641
  // Only reload for changes that affect the request (source,
386
642
  // defaultSource, resizeMode), not for every prop update.
387
643
  mNeedsReload = false;
644
+ mBlurChanged = false;
645
+ boolean restarting = mRestarting;
646
+ mRestarting = false;
388
647
  mLoadCount++;
648
+ // A fade in progress carries on, as on iOS: into the next image if it
649
+ // shows at once, and a next image that fades starts from it.
389
650
  RequestBuilder<Drawable> shownRequest = mShownRequest;
390
651
  mShownRequest = null;
391
652
  mLoadingRequest = null;
653
+ mBaseRequest = null;
654
+ mModel = null;
392
655
 
393
656
  // Nothing to show.
394
657
  if (mSource == null && mDefaultSource == null) {
@@ -453,7 +716,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
453
716
  }
454
717
  }
455
718
 
456
- if (imageSource != null) {
719
+ if (imageSource != null && !restarting) {
457
720
  // This is an orphan even without a load/loadend when only loading a placeholder
458
721
  FastImageEvents.send(this, FastImageViewManager.REACT_ON_LOAD_START_EVENT);
459
722
  }
@@ -461,7 +724,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
461
724
  if (requestManager != null) {
462
725
  // Records the image's own size when Glide decodes it, for onLoad.
463
726
  Object model = imageSource == null ? null : imageSource.getSourceForLoad();
464
- RequestBuilder<Drawable> builder =
727
+ RequestBuilder<Drawable> base =
465
728
  requestManager
466
729
  // This will make this work for remote and local images. e.g.
467
730
  // - file:///
@@ -473,14 +736,26 @@ class FastImageViewWithUrl extends AppCompatImageView {
473
736
  .apply(FastImageViewConverter
474
737
  .getOptions(getContext(), imageSource, mSource)
475
738
  .placeholder(mDefaultSource) // show until loaded
476
- .fallback(mDefaultSource)) // null will not be treated as error
477
- // What into() would apply for the scale type, with
478
- // the size capture, for imageRendering.
479
- .apply(renderingOptions(model));
739
+ .fallback(mDefaultSource)); // null will not be treated as error
740
+ mBaseRequest = model == null ? null : base;
741
+ mModel = model;
742
+ // What into() would apply for the scale type, with the size
743
+ // capture, for imageRendering, and the blur.
744
+ RequestBuilder<Drawable> builder = base.clone().apply(renderingOptions(model));
480
745
  RequestBuilder<Drawable> request = builder.clone();
481
746
 
482
747
  if (model == null) meanwhile = null;
483
748
  boolean thumbnail = shownRequest != null && model != null && meanwhile == null;
749
+ // After the clone: loading the image that's showing again (at a
750
+ // new size, or with another blur) doesn't fade it in again. Over a
751
+ // loaded image (a new source) only with betweenImages: otherwise it
752
+ // replaces it at once, as Glide, Coil and Fresco fade an image in
753
+ // from nothing and never between images.
754
+ boolean replacing = shownRequest != null && model != null;
755
+ if (mTransitionDuration > 0 && (!replacing || mTransitionBetweenImages)) {
756
+ builder = builder.transition(DrawableTransitionOptions.with(
757
+ new FadeFactory(mTransitionDuration, mTransitionSkipOnCacheHit)));
758
+ }
484
759
  if (thumbnail) {
485
760
  builder = builder.thumbnail(fromCache(shownRequest));
486
761
  }
package/dist/index.cjs.js CHANGED
@@ -91,11 +91,24 @@ function withProgress(onProgress) {
91
91
  onProgress(event);
92
92
  });
93
93
  }
94
+ const DEFAULT_FADE_MS = react_native.Platform.OS === "ios" ? 250 : 300;
95
+ function transitionProps(transition) {
96
+ var _transition$duration;
97
+ const { duration, betweenImages, skipOnCacheHit } = typeof transition === "number" ? { duration: transition } : typeof transition === "object" && transition ? {
98
+ ...transition,
99
+ duration: (_transition$duration = transition.duration) !== null && _transition$duration !== void 0 ? _transition$duration : DEFAULT_FADE_MS
100
+ } : { duration: transition ? DEFAULT_FADE_MS : 0 };
101
+ return {
102
+ transitionDuration: typeof duration === "number" && duration > 0 ? duration : 0,
103
+ transitionBetweenImages: !!betweenImages,
104
+ transitionSkipOnCacheHit: skipOnCacheHit || "memory"
105
+ };
106
+ }
94
107
  function withoutCache(source) {
95
108
  const { cache: _cache, ...rest } = source || {};
96
109
  return rest;
97
110
  }
98
- function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", loop, forwardedRef, onLayout, pointerEvents, ...viewProps }) {
111
+ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", loop, transition, forwardedRef, onLayout, pointerEvents, ...viewProps }) {
99
112
  const { onClick, ...props } = viewProps;
100
113
  const fallbackResult = (0, react.useRef)(void 0);
101
114
  const wrapperProps = {
@@ -158,6 +171,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
158
171
  ...imageProps,
159
172
  tintColor: resolvedTintColor,
160
173
  loopCount: loopCount(loop),
174
+ ...transitionProps(transition),
161
175
  style: react_native.StyleSheet.absoluteFill,
162
176
  source: resolvedSource,
163
177
  defaultSource: resolvedDefaultSource,
@@ -72,6 +72,12 @@ export type CachePathResult =
72
72
  | {| ok: true, path: string |}
73
73
  | {| ok: false, error: string |}
74
74
 
75
+ export type Transition = $ReadOnly<{|
76
+ duration?: ?number,
77
+ betweenImages?: ?boolean,
78
+ skipOnCacheHit?: ?('none' | 'memory' | 'all'),
79
+ |}>
80
+
75
81
  export type PreloadSuccess = {|
76
82
  uri: string,
77
83
  ok: true,
@@ -117,7 +123,9 @@ export type FastImageProps = $ReadOnly<{|
117
123
  loop?: ?(boolean | number),
118
124
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
119
125
  paused?: ?boolean,
126
+ transition?: ?(boolean | number | Transition),
120
127
  downsample?: ?boolean,
128
+ blurRadius?: ?number,
121
129
  testID?: ?string,
122
130
  |}>
123
131
 
package/dist/index.d.ts CHANGED
@@ -42,6 +42,35 @@ export type Source = {
42
42
  */
43
43
  memoryCache?: boolean;
44
44
  };
45
+ export type Transition = {
46
+ /**
47
+ * How long the fade takes, in milliseconds; 0 means no fade. Defaults to
48
+ * the platform's usual length: 300 ms on Android, 250 ms on iOS.
49
+ */
50
+ duration?: number;
51
+ /**
52
+ * Also fades between images: a new `source` replacing an image that's
53
+ * showing cross-dissolves from it. Otherwise only an image that appears
54
+ * over nothing (or over `defaultSource`) fades in, and a new source
55
+ * replaces the image showing at once, once it has loaded.
56
+ * @default false
57
+ */
58
+ betweenImages?: boolean;
59
+ /**
60
+ * Skips the fade for an image from a cache, so images already loaded
61
+ * show at once, e.g. in a list scrolled back up or a reused row.
62
+ *
63
+ * - `'memory'`: skips it for images from the memory cache.
64
+ * - `'all'`: skips it for images from the memory or disk cache too, so
65
+ * images that download fade in, and local files and bundled images
66
+ * usually only the first time (the disk cache usually keeps them too).
67
+ * - `'none'`: always fades.
68
+ *
69
+ * Downloads, local files and bundled images (`require()`) fade in.
70
+ * @default 'memory'
71
+ */
72
+ skipOnCacheHit?: 'none' | 'memory' | 'all' | null;
73
+ };
45
74
  export interface OnLoadEvent {
46
75
  nativeEvent: {
47
76
  width: number;
@@ -135,6 +164,13 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
135
164
  * its own, so pausing one doesn't pause others showing the same file.
136
165
  */
137
166
  paused?: boolean;
167
+ /**
168
+ * Fades the image in when it loads: `true` for the platform's usual fade,
169
+ * a duration in milliseconds, or a `Transition` (default: no fade). An
170
+ * image fades in when it appears over nothing (see `betweenImages`),
171
+ * unless it's from the memory cache (see `skipOnCacheHit`).
172
+ */
173
+ transition?: boolean | number | Transition | null;
138
174
  /**
139
175
  * iOS only. Decodes a large image at about the size it's shown at, so it
140
176
  * takes much less memory. For images much larger than their views that
@@ -142,6 +178,14 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
142
178
  * or with an image CDN instead). See the README.
143
179
  */
144
180
  downsample?: boolean;
181
+ /**
182
+ * Blurs the image by this radius, in points, like React Native's Image
183
+ * (the same radius looks about the same on both platforms). `0` (default)
184
+ * is no blur. For still images or occasional changes, not for animating.
185
+ * Only the loaded image is blurred, not `defaultSource`. An animated image
186
+ * shows its first frame, blurred. See the README.
187
+ */
188
+ blurRadius?: number;
145
189
  onLoadStart?(): void;
146
190
  onProgress?(event: OnProgressEvent): void;
147
191
  onLoad?(event: OnLoadEvent): void;
package/dist/index.js CHANGED
@@ -64,11 +64,24 @@ function withProgress(onProgress) {
64
64
  onProgress(event);
65
65
  });
66
66
  }
67
+ const DEFAULT_FADE_MS = Platform.OS === "ios" ? 250 : 300;
68
+ function transitionProps(transition) {
69
+ var _transition$duration;
70
+ const { duration, betweenImages, skipOnCacheHit } = typeof transition === "number" ? { duration: transition } : typeof transition === "object" && transition ? {
71
+ ...transition,
72
+ duration: (_transition$duration = transition.duration) !== null && _transition$duration !== void 0 ? _transition$duration : DEFAULT_FADE_MS
73
+ } : { duration: transition ? DEFAULT_FADE_MS : 0 };
74
+ return {
75
+ transitionDuration: typeof duration === "number" && duration > 0 ? duration : 0,
76
+ transitionBetweenImages: !!betweenImages,
77
+ transitionSkipOnCacheHit: skipOnCacheHit || "memory"
78
+ };
79
+ }
67
80
  function withoutCache(source) {
68
81
  const { cache: _cache, ...rest } = source || {};
69
82
  return rest;
70
83
  }
71
- function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", loop, forwardedRef, onLayout, pointerEvents, ...viewProps }) {
84
+ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", loop, transition, forwardedRef, onLayout, pointerEvents, ...viewProps }) {
72
85
  const { onClick, ...props } = viewProps;
73
86
  const fallbackResult = useRef(void 0);
74
87
  const wrapperProps = {
@@ -131,6 +144,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
131
144
  ...imageProps,
132
145
  tintColor: resolvedTintColor,
133
146
  loopCount: loopCount(loop),
147
+ ...transitionProps(transition),
134
148
  style: StyleSheet.absoluteFill,
135
149
  source: resolvedSource,
136
150
  defaultSource: resolvedDefaultSource,
@@ -72,6 +72,12 @@ export type CachePathResult =
72
72
  | {| ok: true, path: string |}
73
73
  | {| ok: false, error: string |}
74
74
 
75
+ export type Transition = $ReadOnly<{|
76
+ duration?: ?number,
77
+ betweenImages?: ?boolean,
78
+ skipOnCacheHit?: ?('none' | 'memory' | 'all'),
79
+ |}>
80
+
75
81
  export type PreloadSuccess = {|
76
82
  uri: string,
77
83
  ok: true,
@@ -117,7 +123,9 @@ export type FastImageProps = $ReadOnly<{|
117
123
  loop?: ?(boolean | number),
118
124
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
119
125
  paused?: ?boolean,
126
+ transition?: ?(boolean | number | Transition),
120
127
  downsample?: ?boolean,
128
+ blurRadius?: ?number,
121
129
  testID?: ?string,
122
130
  |}>
123
131
 
@@ -30,8 +30,16 @@
30
30
  @property (nonatomic, copy) NSString *imageRendering;
31
31
  // Pauses animated images on the frame they're showing.
32
32
  @property (nonatomic, assign) BOOL paused;
33
+ // The `transition` prop: how long a loaded image takes to fade in, in
34
+ // milliseconds (0 for no fade), whether it also fades over a loaded image
35
+ // (a new source), and which cache hits show at once (none, memory or all).
36
+ @property (nonatomic, assign) double transitionDuration;
37
+ @property (nonatomic, assign) BOOL transitionBetweenImages;
38
+ @property (nonatomic, copy) NSString *transitionSkipOnCacheHit;
33
39
  // Decodes images at about the view's size instead of at full size.
34
40
  @property (nonatomic, assign) BOOL downsample;
41
+ // Blurs the loaded image by this radius, in points (0 is no blur).
42
+ @property (nonatomic, assign) CGFloat blurRadius;
35
43
 
36
44
  @end
37
45