react-native-fast-image 8.8.1 → 8.10.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 +17 -2
- package/android/src/main/java/com/dylanvann/fastimage/FastImageGif.java +73 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +18 -3
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +5 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +1 -12
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +67 -2
- package/dist/index.cjs.js +8 -1
- package/dist/index.cjs.js.flow +8 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +8 -1
- package/dist/index.js.flow +8 -1
- package/ios/FastImage/FFFastImageView.h +7 -0
- package/ios/FastImage/FFFastImageView.m +47 -9
- package/ios/FastImage/FFFastImageViewManager.m +1 -10
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -173,6 +173,19 @@ For views that get reused for other content, such as rows in FlashList or recycl
|
|
|
173
173
|
|
|
174
174
|
---
|
|
175
175
|
|
|
176
|
+
### `loop?: boolean | number`
|
|
177
|
+
|
|
178
|
+
How many times an animated image (GIF, animated WebP) plays:
|
|
179
|
+
|
|
180
|
+
- Not set **(Default)** - As many times as the file says (like a browser).
|
|
181
|
+
- `true` - Loop forever.
|
|
182
|
+
- `false` - Play once.
|
|
183
|
+
- A number - Play that many times.
|
|
184
|
+
|
|
185
|
+
Changing it restarts the animation.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
176
189
|
### `onLoadStart?: () => void`
|
|
177
190
|
|
|
178
191
|
Called when the image starts to load.
|
|
@@ -195,9 +208,11 @@ e.g. `onLoad={e => console.log(e.nativeEvent.width, e.nativeEvent.height)}`
|
|
|
195
208
|
|
|
196
209
|
---
|
|
197
210
|
|
|
198
|
-
### `onError?: () => void`
|
|
211
|
+
### `onError?: (event) => void`
|
|
212
|
+
|
|
213
|
+
Called on an image fetching error, with a message describing it (e.g. the HTTP status code).
|
|
199
214
|
|
|
200
|
-
|
|
215
|
+
e.g. `onError={e => console.log(e.nativeEvent.error)}`
|
|
201
216
|
|
|
202
217
|
---
|
|
203
218
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Bitmap;
|
|
5
|
+
|
|
6
|
+
import androidx.annotation.Nullable;
|
|
7
|
+
|
|
8
|
+
import com.bumptech.glide.Glide;
|
|
9
|
+
import com.bumptech.glide.gifdecoder.GifDecoder;
|
|
10
|
+
import com.bumptech.glide.gifdecoder.GifHeader;
|
|
11
|
+
import com.bumptech.glide.gifdecoder.GifHeaderParser;
|
|
12
|
+
import com.bumptech.glide.gifdecoder.StandardGifDecoder;
|
|
13
|
+
import com.bumptech.glide.load.Transformation;
|
|
14
|
+
import com.bumptech.glide.load.engine.Resource;
|
|
15
|
+
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
|
|
16
|
+
import com.bumptech.glide.load.resource.UnitTransformation;
|
|
17
|
+
import com.bumptech.glide.load.resource.bitmap.BitmapResource;
|
|
18
|
+
import com.bumptech.glide.load.resource.gif.GifBitmapProvider;
|
|
19
|
+
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
|
20
|
+
|
|
21
|
+
import java.nio.ByteBuffer;
|
|
22
|
+
|
|
23
|
+
// Gives a view its own animation of a GIF. Glide gives every view that shows
|
|
24
|
+
// the same GIF (same url and size) a GifDrawable over one frame loader (it's
|
|
25
|
+
// in the cached drawable's constant state), so they animate as one: a view
|
|
26
|
+
// whose GIF stopped (the `loop` prop) keeps drawing another view's frames, and
|
|
27
|
+
// restarting one throws while another view plays it. A copy has its own
|
|
28
|
+
// decoder and frame loader over the same data, so each view animates on its
|
|
29
|
+
// own, as with React Native's Image and on iOS. Decoded like Glide's
|
|
30
|
+
// ByteBufferGifDecoder does, at the size the image was loaded at, and cropped
|
|
31
|
+
// or fitted with the same transformation.
|
|
32
|
+
final class FastImageGif {
|
|
33
|
+
private FastImageGif() {}
|
|
34
|
+
|
|
35
|
+
// Null if the GIF can't be decoded again (then the view shows Glide's).
|
|
36
|
+
@Nullable
|
|
37
|
+
static GifDrawable copy(Context context, GifDrawable gif, int width, int height) {
|
|
38
|
+
if (width <= 0 || height <= 0) return null;
|
|
39
|
+
ByteBuffer data = gif.getBuffer();
|
|
40
|
+
GifHeader header = new GifHeaderParser().setData(data).parseHeader();
|
|
41
|
+
if (header.getNumFrames() <= 0 || header.getStatus() != GifDecoder.STATUS_OK) return null;
|
|
42
|
+
Glide glide = Glide.get(context);
|
|
43
|
+
BitmapPool bitmapPool = glide.getBitmapPool();
|
|
44
|
+
GifDecoder decoder = new StandardGifDecoder(
|
|
45
|
+
new GifBitmapProvider(bitmapPool, glide.getArrayPool()),
|
|
46
|
+
header, gif.getBuffer(), sampleSize(header, width, height));
|
|
47
|
+
decoder.setDefaultBitmapConfig(Bitmap.Config.ARGB_8888);
|
|
48
|
+
decoder.advance();
|
|
49
|
+
Bitmap firstFrame = decoder.getNextFrame();
|
|
50
|
+
if (firstFrame == null) {
|
|
51
|
+
decoder.clear();
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
GifDrawable copy = new GifDrawable(
|
|
55
|
+
context, decoder, UnitTransformation.<Bitmap>get(), width, height, firstFrame);
|
|
56
|
+
Transformation<Bitmap> transformation = gif.getFrameTransformation();
|
|
57
|
+
if (transformation != null && !(transformation instanceof UnitTransformation)) {
|
|
58
|
+
// As GifDrawableTransformation does.
|
|
59
|
+
Resource<Bitmap> frame = new BitmapResource(firstFrame, bitmapPool);
|
|
60
|
+
Resource<Bitmap> transformed = transformation.transform(context, frame, width, height);
|
|
61
|
+
if (!frame.equals(transformed)) frame.recycle();
|
|
62
|
+
copy.setFrameTransformation(transformation, transformed.get());
|
|
63
|
+
}
|
|
64
|
+
return copy;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ByteBufferGifDecoder's.
|
|
68
|
+
private static int sampleSize(GifHeader header, int width, int height) {
|
|
69
|
+
int exact = Math.min(header.getHeight() / height, header.getWidth() / width);
|
|
70
|
+
int powerOfTwo = exact == 0 ? 0 : Integer.highestOneBit(exact);
|
|
71
|
+
return Math.max(1, powerOfTwo);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -41,11 +41,26 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
|
|
|
41
41
|
}
|
|
42
42
|
FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
|
|
43
43
|
view.onImageFailed(thumbnail);
|
|
44
|
-
|
|
44
|
+
WritableMap event = new WritableNativeMap();
|
|
45
|
+
event.putString("error", errorMessage(e));
|
|
46
|
+
FastImageEvents.send(view, REACT_ON_ERROR_EVENT, event);
|
|
45
47
|
FastImageEvents.send(view, REACT_ON_LOAD_END_EVENT);
|
|
46
48
|
return false;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
// The first root cause's message, e.g. "Not Found, status code: 404".
|
|
52
|
+
// The first root cause's message, e.g. "Not Found, status code: 404" (also
|
|
53
|
+
// for preload results).
|
|
54
|
+
static String errorMessage(@androidx.annotation.Nullable GlideException e) {
|
|
55
|
+
if (e != null) {
|
|
56
|
+
for (Throwable cause : e.getRootCauses()) {
|
|
57
|
+
if (cause.getMessage() != null) return cause.getMessage();
|
|
58
|
+
}
|
|
59
|
+
if (e.getMessage() != null) return e.getMessage();
|
|
60
|
+
}
|
|
61
|
+
return "Failed to load the image";
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
@Override
|
|
50
65
|
public boolean onResourceReady(Drawable resource, Object model, final Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
|
51
66
|
if (!(target instanceof ImageViewTarget)) {
|
|
@@ -54,9 +69,9 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
|
|
|
54
69
|
final FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
|
|
55
70
|
view.onImageLoaded();
|
|
56
71
|
if (resource instanceof GifDrawable) {
|
|
57
|
-
//
|
|
72
|
+
// The `loop` prop, or the file's own loop count as on iOS. Glide
|
|
58
73
|
// loops every GIF forever by default (#651).
|
|
59
|
-
((GifDrawable) resource)
|
|
74
|
+
view.applyLoopCount((GifDrawable) resource);
|
|
60
75
|
}
|
|
61
76
|
if (!events) return false;
|
|
62
77
|
boolean local = !(model instanceof GlideUrl);
|
|
@@ -89,6 +89,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
89
89
|
view.setRecyclingKey(recyclingKey);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
@ReactProp(name = "loopCount", defaultInt = -1)
|
|
93
|
+
public void setLoopCount(FastImageViewWithUrl view, int loopCount) {
|
|
94
|
+
view.setLoopCount(loopCount);
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
@ReactProp(name = "resizeMode")
|
|
93
98
|
public void setResizeMode(FastImageViewWithUrl view, String resizeMode) {
|
|
94
99
|
final FastImageViewWithUrl.ScaleType scaleType = FastImageViewConverter.getScaleType(resizeMode);
|
|
@@ -122,7 +122,7 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
|
|
|
122
122
|
.listener(new RequestListener<Drawable>() {
|
|
123
123
|
@Override
|
|
124
124
|
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
|
125
|
-
results[index] = failure(errorMessage(e));
|
|
125
|
+
results[index] = failure(FastImageRequestListener.errorMessage(e));
|
|
126
126
|
preloadsInFlight--;
|
|
127
127
|
finishOne.run();
|
|
128
128
|
startPendingPreloads();
|
|
@@ -164,17 +164,6 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
|
|
|
164
164
|
return result;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// The first root cause's message, e.g. "Not Found, status code: 404".
|
|
168
|
-
private static String errorMessage(@Nullable GlideException e) {
|
|
169
|
-
if (e != null) {
|
|
170
|
-
for (Throwable cause : e.getRootCauses()) {
|
|
171
|
-
if (cause.getMessage() != null) return cause.getMessage();
|
|
172
|
-
}
|
|
173
|
-
if (e.getMessage() != null) return e.getMessage();
|
|
174
|
-
}
|
|
175
|
-
return "Failed to load the image";
|
|
176
|
-
}
|
|
177
|
-
|
|
178
167
|
@ReactMethod
|
|
179
168
|
public void clearMemoryCache(final Promise promise) {
|
|
180
169
|
final Activity activity = getCurrentActivity();
|
|
@@ -7,6 +7,7 @@ import android.annotation.SuppressLint;
|
|
|
7
7
|
import android.content.Context;
|
|
8
8
|
import android.graphics.drawable.Drawable;
|
|
9
9
|
|
|
10
|
+
import androidx.annotation.NonNull;
|
|
10
11
|
import androidx.annotation.Nullable;
|
|
11
12
|
import androidx.appcompat.widget.AppCompatImageView;
|
|
12
13
|
import androidx.core.view.ViewCompat;
|
|
@@ -14,9 +15,11 @@ import androidx.core.view.ViewCompat;
|
|
|
14
15
|
import com.bumptech.glide.RequestBuilder;
|
|
15
16
|
import com.bumptech.glide.RequestManager;
|
|
16
17
|
import com.bumptech.glide.load.model.GlideUrl;
|
|
18
|
+
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
|
17
19
|
import com.bumptech.glide.request.Request;
|
|
18
20
|
import com.bumptech.glide.request.target.DrawableImageViewTarget;
|
|
19
21
|
import com.bumptech.glide.request.target.SizeReadyCallback;
|
|
22
|
+
import com.bumptech.glide.request.transition.Transition;
|
|
20
23
|
import com.facebook.react.bridge.ReadableMap;
|
|
21
24
|
import com.facebook.react.bridge.WritableMap;
|
|
22
25
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
@@ -61,6 +64,68 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
61
64
|
if (!ViewCompat.isLaidOut(this)) layout(0, 0, 0, 0);
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
// How many times GIFs play: -1 for the file's own loop count (the `loop`
|
|
68
|
+
// prop not set), 0 for forever, or a number of times.
|
|
69
|
+
private int mLoopCount = -1;
|
|
70
|
+
|
|
71
|
+
public void setLoopCount(int loopCount) {
|
|
72
|
+
if (loopCount == mLoopCount) return;
|
|
73
|
+
mLoopCount = loopCount;
|
|
74
|
+
// Apply it to the GIF that's showing, and play it again. Only this
|
|
75
|
+
// view's own animation: restarting one Glide shares throws while
|
|
76
|
+
// another view plays it.
|
|
77
|
+
if (mOwnGif != null && getDrawable() == mOwnGif) {
|
|
78
|
+
applyLoopCount(mOwnGif);
|
|
79
|
+
mOwnGif.stop();
|
|
80
|
+
mOwnGif.startFromFirstFrame();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The GIF this view shows as its own animation (FastImageGif), recycled
|
|
85
|
+
// when the view stops showing it.
|
|
86
|
+
@Nullable
|
|
87
|
+
private GifDrawable mOwnGif;
|
|
88
|
+
|
|
89
|
+
// Shows each GIF as this view's own animation. Glide's target also starts
|
|
90
|
+
// and stops it with the Activity, as it does Glide's own GifDrawable.
|
|
91
|
+
private final class OwnGifTarget extends DrawableImageViewTarget {
|
|
92
|
+
OwnGifTarget() {
|
|
93
|
+
super(FastImageViewWithUrl.this);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Override
|
|
97
|
+
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
|
98
|
+
if (resource instanceof GifDrawable) {
|
|
99
|
+
GifDrawable own = FastImageGif.copy(
|
|
100
|
+
getContext(), (GifDrawable) resource, mLoadingWidth, mLoadingHeight);
|
|
101
|
+
if (own != null) {
|
|
102
|
+
applyLoopCount(own);
|
|
103
|
+
super.onResourceReady(own, transition);
|
|
104
|
+
mOwnGif = own;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
super.onResourceReady(resource, transition);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Override
|
|
112
|
+
protected void setResource(@Nullable Drawable resource) {
|
|
113
|
+
super.setResource(resource);
|
|
114
|
+
// Not shown anymore (replaced, cleared or failed): free its frames.
|
|
115
|
+
if (mOwnGif != null && mOwnGif != resource) {
|
|
116
|
+
mOwnGif.stop();
|
|
117
|
+
mOwnGif.recycle();
|
|
118
|
+
mOwnGif = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
void applyLoopCount(GifDrawable gif) {
|
|
124
|
+
gif.setLoopCount(mLoopCount == -1 ? GifDrawable.LOOP_INTRINSIC
|
|
125
|
+
: mLoopCount == 0 ? GifDrawable.LOOP_FOREVER
|
|
126
|
+
: mLoopCount);
|
|
127
|
+
}
|
|
128
|
+
|
|
64
129
|
// Glide crops or fits the bitmap for the scale type when it loads, so a
|
|
65
130
|
// new resizeMode needs a reload to take effect (#762).
|
|
66
131
|
public void setResizeMode(ScaleType scaleType) {
|
|
@@ -179,7 +244,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
179
244
|
mLoadingRequest = request;
|
|
180
245
|
mLoadingWidth = 0;
|
|
181
246
|
mLoadingHeight = 0;
|
|
182
|
-
|
|
247
|
+
OwnGifTarget target = new OwnGifTarget();
|
|
183
248
|
target.getSize(new SizeReadyCallback() {
|
|
184
249
|
@Override
|
|
185
250
|
public void onSizeReady(int width, int height) {
|
|
@@ -243,7 +308,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
243
308
|
// (#1028, #945).
|
|
244
309
|
if (mSource != null && (imageSource == null || imageSource.getUri().toString().length() == 0)) {
|
|
245
310
|
WritableMap event = new WritableNativeMap();
|
|
246
|
-
event.putString("
|
|
311
|
+
event.putString("error", "Invalid source: " + mSource);
|
|
247
312
|
FastImageEvents.send(this, REACT_ON_ERROR_EVENT, event);
|
|
248
313
|
FastImageEvents.send(this, REACT_ON_LOAD_END_EVENT);
|
|
249
314
|
|
package/dist/index.cjs.js
CHANGED
|
@@ -63,11 +63,17 @@ function tintColorFromStyle(style) {
|
|
|
63
63
|
}
|
|
64
64
|
return style && typeof style === "object" ? style.tintColor : void 0;
|
|
65
65
|
}
|
|
66
|
+
function loopCount(loop) {
|
|
67
|
+
if (loop === void 0) return -1;
|
|
68
|
+
if (loop === true) return 0;
|
|
69
|
+
if (loop === false) return 1;
|
|
70
|
+
return Number.isFinite(loop) ? Math.max(1, Math.floor(loop)) : 0;
|
|
71
|
+
}
|
|
66
72
|
function withoutCache(source) {
|
|
67
73
|
const { cache: _cache, ...rest } = source || {};
|
|
68
74
|
return rest;
|
|
69
75
|
}
|
|
70
|
-
function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", forwardedRef, onLayout, pointerEvents, ...viewProps }) {
|
|
76
|
+
function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", loop, forwardedRef, onLayout, pointerEvents, ...viewProps }) {
|
|
71
77
|
const { onClick, ...props } = viewProps;
|
|
72
78
|
const wrapperProps = {
|
|
73
79
|
onLayout,
|
|
@@ -108,6 +114,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
108
114
|
}, /* @__PURE__ */ react.default.createElement(FastImageView, {
|
|
109
115
|
...imageProps,
|
|
110
116
|
tintColor: resolvedTintColor,
|
|
117
|
+
loopCount: loopCount(loop),
|
|
111
118
|
style: react_native.StyleSheet.absoluteFill,
|
|
112
119
|
source: resolvedSource,
|
|
113
120
|
defaultSource: resolvedDefaultSource,
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -13,6 +13,12 @@ export type OnLoadEvent = SyntheticEvent<
|
|
|
13
13
|
}>,
|
|
14
14
|
>
|
|
15
15
|
|
|
16
|
+
export type OnErrorEvent = SyntheticEvent<
|
|
17
|
+
$ReadOnly<{|
|
|
18
|
+
error: string,
|
|
19
|
+
|}>,
|
|
20
|
+
>
|
|
21
|
+
|
|
16
22
|
export type OnProgressEvent = SyntheticEvent<
|
|
17
23
|
$ReadOnly<{|
|
|
18
24
|
loaded: number,
|
|
@@ -70,7 +76,7 @@ export type FastImageSource = {
|
|
|
70
76
|
|
|
71
77
|
export type FastImageProps = $ReadOnly<{|
|
|
72
78
|
...ViewProps,
|
|
73
|
-
onError?: ?() => void,
|
|
79
|
+
onError?: ?(event: OnErrorEvent) => void,
|
|
74
80
|
onLoad?: ?(event: OnLoadEvent) => void,
|
|
75
81
|
onLoadEnd?: ?() => void,
|
|
76
82
|
onLoadStart?: ?() => void,
|
|
@@ -83,6 +89,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
83
89
|
resizeMode?: ?ResizeModes,
|
|
84
90
|
fallback?: ?boolean,
|
|
85
91
|
recyclingKey?: ?string,
|
|
92
|
+
loop?: ?(boolean | number),
|
|
86
93
|
testID?: ?string,
|
|
87
94
|
|}>
|
|
88
95
|
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface OnLoadEvent {
|
|
|
35
35
|
target?: number;
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
export interface OnErrorEvent {
|
|
39
|
+
nativeEvent: {
|
|
40
|
+
error: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
38
43
|
export interface OnProgressEvent {
|
|
39
44
|
nativeEvent: {
|
|
40
45
|
loaded: number;
|
|
@@ -69,10 +74,16 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
69
74
|
* image. Unlike changing `key`, the view is kept.
|
|
70
75
|
*/
|
|
71
76
|
recyclingKey?: string | null;
|
|
77
|
+
/**
|
|
78
|
+
* How many times an animated image (GIF, animated WebP) plays: the file's
|
|
79
|
+
* own loop count by default, `true` to loop forever, `false` to play once,
|
|
80
|
+
* or a number of times. Changing it restarts the animation.
|
|
81
|
+
*/
|
|
82
|
+
loop?: boolean | number;
|
|
72
83
|
onLoadStart?(): void;
|
|
73
84
|
onProgress?(event: OnProgressEvent): void;
|
|
74
85
|
onLoad?(event: OnLoadEvent): void;
|
|
75
|
-
onError?(): void;
|
|
86
|
+
onError?(event: OnErrorEvent): void;
|
|
76
87
|
onLoadEnd?(): void;
|
|
77
88
|
/**
|
|
78
89
|
* onLayout function
|
package/dist/index.js
CHANGED
|
@@ -40,11 +40,17 @@ function tintColorFromStyle(style) {
|
|
|
40
40
|
}
|
|
41
41
|
return style && typeof style === "object" ? style.tintColor : void 0;
|
|
42
42
|
}
|
|
43
|
+
function loopCount(loop) {
|
|
44
|
+
if (loop === void 0) return -1;
|
|
45
|
+
if (loop === true) return 0;
|
|
46
|
+
if (loop === false) return 1;
|
|
47
|
+
return Number.isFinite(loop) ? Math.max(1, Math.floor(loop)) : 0;
|
|
48
|
+
}
|
|
43
49
|
function withoutCache(source) {
|
|
44
50
|
const { cache: _cache, ...rest } = source || {};
|
|
45
51
|
return rest;
|
|
46
52
|
}
|
|
47
|
-
function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", forwardedRef, onLayout, pointerEvents, ...viewProps }) {
|
|
53
|
+
function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", loop, forwardedRef, onLayout, pointerEvents, ...viewProps }) {
|
|
48
54
|
const { onClick, ...props } = viewProps;
|
|
49
55
|
const wrapperProps = {
|
|
50
56
|
onLayout,
|
|
@@ -85,6 +91,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
85
91
|
}, /* @__PURE__ */ React.createElement(FastImageView, {
|
|
86
92
|
...imageProps,
|
|
87
93
|
tintColor: resolvedTintColor,
|
|
94
|
+
loopCount: loopCount(loop),
|
|
88
95
|
style: StyleSheet.absoluteFill,
|
|
89
96
|
source: resolvedSource,
|
|
90
97
|
defaultSource: resolvedDefaultSource,
|
package/dist/index.js.flow
CHANGED
|
@@ -13,6 +13,12 @@ export type OnLoadEvent = SyntheticEvent<
|
|
|
13
13
|
}>,
|
|
14
14
|
>
|
|
15
15
|
|
|
16
|
+
export type OnErrorEvent = SyntheticEvent<
|
|
17
|
+
$ReadOnly<{|
|
|
18
|
+
error: string,
|
|
19
|
+
|}>,
|
|
20
|
+
>
|
|
21
|
+
|
|
16
22
|
export type OnProgressEvent = SyntheticEvent<
|
|
17
23
|
$ReadOnly<{|
|
|
18
24
|
loaded: number,
|
|
@@ -70,7 +76,7 @@ export type FastImageSource = {
|
|
|
70
76
|
|
|
71
77
|
export type FastImageProps = $ReadOnly<{|
|
|
72
78
|
...ViewProps,
|
|
73
|
-
onError?: ?() => void,
|
|
79
|
+
onError?: ?(event: OnErrorEvent) => void,
|
|
74
80
|
onLoad?: ?(event: OnLoadEvent) => void,
|
|
75
81
|
onLoadEnd?: ?() => void,
|
|
76
82
|
onLoadStart?: ?() => void,
|
|
@@ -83,6 +89,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
83
89
|
resizeMode?: ?ResizeModes,
|
|
84
90
|
fallback?: ?boolean,
|
|
85
91
|
recyclingKey?: ?string,
|
|
92
|
+
loop?: ?(boolean | number),
|
|
86
93
|
testID?: ?string,
|
|
87
94
|
|}>
|
|
88
95
|
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
// When it changes, the next image doesn't replace the current one: the view
|
|
23
23
|
// clears first (for views reused for other content, like list rows).
|
|
24
24
|
@property (nonatomic, copy) NSString *recyclingKey;
|
|
25
|
+
// How many times animated images play: -1 for the file's own loop count (the
|
|
26
|
+
// `loop` prop not set), 0 for forever, or a number of times.
|
|
27
|
+
@property (nonatomic, assign) NSInteger loopCount;
|
|
25
28
|
|
|
26
29
|
@end
|
|
27
30
|
|
|
31
|
+
// An error's description, with the HTTP status code when there is one.
|
|
32
|
+
FOUNDATION_EXTERN NSString *FFFErrorMessage(NSError *error);
|
|
33
|
+
|
|
34
|
+
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#import <SDWebImage/UIImage+MultiFormat.h>
|
|
3
3
|
#import <SDWebImage/UIView+WebCache.h>
|
|
4
4
|
#import <React/RCTUtils.h>
|
|
5
|
+
#import <SDWebImage/SDWebImageError.h>
|
|
5
6
|
|
|
6
7
|
@interface FFFastImageView ()
|
|
7
8
|
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
@property(nonatomic, assign) BOOL needsReload;
|
|
13
14
|
|
|
14
15
|
@property(nonatomic, strong) NSDictionary* onLoadEvent;
|
|
16
|
+
@property(nonatomic, strong) NSDictionary* onErrorEvent;
|
|
15
17
|
// The image before tinting, kept while a tint is applied so the tint can be
|
|
16
18
|
// changed or removed. nil when there's no tint (super.image is untinted).
|
|
17
19
|
@property(nonatomic, strong) UIImage* untintedImage;
|
|
@@ -47,9 +49,31 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
47
49
|
self = [super init];
|
|
48
50
|
self.resizeMode = RCTResizeModeCover;
|
|
49
51
|
self.clipsToBounds = YES;
|
|
52
|
+
_loopCount = -1;
|
|
50
53
|
return self;
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
- (void) setLoopCount: (NSInteger)loopCount {
|
|
57
|
+
if (_loopCount == loopCount) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
_loopCount = loopCount;
|
|
61
|
+
// SDAnimatedImageView uses animationRepeatCount (0 is forever) instead of
|
|
62
|
+
// the file's loop count when shouldCustomLoopCount is set, for the next
|
|
63
|
+
// image it shows.
|
|
64
|
+
self.shouldCustomLoopCount = loopCount >= 0;
|
|
65
|
+
if (loopCount >= 0) {
|
|
66
|
+
self.animationRepeatCount = loopCount;
|
|
67
|
+
} else if (self.player && [self.image conformsToProtocol: @protocol(SDAnimatedImage)]) {
|
|
68
|
+
self.player.totalLoopCount = [(id<SDAnimatedImage>) self.image animatedImageLoopCount];
|
|
69
|
+
}
|
|
70
|
+
// Apply it to the image that's showing, and play it again.
|
|
71
|
+
if (self.player) {
|
|
72
|
+
[self.player seekToFrameAtIndex: 0 loopCount: 0];
|
|
73
|
+
[self startAnimating];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
53
77
|
- (void) setResizeMode: (RCTResizeMode)resizeMode {
|
|
54
78
|
if (_resizeMode != resizeMode) {
|
|
55
79
|
_resizeMode = resizeMode;
|
|
@@ -98,7 +122,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
98
122
|
- (void) setOnFastImageError: (RCTDirectEventBlock)onFastImageError {
|
|
99
123
|
_onFastImageError = onFastImageError;
|
|
100
124
|
if (self.hasErrored && _onFastImageError) {
|
|
101
|
-
_onFastImageError(
|
|
125
|
+
_onFastImageError(self.onErrorEvent);
|
|
102
126
|
}
|
|
103
127
|
}
|
|
104
128
|
|
|
@@ -172,6 +196,26 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
172
196
|
[self updateContentMode];
|
|
173
197
|
}
|
|
174
198
|
|
|
199
|
+
// The error's description, with the HTTP status code when there is one (as on
|
|
200
|
+
// Android): SDWebImage keeps the code out of the description. Also for preload
|
|
201
|
+
// results.
|
|
202
|
+
NSString *FFFErrorMessage(NSError *error)
|
|
203
|
+
{
|
|
204
|
+
NSNumber *statusCode = error.userInfo[SDWebImageErrorDownloadStatusCodeKey];
|
|
205
|
+
if (statusCode) {
|
|
206
|
+
return [NSString stringWithFormat: @"%@, status code: %@", error.localizedDescription, statusCode];
|
|
207
|
+
}
|
|
208
|
+
return error.localizedDescription ?: @"Failed to load the image";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
- (void) sendOnError: (nullable NSString*)message {
|
|
212
|
+
self.hasErrored = YES;
|
|
213
|
+
self.onErrorEvent = @{ @"error": message ?: @"Failed to load the image" };
|
|
214
|
+
if (self.onFastImageError) {
|
|
215
|
+
self.onFastImageError(self.onErrorEvent);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
175
219
|
- (void) sendOnLoad: (UIImage*)image {
|
|
176
220
|
self.onLoadEvent = @{
|
|
177
221
|
@"width": [NSNumber numberWithDouble: image.size.width],
|
|
@@ -236,10 +280,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
236
280
|
// Not decodable: fail like a remote image, showing defaultSource.
|
|
237
281
|
[self setImage: _defaultSource];
|
|
238
282
|
self.showsLoadedImage = NO;
|
|
239
|
-
self
|
|
240
|
-
if (self.onFastImageError) {
|
|
241
|
-
self.onFastImageError(@{});
|
|
242
|
-
}
|
|
283
|
+
[self sendOnError: @"Failed to decode the image"];
|
|
243
284
|
if (self.onFastImageLoadEnd) {
|
|
244
285
|
self.onFastImageLoadEnd(@{});
|
|
245
286
|
}
|
|
@@ -369,10 +410,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
369
410
|
if (error) {
|
|
370
411
|
// SDWebImage shows the placeholder (defaultSource or nothing).
|
|
371
412
|
weakSelf.showsLoadedImage = NO;
|
|
372
|
-
weakSelf
|
|
373
|
-
if (weakSelf.onFastImageError) {
|
|
374
|
-
weakSelf.onFastImageError(@{});
|
|
375
|
-
}
|
|
413
|
+
[weakSelf sendOnError: FFFErrorMessage(error)];
|
|
376
414
|
if (weakSelf.onFastImageLoadEnd) {
|
|
377
415
|
weakSelf.onFastImageLoadEnd(@{});
|
|
378
416
|
}
|
|
@@ -18,6 +18,7 @@ RCT_EXPORT_VIEW_PROPERTY(source, FFFastImageSource)
|
|
|
18
18
|
RCT_EXPORT_VIEW_PROPERTY(defaultSource, UIImage)
|
|
19
19
|
RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
|
|
20
20
|
RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
|
|
21
|
+
RCT_EXPORT_VIEW_PROPERTY(loopCount, NSInteger)
|
|
21
22
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
|
|
22
23
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock)
|
|
23
24
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock)
|
|
@@ -25,16 +26,6 @@ RCT_EXPORT_VIEW_PROPERTY(onFastImageLoad, RCTDirectEventBlock)
|
|
|
25
26
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadEnd, RCTDirectEventBlock)
|
|
26
27
|
RCT_REMAP_VIEW_PROPERTY(tintColor, imageColor, UIColor)
|
|
27
28
|
|
|
28
|
-
// The error's description, with the HTTP status code when there is one.
|
|
29
|
-
static NSString *FFFErrorMessage(NSError *error)
|
|
30
|
-
{
|
|
31
|
-
NSNumber *statusCode = error.userInfo[SDWebImageErrorDownloadStatusCodeKey];
|
|
32
|
-
if (statusCode) {
|
|
33
|
-
return [NSString stringWithFormat:@"%@, status code: %@", error.localizedDescription, statusCode];
|
|
34
|
-
}
|
|
35
|
-
return error.localizedDescription ?: @"Failed to load the image";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
// Preloads waiting to start, in the order they were added, and the number
|
|
39
30
|
// loading, across all preload calls. Like SDWebImagePrefetcher, at most
|
|
40
31
|
// maxConcurrentPrefetchCount load at a time, so a long list doesn't queue
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-fast-image",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.10.0",
|
|
4
4
|
"description": "🚩 FastImage, performant React Native image component.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@types/react-native": "^0.69.5",
|
|
53
53
|
"@types/react-test-renderer": "^17.0.1",
|
|
54
54
|
"maestro-runner": "1.1.27",
|
|
55
|
+
"odiff-bin": "^4.5.0",
|
|
55
56
|
"oxfmt": "0.70.0",
|
|
56
57
|
"oxlint": "1.85.0",
|
|
57
58
|
"pretty-format": "^30.5.1",
|