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.
- package/README.md +37 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageBlur.java +213 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +6 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +17 -2
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +21 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +287 -12
- package/dist/index.cjs.js +15 -1
- package/dist/index.cjs.js.flow +8 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +15 -1
- package/dist/index.js.flow +8 -0
- package/ios/FastImage/FFFastImageView.h +8 -0
- package/ios/FastImage/FFFastImageView.m +324 -17
- package/ios/FastImage/FFFastImageViewManager.m +4 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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,
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -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,
|
package/dist/index.js.flow
CHANGED
|
@@ -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
|
|