react-native-fast-image 8.21.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 +21 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +15 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +199 -3
- package/dist/index.cjs.js +15 -1
- package/dist/index.cjs.js.flow +7 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +15 -1
- package/dist/index.js.flow +7 -0
- package/ios/FastImage/FFFastImageView.h +6 -0
- package/ios/FastImage/FFFastImageView.m +107 -7
- package/ios/FastImage/FFFastImageViewManager.m +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -212,6 +212,27 @@ Pauses an animated image (GIF, and animated WebP on iOS) on the frame it's showi
|
|
|
212
212
|
|
|
213
213
|
---
|
|
214
214
|
|
|
215
|
+
### `transition?: boolean | number | Transition`
|
|
216
|
+
|
|
217
|
+
Fades the image in when it loads. Off by default. `true` uses the platform's usual fade, a number is the duration in milliseconds, or pass an object:
|
|
218
|
+
|
|
219
|
+
- `duration`: milliseconds; 0 is no fade. Defaults to the platform's usual length: 300 ms on Android, 250 ms on iOS.
|
|
220
|
+
- `betweenImages` (default `false`): also fade between images. By default an image fades in when it appears over nothing (or over `defaultSource`), and a new `source` replaces the image that's showing at once, once it has loaded. With `true`, the new image cross-dissolves from the one showing (for a gallery or an avatar that changes, say).
|
|
221
|
+
- `skipOnCacheHit`: which images show at once, without the fade:
|
|
222
|
+
- `'memory'` (default): images from the memory cache, so a list scrolled back up shows images it already loaded at once. Images from the disk cache fade in.
|
|
223
|
+
- `'all'`: images from the memory or disk cache: images that download fade in, and local files and bundled images usually only the first time they load (the disk cache usually keeps them too).
|
|
224
|
+
- `'none'`: every image fades in.
|
|
225
|
+
|
|
226
|
+
Downloads, local files (`file://`, `content://`) and bundled images (`require()`) fade in. In lists that reuse views (e.g. FlashList), set `recyclingKey`, so a reused view starts empty and its image fades in, instead of showing the previous item's image until it loads.
|
|
227
|
+
|
|
228
|
+
```jsx
|
|
229
|
+
<FastImage source={{ uri }} transition />
|
|
230
|
+
<FastImage source={{ uri }} transition={500} />
|
|
231
|
+
<FastImage source={{ uri }} transition={{ betweenImages: true, skipOnCacheHit: 'none' }} />
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
215
236
|
### `downsample?: boolean`
|
|
216
237
|
|
|
217
238
|
iOS only. Decodes a large image at about the size it's shown at, instead of at full size, so it takes much less memory.
|
|
@@ -105,6 +105,21 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
105
105
|
view.setBlurRadius(PixelUtil.toPixelFromDIP(blurRadius));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
@ReactProp(name = "transitionDuration")
|
|
109
|
+
public void setTransitionDuration(FastImageViewWithUrl view, int transitionDuration) {
|
|
110
|
+
view.setTransitionDuration(transitionDuration);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@ReactProp(name = "transitionBetweenImages")
|
|
114
|
+
public void setTransitionBetweenImages(FastImageViewWithUrl view, boolean betweenImages) {
|
|
115
|
+
view.setTransitionBetweenImages(betweenImages);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@ReactProp(name = "transitionSkipOnCacheHit")
|
|
119
|
+
public void setTransitionSkipOnCacheHit(FastImageViewWithUrl view, @Nullable String skipOnCacheHit) {
|
|
120
|
+
view.setTransitionSkipOnCacheHit(skipOnCacheHit);
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
@ReactProp(name = "paused")
|
|
109
124
|
public void setPaused(FastImageViewWithUrl view, boolean paused) {
|
|
110
125
|
view.setPaused(paused);
|
|
@@ -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;
|
|
@@ -277,9 +451,10 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
277
451
|
mRecyclingKey = recyclingKey;
|
|
278
452
|
if (changed) {
|
|
279
453
|
// No thumbnail of the current image, so Glide clears the view to
|
|
280
|
-
// defaultSource (or nothing) while the next one loads
|
|
281
|
-
// 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.
|
|
282
456
|
mShownRequest = null;
|
|
457
|
+
endFade();
|
|
283
458
|
mNeedsReload = true;
|
|
284
459
|
}
|
|
285
460
|
}
|
|
@@ -299,6 +474,13 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
299
474
|
@Override
|
|
300
475
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
301
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
|
+
}
|
|
302
484
|
reloadIfResized();
|
|
303
485
|
}
|
|
304
486
|
|
|
@@ -395,7 +577,9 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
395
577
|
// it was loaded at: the key it's in Glide's memory cache under.
|
|
396
578
|
@SuppressLint("CheckResult")
|
|
397
579
|
private RequestBuilder<Drawable> fromCache(RequestBuilder<Drawable> shown) {
|
|
398
|
-
|
|
580
|
+
// Never fades: it's the image the view showed.
|
|
581
|
+
RequestBuilder<Drawable> request = shown.clone().onlyRetrieveFromCache(true)
|
|
582
|
+
.transition(GenericTransitionOptions.<Drawable>withNoTransition());
|
|
399
583
|
if (mShownWidth > 0 && mShownHeight > 0) request = request.override(mShownWidth, mShownHeight);
|
|
400
584
|
return request;
|
|
401
585
|
}
|
|
@@ -461,6 +645,8 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
461
645
|
boolean restarting = mRestarting;
|
|
462
646
|
mRestarting = false;
|
|
463
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.
|
|
464
650
|
RequestBuilder<Drawable> shownRequest = mShownRequest;
|
|
465
651
|
mShownRequest = null;
|
|
466
652
|
mLoadingRequest = null;
|
|
@@ -560,6 +746,16 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
560
746
|
|
|
561
747
|
if (model == null) meanwhile = null;
|
|
562
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
|
+
}
|
|
563
759
|
if (thumbnail) {
|
|
564
760
|
builder = builder.thumbnail(fromCache(shownRequest));
|
|
565
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,6 +123,7 @@ 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,
|
|
121
128
|
blurRadius?: ?number,
|
|
122
129
|
testID?: ?string,
|
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
|
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,6 +123,7 @@ 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,
|
|
121
128
|
blurRadius?: ?number,
|
|
122
129
|
testID?: ?string,
|
|
@@ -30,6 +30,12 @@
|
|
|
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;
|
|
35
41
|
// Blurs the loaded image by this radius, in points (0 is no blur).
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
@property(nonatomic, assign) CGSize blurredForSize;
|
|
27
27
|
// The blurred image showing (untinted), so a tint change can re-show it.
|
|
28
28
|
@property(nonatomic, strong) UIImage* blurredImage;
|
|
29
|
+
// The image being blurred fades in once it's blurred (transition).
|
|
30
|
+
@property(nonatomic, assign) BOOL fadesBlurredImage;
|
|
31
|
+
// The image being blurred is the first loaded one the view shows: until it's
|
|
32
|
+
// blurred, the view shows no loaded image (nothing, or defaultSource).
|
|
33
|
+
@property(nonatomic, assign) BOOL blursOverNothing;
|
|
29
34
|
// Whether the current load was already restarted after the app came back
|
|
30
35
|
// from the background (see downloadImage:).
|
|
31
36
|
@property(nonatomic, assign) BOOL retriedAfterBackground;
|
|
@@ -117,6 +122,77 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
// Whether a loaded image fades in (the `transition` prop). Over a loaded
|
|
126
|
+
// image (a new source) only with betweenImages: otherwise it replaces it at
|
|
127
|
+
// once, as Android's image libraries fade an image in from nothing and never
|
|
128
|
+
// between images. Then as SDWebImage's transitions and those libraries
|
|
129
|
+
// decide: from the memory cache it shows at once (skipOnCacheHit 'memory',
|
|
130
|
+
// the default), and from the disk cache too with 'all'. Downloads, local
|
|
131
|
+
// files and bundled images fade.
|
|
132
|
+
- (BOOL) fadesImageFromCache: (SDImageCacheType)cacheType {
|
|
133
|
+
if (self.transitionDuration <= 0 || ([self displaysLoadedImage] && !self.transitionBetweenImages)) {
|
|
134
|
+
return NO;
|
|
135
|
+
}
|
|
136
|
+
NSString* skip = self.transitionSkipOnCacheHit;
|
|
137
|
+
if ([skip isEqualToString: @"none"]) {
|
|
138
|
+
return YES;
|
|
139
|
+
}
|
|
140
|
+
if (cacheType == SDImageCacheTypeMemory) {
|
|
141
|
+
return NO;
|
|
142
|
+
}
|
|
143
|
+
// SDWebImage also keeps local files and bundled images in its disk cache,
|
|
144
|
+
// so with 'all' they usually only fade the first time.
|
|
145
|
+
return !(cacheType == SDImageCacheTypeDisk && [skip isEqualToString: @"all"]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Whether the view shows a loaded image now: a loaded image that's still
|
|
149
|
+
// being blurred isn't showing yet.
|
|
150
|
+
- (BOOL) displaysLoadedImage {
|
|
151
|
+
return self.showsLoadedImage && !self.blursOverNothing;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Shows a loaded image, fading it in over what the view shows if `fade`. A
|
|
155
|
+
// blurred image shows, and fades in, once it's blurred.
|
|
156
|
+
- (void) showLoadedImage: (UIImage*)image fade: (BOOL)fade {
|
|
157
|
+
if ([self blurs: image]) {
|
|
158
|
+
self.blursOverNothing = ![self displaysLoadedImage];
|
|
159
|
+
self.fadesBlurredImage = fade;
|
|
160
|
+
self.image = image;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
self.blursOverNothing = NO;
|
|
164
|
+
self.fadesBlurredImage = NO;
|
|
165
|
+
if (fade) {
|
|
166
|
+
[self fadeIn: ^{
|
|
167
|
+
self.image = image;
|
|
168
|
+
}];
|
|
169
|
+
} else {
|
|
170
|
+
self.image = image;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Makes the change (showing another image), fading it in over what the view
|
|
175
|
+
// showed.
|
|
176
|
+
- (void) fadeIn: (void (^)(void))change {
|
|
177
|
+
if (!super.image || !self.layer.presentationLayer) {
|
|
178
|
+
// Nothing to cross-dissolve from: the view shows no image, or isn't
|
|
179
|
+
// on screen yet (e.g. a new view whose image loads as it mounts),
|
|
180
|
+
// where a cross-dissolve has nothing drawn to start from and shows
|
|
181
|
+
// the image at once. Fade the view in instead.
|
|
182
|
+
change();
|
|
183
|
+
CABasicAnimation* fadeIn = [CABasicAnimation animationWithKeyPath: @"opacity"];
|
|
184
|
+
fadeIn.fromValue = @0;
|
|
185
|
+
fadeIn.duration = self.transitionDuration / 1000;
|
|
186
|
+
[self.layer addAnimation: fadeIn forKey: @"FFFFadeIn"];
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
[UIView transitionWithView: self
|
|
190
|
+
duration: self.transitionDuration / 1000
|
|
191
|
+
options: UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowUserInteraction
|
|
192
|
+
animations: change
|
|
193
|
+
completion: nil];
|
|
194
|
+
}
|
|
195
|
+
|
|
120
196
|
- (void) setPaused: (BOOL)paused {
|
|
121
197
|
if (_paused == paused) {
|
|
122
198
|
return;
|
|
@@ -246,7 +322,17 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
246
322
|
}
|
|
247
323
|
self.originalImage = image && self.imageColor ? image : nil;
|
|
248
324
|
self.blurredImage = nil;
|
|
249
|
-
|
|
325
|
+
self.blursOverNothing = NO;
|
|
326
|
+
// The blur was removed before it was ready: the fade it was waiting for
|
|
327
|
+
// happens now.
|
|
328
|
+
if (self.fadesBlurredImage) {
|
|
329
|
+
self.fadesBlurredImage = NO;
|
|
330
|
+
[self fadeIn: ^{
|
|
331
|
+
[self showImage: image];
|
|
332
|
+
}];
|
|
333
|
+
} else {
|
|
334
|
+
[self showImage: image];
|
|
335
|
+
}
|
|
250
336
|
}
|
|
251
337
|
|
|
252
338
|
// Shows the image, tinted as React Native's Image does it: UIKit draws a
|
|
@@ -336,7 +422,15 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
336
422
|
- (void) showBlurredImage: (UIImage*)image count: (NSUInteger)count {
|
|
337
423
|
if (self.imageCount == count) {
|
|
338
424
|
self.blurredImage = image;
|
|
339
|
-
|
|
425
|
+
self.blursOverNothing = NO;
|
|
426
|
+
if (self.fadesBlurredImage) {
|
|
427
|
+
self.fadesBlurredImage = NO;
|
|
428
|
+
[self fadeIn: ^{
|
|
429
|
+
[self showImage: image];
|
|
430
|
+
}];
|
|
431
|
+
} else {
|
|
432
|
+
[self showImage: image];
|
|
433
|
+
}
|
|
340
434
|
}
|
|
341
435
|
}
|
|
342
436
|
|
|
@@ -513,8 +607,9 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
513
607
|
if (changed) {
|
|
514
608
|
// The view shows other content now: don't keep the current image
|
|
515
609
|
// while the next one loads (reloadImage clears it), even if the
|
|
516
|
-
// source is the same.
|
|
610
|
+
// source is the same, nor a fade from it.
|
|
517
611
|
self.showsLoadedImage = NO;
|
|
612
|
+
[self.layer removeAllAnimations];
|
|
518
613
|
_needsReload = YES;
|
|
519
614
|
}
|
|
520
615
|
}
|
|
@@ -646,7 +741,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
646
741
|
}
|
|
647
742
|
return;
|
|
648
743
|
}
|
|
649
|
-
[self
|
|
744
|
+
[self showLoadedImage: image fade: [self fadesImageFromCache: SDImageCacheTypeNone]];
|
|
650
745
|
if (self.onFastImageProgress) {
|
|
651
746
|
self.onFastImageProgress(@{
|
|
652
747
|
@"loaded": @(1),
|
|
@@ -799,13 +894,18 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
799
894
|
// class to SDAnimatedImage (loadContext sets it). Only while this is the
|
|
800
895
|
// current load: SDWebImage sets the placeholder when a load it cancelled
|
|
801
896
|
// completes, which could replace a newer image.
|
|
897
|
+
UIImage* placeholder = _defaultSource;
|
|
802
898
|
SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
|
|
803
|
-
if (weakSelf.loadCount
|
|
804
|
-
|
|
899
|
+
if (weakSelf.loadCount != load) {
|
|
900
|
+
return;
|
|
805
901
|
}
|
|
902
|
+
// The loaded image, not defaultSource (shown while it loads, or if it
|
|
903
|
+
// fails).
|
|
904
|
+
BOOL loaded = image && image != placeholder;
|
|
905
|
+
[weakSelf showLoadedImage: image fade: loaded && [weakSelf fadesImageFromCache: cacheType]];
|
|
806
906
|
};
|
|
807
907
|
[self sd_internalSetImageWithURL: url
|
|
808
|
-
placeholderImage:
|
|
908
|
+
placeholderImage: placeholder
|
|
809
909
|
options: options
|
|
810
910
|
context: context
|
|
811
911
|
setImageBlock: setImage
|
|
@@ -107,6 +107,9 @@ RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
|
|
|
107
107
|
RCT_EXPORT_VIEW_PROPERTY(loopCount, NSInteger)
|
|
108
108
|
RCT_EXPORT_VIEW_PROPERTY(imageRendering, NSString)
|
|
109
109
|
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
|
|
110
|
+
RCT_EXPORT_VIEW_PROPERTY(transitionDuration, double)
|
|
111
|
+
RCT_EXPORT_VIEW_PROPERTY(transitionBetweenImages, BOOL)
|
|
112
|
+
RCT_EXPORT_VIEW_PROPERTY(transitionSkipOnCacheHit, NSString)
|
|
110
113
|
RCT_EXPORT_VIEW_PROPERTY(downsample, BOOL)
|
|
111
114
|
RCT_EXPORT_VIEW_PROPERTY(blurRadius, CGFloat)
|
|
112
115
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
|