@depup/react-native-fast-image 8.6.3-depup.0 → 8.13.1-depup.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 +2 -2
- package/RNFastImage.podspec +5 -2
- package/android/build.gradle +13 -0
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageCookieHandler.java +84 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageEvent.java +45 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageEvents.java +60 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageGif.java +110 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +65 -5
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +65 -22
- package/android/src/main/java/com/dylanvann/fastimage/FastImageShadowNode.java +21 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java +21 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +237 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +14 -5
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +66 -22
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +130 -24
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +359 -39
- package/android/src/main/java/com/dylanvann/fastimage/FastImageWebGlideUrl.java +13 -0
- package/changes.json +1 -1
- package/dist/index.cjs.js +166 -130
- package/dist/index.cjs.js.flow +33 -2
- package/dist/index.d.ts +77 -22
- package/dist/index.js +141 -121
- package/dist/index.js.flow +33 -2
- package/ios/FastImage/FFFDownsampledImage.h +28 -0
- package/ios/FastImage/FFFDownsampledImage.m +169 -0
- package/ios/FastImage/FFFastImageSource.h +6 -0
- package/ios/FastImage/FFFastImageSource.m +13 -0
- package/ios/FastImage/FFFastImageView.h +17 -0
- package/ios/FastImage/FFFastImageView.m +498 -71
- package/ios/FastImage/FFFastImageViewManager.m +106 -8
- package/ios/FastImage/RCTConvert+FFFastImage.m +4 -1
- package/package.json +31 -43
- package/dist/index.d.ts.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.BitmapFactory;
|
|
5
|
+
import android.graphics.drawable.Drawable;
|
|
6
|
+
import android.net.Uri;
|
|
7
|
+
import android.os.Handler;
|
|
8
|
+
import android.os.Looper;
|
|
9
|
+
import android.util.Base64;
|
|
10
|
+
import android.util.LruCache;
|
|
11
|
+
import android.widget.ImageView;
|
|
12
|
+
|
|
13
|
+
import androidx.annotation.Nullable;
|
|
14
|
+
|
|
15
|
+
import com.bumptech.glide.Glide;
|
|
16
|
+
import com.bumptech.glide.load.ImageHeaderParserUtils;
|
|
17
|
+
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
|
18
|
+
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
|
19
|
+
import com.bumptech.glide.request.RequestOptions;
|
|
20
|
+
|
|
21
|
+
import java.io.ByteArrayInputStream;
|
|
22
|
+
import java.io.IOException;
|
|
23
|
+
import java.io.InputStream;
|
|
24
|
+
import java.nio.ByteBuffer;
|
|
25
|
+
import java.util.concurrent.Executor;
|
|
26
|
+
import java.util.concurrent.Executors;
|
|
27
|
+
|
|
28
|
+
// Works out an image's own size for onLoad, as iOS reports it (#608). Glide
|
|
29
|
+
// decodes images to fit the view, so the loaded drawable is usually smaller
|
|
30
|
+
// than the image.
|
|
31
|
+
final class FastImageSourceSize {
|
|
32
|
+
// Sizes of recently decoded images, for loads from the memory cache, which
|
|
33
|
+
// don't decode.
|
|
34
|
+
private static final LruCache<String, int[]> SIZES = new LruCache<>(500);
|
|
35
|
+
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
|
|
36
|
+
private static final Handler MAIN = new Handler(Looper.getMainLooper());
|
|
37
|
+
|
|
38
|
+
private FastImageSourceSize() {
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Wraps the downsample strategy Glide would use, to record the image's
|
|
42
|
+
// size (after EXIF rotation) when Glide decodes it. Views loading the same
|
|
43
|
+
// image at the same size share one decode, which uses the first view's
|
|
44
|
+
// options, so the size is recorded by image rather than by request.
|
|
45
|
+
static final class Capture extends DownsampleStrategy {
|
|
46
|
+
private final DownsampleStrategy strategy;
|
|
47
|
+
private final String key;
|
|
48
|
+
private boolean recorded;
|
|
49
|
+
|
|
50
|
+
Capture(DownsampleStrategy strategy, String key) {
|
|
51
|
+
this.strategy = strategy;
|
|
52
|
+
this.key = key;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Override
|
|
56
|
+
public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
|
|
57
|
+
// Called again with the sampled size; the first call has the image's.
|
|
58
|
+
if (!recorded) {
|
|
59
|
+
recorded = true;
|
|
60
|
+
SIZES.put(key, new int[]{sourceWidth, sourceHeight});
|
|
61
|
+
}
|
|
62
|
+
return strategy.getScaleFactor(sourceWidth, sourceHeight, requestedWidth, requestedHeight);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Override
|
|
66
|
+
public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
|
|
67
|
+
return strategy.getSampleSizeRounding(sourceWidth, sourceHeight, requestedWidth, requestedHeight);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Glide's memory cache keys include the strategy, so match the wrapped
|
|
71
|
+
// one to keep sharing cached images between views.
|
|
72
|
+
@Override
|
|
73
|
+
public boolean equals(Object o) {
|
|
74
|
+
return o instanceof Capture && ((Capture) o).strategy.equals(strategy);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Override
|
|
78
|
+
public int hashCode() {
|
|
79
|
+
return strategy.hashCode();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// What RequestBuilder.into(ImageView) applies for the view's scale type
|
|
84
|
+
// (it leaves a transformation that's already set alone), with the
|
|
85
|
+
// downsample strategy it implies wrapped by capture.
|
|
86
|
+
static RequestOptions scaleTypeOptions(@Nullable ImageView.ScaleType scaleType, Capture capture) {
|
|
87
|
+
RequestOptions options = new RequestOptions();
|
|
88
|
+
if (scaleType != null) {
|
|
89
|
+
switch (scaleType) {
|
|
90
|
+
case CENTER_CROP:
|
|
91
|
+
options = options.optionalCenterCrop();
|
|
92
|
+
break;
|
|
93
|
+
case CENTER_INSIDE:
|
|
94
|
+
case FIT_XY:
|
|
95
|
+
options = options.optionalCenterInside();
|
|
96
|
+
break;
|
|
97
|
+
case FIT_CENTER:
|
|
98
|
+
case FIT_START:
|
|
99
|
+
case FIT_END:
|
|
100
|
+
options = options.optionalFitCenter();
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return options.downsample(capture);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static Capture capture(@Nullable ImageView.ScaleType scaleType, Object model) {
|
|
110
|
+
return new Capture(strategy(scaleType), String.valueOf(model));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private static DownsampleStrategy strategy(@Nullable ImageView.ScaleType scaleType) {
|
|
114
|
+
if (scaleType == null) return DownsampleStrategy.DEFAULT;
|
|
115
|
+
switch (scaleType) {
|
|
116
|
+
case CENTER_CROP:
|
|
117
|
+
return DownsampleStrategy.CENTER_OUTSIDE;
|
|
118
|
+
case CENTER_INSIDE:
|
|
119
|
+
case FIT_XY:
|
|
120
|
+
return DownsampleStrategy.CENTER_INSIDE;
|
|
121
|
+
case FIT_CENTER:
|
|
122
|
+
case FIT_START:
|
|
123
|
+
case FIT_END:
|
|
124
|
+
return DownsampleStrategy.FIT_CENTER;
|
|
125
|
+
default:
|
|
126
|
+
return DownsampleStrategy.DEFAULT;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The image's size, or null when it has to be read from a local image's
|
|
131
|
+
// header first (see readLocal).
|
|
132
|
+
@Nullable
|
|
133
|
+
static int[] get(Drawable resource, Object model, boolean local, boolean fromResourceCache) {
|
|
134
|
+
String key = String.valueOf(model);
|
|
135
|
+
if (resource instanceof GifDrawable) {
|
|
136
|
+
// Glide decodes GIFs itself (sampled to fit the view), without the
|
|
137
|
+
// downsample strategy; their header has the size.
|
|
138
|
+
int[] size = gifSize((GifDrawable) resource);
|
|
139
|
+
if (size != null) return size;
|
|
140
|
+
} else if (fromResourceCache) {
|
|
141
|
+
// Glide caches local images already resized for the view, and the
|
|
142
|
+
// decode from there recorded the resized size.
|
|
143
|
+
SIZES.remove(key);
|
|
144
|
+
return null;
|
|
145
|
+
} else {
|
|
146
|
+
int[] size = SIZES.get(key);
|
|
147
|
+
if (size != null) return size;
|
|
148
|
+
if (local) return null;
|
|
149
|
+
}
|
|
150
|
+
return new int[]{resource.getIntrinsicWidth(), resource.getIntrinsicHeight()};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Nullable
|
|
154
|
+
private static int[] gifSize(GifDrawable gif) {
|
|
155
|
+
ByteBuffer buffer = gif.getBuffer();
|
|
156
|
+
if (buffer == null || buffer.limit() < 10) return null;
|
|
157
|
+
// The logical screen width and height (little-endian) follow the
|
|
158
|
+
// 6-byte signature.
|
|
159
|
+
int width = (buffer.get(6) & 0xff) | (buffer.get(7) & 0xff) << 8;
|
|
160
|
+
int height = (buffer.get(8) & 0xff) | (buffer.get(9) & 0xff) << 8;
|
|
161
|
+
return new int[]{width, height};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface Callback {
|
|
165
|
+
void onSize(int[] size);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Reads a local image's size from its header, off the main thread.
|
|
169
|
+
static void readLocal(Context context, final FastImageSource source, final Drawable resource, final Object model, final Callback callback) {
|
|
170
|
+
final Context appContext = context.getApplicationContext();
|
|
171
|
+
EXECUTOR.execute(new Runnable() {
|
|
172
|
+
@Override
|
|
173
|
+
public void run() {
|
|
174
|
+
int[] size = readBounds(appContext, source);
|
|
175
|
+
if (size != null) {
|
|
176
|
+
SIZES.put(String.valueOf(model), size);
|
|
177
|
+
} else {
|
|
178
|
+
size = new int[]{resource.getIntrinsicWidth(), resource.getIntrinsicHeight()};
|
|
179
|
+
}
|
|
180
|
+
final int[] result = size;
|
|
181
|
+
MAIN.post(new Runnable() {
|
|
182
|
+
@Override
|
|
183
|
+
public void run() {
|
|
184
|
+
callback.onSize(result);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@Nullable
|
|
192
|
+
private static int[] readBounds(Context context, FastImageSource source) {
|
|
193
|
+
try {
|
|
194
|
+
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
195
|
+
options.inJustDecodeBounds = true;
|
|
196
|
+
InputStream stream = open(context, source);
|
|
197
|
+
try {
|
|
198
|
+
BitmapFactory.decodeStream(stream, null, options);
|
|
199
|
+
} finally {
|
|
200
|
+
stream.close();
|
|
201
|
+
}
|
|
202
|
+
if (options.outWidth <= 0 || options.outHeight <= 0) return null;
|
|
203
|
+
int orientation;
|
|
204
|
+
stream = open(context, source);
|
|
205
|
+
try {
|
|
206
|
+
Glide glide = Glide.get(context);
|
|
207
|
+
orientation = ImageHeaderParserUtils.getOrientation(
|
|
208
|
+
glide.getRegistry().getImageHeaderParsers(), stream, glide.getArrayPool());
|
|
209
|
+
} finally {
|
|
210
|
+
stream.close();
|
|
211
|
+
}
|
|
212
|
+
// EXIF orientations 5 to 8 rotate by 90 or 270 degrees.
|
|
213
|
+
boolean rotated = orientation >= 5 && orientation <= 8;
|
|
214
|
+
return rotated
|
|
215
|
+
? new int[]{options.outHeight, options.outWidth}
|
|
216
|
+
: new int[]{options.outWidth, options.outHeight};
|
|
217
|
+
} catch (IOException | RuntimeException e) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private static InputStream open(Context context, FastImageSource source) throws IOException {
|
|
223
|
+
if (source.isBase64Resource()) {
|
|
224
|
+
String data = source.getSource();
|
|
225
|
+
byte[] bytes = Base64.decode(data.substring(data.indexOf(',') + 1), Base64.DEFAULT);
|
|
226
|
+
return new ByteArrayInputStream(bytes);
|
|
227
|
+
}
|
|
228
|
+
Uri uri = source.getUri();
|
|
229
|
+
String path = uri.getPath();
|
|
230
|
+
if ("file".equals(uri.getScheme()) && path != null && path.startsWith("/android_asset/")) {
|
|
231
|
+
return context.getAssets().open(path.substring("/android_asset/".length()));
|
|
232
|
+
}
|
|
233
|
+
InputStream stream = context.getContentResolver().openInputStream(uri);
|
|
234
|
+
if (stream == null) throw new IOException("Can't open " + uri);
|
|
235
|
+
return stream;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -50,12 +50,20 @@ class FastImageViewConverter {
|
|
|
50
50
|
put("center", ScaleType.CENTER_INSIDE);
|
|
51
51
|
}};
|
|
52
52
|
|
|
53
|
+
// Whether the source has a uri to load (not null, missing or blank).
|
|
54
|
+
static boolean hasUri(@Nullable ReadableMap source) {
|
|
55
|
+
if (source == null || !source.hasKey("uri") || source.isNull("uri")) return false;
|
|
56
|
+
String uri = source.getString("uri");
|
|
57
|
+
return uri != null && !uri.trim().isEmpty();
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
// Resolve the source uri to a file path that android understands.
|
|
54
61
|
static @Nullable
|
|
55
62
|
FastImageSource getImageSource(Context context, @Nullable ReadableMap source) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
if (source == null) return null;
|
|
64
|
+
FastImageSource imageSource = new FastImageSource(context, source.getString("uri"), getHeaders(source));
|
|
65
|
+
imageSource.setWebCache(getCacheControl(source) == FastImageCacheControl.WEB);
|
|
66
|
+
return imageSource;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
static Headers getHeaders(ReadableMap source) {
|
|
@@ -79,7 +87,7 @@ class FastImageViewConverter {
|
|
|
79
87
|
return headers;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
static RequestOptions getOptions(Context context, FastImageSource imageSource, ReadableMap source) {
|
|
90
|
+
static RequestOptions getOptions(Context context, @Nullable FastImageSource imageSource, @Nullable ReadableMap source) {
|
|
83
91
|
// Get priority.
|
|
84
92
|
final Priority priority = FastImageViewConverter.getPriority(source);
|
|
85
93
|
// Get cache control method.
|
|
@@ -108,7 +116,8 @@ class FastImageViewConverter {
|
|
|
108
116
|
.priority(priority)
|
|
109
117
|
.placeholder(TRANSPARENT_DRAWABLE);
|
|
110
118
|
|
|
111
|
-
|
|
119
|
+
// imageSource is null when only a defaultSource is shown.
|
|
120
|
+
if (imageSource != null && imageSource.isResource()) {
|
|
112
121
|
// Every local resource (drawable) in Android has its own unique numeric id, which are
|
|
113
122
|
// generated at build time. Although these ids are unique, they are not guaranteed unique
|
|
114
123
|
// across builds. The underlying glide implementation caches these resources. To make
|
|
@@ -18,10 +18,11 @@ import com.facebook.react.bridge.ReadableMap;
|
|
|
18
18
|
import com.facebook.react.bridge.WritableMap;
|
|
19
19
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
20
20
|
import com.facebook.react.common.MapBuilder;
|
|
21
|
+
import com.facebook.react.uimanager.LayoutShadowNode;
|
|
21
22
|
import com.facebook.react.uimanager.SimpleViewManager;
|
|
23
|
+
import com.facebook.react.bridge.ReactContext;
|
|
22
24
|
import com.facebook.react.uimanager.ThemedReactContext;
|
|
23
25
|
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
24
|
-
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
|
25
26
|
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
|
|
26
27
|
|
|
27
28
|
import java.util.List;
|
|
@@ -37,9 +38,6 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
37
38
|
static final String REACT_ON_PROGRESS_EVENT = "onFastImageProgress";
|
|
38
39
|
private static final Map<String, List<FastImageViewWithUrl>> VIEWS_FOR_URLS = new WeakHashMap<>();
|
|
39
40
|
|
|
40
|
-
@Nullable
|
|
41
|
-
private RequestManager requestManager = null;
|
|
42
|
-
|
|
43
41
|
@NonNull
|
|
44
42
|
@Override
|
|
45
43
|
public String getName() {
|
|
@@ -49,11 +47,20 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
49
47
|
@NonNull
|
|
50
48
|
@Override
|
|
51
49
|
protected FastImageViewWithUrl createViewInstance(@NonNull ThemedReactContext reactContext) {
|
|
50
|
+
// Each view keeps the RequestManager for its own Activity. A single one
|
|
51
|
+
// shared by all views kept the last view's Activity alive after it was
|
|
52
|
+
// destroyed, and gave views another Activity's manager (#492).
|
|
53
|
+
RequestManager requestManager = null;
|
|
52
54
|
if (isValidContextForGlide(reactContext)) {
|
|
53
55
|
requestManager = Glide.with(reactContext);
|
|
56
|
+
} else if (getActivityFromContext(reactContext) == null) {
|
|
57
|
+
// Not in an Activity (e.g. a root view created with the application
|
|
58
|
+
// context): load with the application context, instead of leaving
|
|
59
|
+
// requestManager null and never loading (#520).
|
|
60
|
+
requestManager = Glide.with(reactContext.getApplicationContext());
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
return new FastImageViewWithUrl(reactContext);
|
|
63
|
+
return new FastImageViewWithUrl(reactContext, requestManager);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
@ReactProp(name = "source")
|
|
@@ -77,26 +84,40 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
@ReactProp(name = "recyclingKey")
|
|
88
|
+
public void setRecyclingKey(FastImageViewWithUrl view, @Nullable String recyclingKey) {
|
|
89
|
+
view.setRecyclingKey(recyclingKey);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@ReactProp(name = "loopCount", defaultInt = -1)
|
|
93
|
+
public void setLoopCount(FastImageViewWithUrl view, int loopCount) {
|
|
94
|
+
view.setLoopCount(loopCount);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@ReactProp(name = "imageRendering")
|
|
98
|
+
public void setImageRendering(FastImageViewWithUrl view, @Nullable String imageRendering) {
|
|
99
|
+
view.setImageRendering(imageRendering);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@ReactProp(name = "paused")
|
|
103
|
+
public void setPaused(FastImageViewWithUrl view, boolean paused) {
|
|
104
|
+
view.setPaused(paused);
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
@ReactProp(name = "resizeMode")
|
|
81
108
|
public void setResizeMode(FastImageViewWithUrl view, String resizeMode) {
|
|
82
109
|
final FastImageViewWithUrl.ScaleType scaleType = FastImageViewConverter.getScaleType(resizeMode);
|
|
83
|
-
view.
|
|
110
|
+
view.setResizeMode(scaleType);
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
@Override
|
|
87
114
|
public void onDropViewInstance(@NonNull FastImageViewWithUrl view) {
|
|
88
115
|
// This will cancel existing requests.
|
|
89
|
-
view.clearView(requestManager);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
List<FastImageViewWithUrl> viewsForKey = VIEWS_FOR_URLS.get(key);
|
|
95
|
-
if (viewsForKey != null) {
|
|
96
|
-
viewsForKey.remove(view);
|
|
97
|
-
if (viewsForKey.size() == 0) VIEWS_FOR_URLS.remove(key);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
116
|
+
view.clearView(view.requestManager);
|
|
117
|
+
|
|
118
|
+
// Same key as when the view was tracked (toStringUrl, not toString,
|
|
119
|
+
// which differ for urls that need escaping).
|
|
120
|
+
view.untrackUrl(VIEWS_FOR_URLS);
|
|
100
121
|
|
|
101
122
|
super.onDropViewInstance(view);
|
|
102
123
|
}
|
|
@@ -120,10 +141,7 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
120
141
|
WritableMap event = new WritableNativeMap();
|
|
121
142
|
event.putInt("loaded", (int) bytesRead);
|
|
122
143
|
event.putInt("total", (int) expectedLength);
|
|
123
|
-
|
|
124
|
-
RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class);
|
|
125
|
-
int viewId = view.getId();
|
|
126
|
-
eventEmitter.receiveEvent(viewId, REACT_ON_PROGRESS_EVENT, event);
|
|
144
|
+
FastImageEvents.send(view, REACT_ON_PROGRESS_EVENT, event);
|
|
127
145
|
}
|
|
128
146
|
}
|
|
129
147
|
}
|
|
@@ -143,6 +161,20 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
143
161
|
return !isActivityDestroyed(activity);
|
|
144
162
|
}
|
|
145
163
|
|
|
164
|
+
// A view's context is the ThemedReactContext it was created with, but below
|
|
165
|
+
// Android 5 (API 21) AppCompatImageView wraps it in a TintContextWrapper, so
|
|
166
|
+
// it can't be cast directly (#840). Unwrap until the ReactContext.
|
|
167
|
+
@Nullable
|
|
168
|
+
static ReactContext getReactContext(Context context) {
|
|
169
|
+
while (context instanceof ContextWrapper) {
|
|
170
|
+
if (context instanceof ReactContext) {
|
|
171
|
+
return (ReactContext) context;
|
|
172
|
+
}
|
|
173
|
+
context = ((ContextWrapper) context).getBaseContext();
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
146
178
|
private static Activity getActivityFromContext(final Context context) {
|
|
147
179
|
if (context instanceof Activity) {
|
|
148
180
|
return (Activity) context;
|
|
@@ -175,9 +207,21 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
175
207
|
|
|
176
208
|
}
|
|
177
209
|
|
|
210
|
+
// Legacy architecture only; the New Architecture doesn't use shadow nodes.
|
|
211
|
+
@NonNull
|
|
212
|
+
@Override
|
|
213
|
+
public LayoutShadowNode createShadowNodeInstance() {
|
|
214
|
+
return new FastImageShadowNode();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@Override
|
|
218
|
+
public void updateExtraData(@NonNull FastImageViewWithUrl view, Object extraData) {
|
|
219
|
+
if (extraData == FastImageShadowNode.ZERO_LAYOUT) view.onZeroLayout();
|
|
220
|
+
}
|
|
221
|
+
|
|
178
222
|
@Override
|
|
179
223
|
protected void onAfterUpdateTransaction(@NonNull FastImageViewWithUrl view) {
|
|
180
224
|
super.onAfterUpdateTransaction(view);
|
|
181
|
-
view.onAfterUpdate(this, requestManager, VIEWS_FOR_URLS);
|
|
225
|
+
view.onAfterUpdate(this, view.requestManager, VIEWS_FOR_URLS);
|
|
182
226
|
}
|
|
183
227
|
}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
package com.dylanvann.fastimage;
|
|
2
2
|
|
|
3
3
|
import android.app.Activity;
|
|
4
|
+
import android.graphics.drawable.Drawable;
|
|
4
5
|
|
|
5
6
|
import androidx.annotation.NonNull;
|
|
7
|
+
import androidx.annotation.Nullable;
|
|
6
8
|
|
|
7
9
|
import com.bumptech.glide.Glide;
|
|
8
|
-
import com.bumptech.glide.
|
|
10
|
+
import com.bumptech.glide.Priority;
|
|
11
|
+
import com.bumptech.glide.load.DataSource;
|
|
12
|
+
import com.bumptech.glide.load.engine.GlideException;
|
|
13
|
+
import com.bumptech.glide.request.RequestListener;
|
|
14
|
+
import com.bumptech.glide.request.RequestOptions;
|
|
15
|
+
import com.bumptech.glide.request.target.Target;
|
|
16
|
+
import com.facebook.react.bridge.Arguments;
|
|
9
17
|
import com.facebook.react.bridge.Promise;
|
|
10
18
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
19
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
12
20
|
import com.facebook.react.bridge.ReactMethod;
|
|
13
21
|
import com.facebook.react.bridge.ReadableArray;
|
|
14
22
|
import com.facebook.react.bridge.ReadableMap;
|
|
15
|
-
import com.facebook.react.
|
|
23
|
+
import com.facebook.react.bridge.UiThreadUtil;
|
|
24
|
+
import com.facebook.react.bridge.WritableArray;
|
|
25
|
+
import com.facebook.react.bridge.WritableMap;
|
|
26
|
+
|
|
27
|
+
import java.util.ArrayDeque;
|
|
16
28
|
|
|
17
29
|
class FastImageViewModule extends ReactContextBaseJavaModule {
|
|
18
30
|
|
|
@@ -28,36 +40,130 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
|
|
|
28
40
|
return REACT_CLASS;
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
// At most this many preloaded sources load at a time, across all preload
|
|
44
|
+
// calls (the same as SDWebImagePrefetcher's default on iOS), so a long
|
|
45
|
+
// list doesn't queue hundreds of requests ahead of the images the app is
|
|
46
|
+
// showing (Glide's executors run requests in order within a priority).
|
|
47
|
+
private static final int PRELOAD_LIMIT = 3;
|
|
48
|
+
// Preloads waiting to start, in the order they were added, and the number
|
|
49
|
+
// loading. Only used on the UI thread (Glide calls its listeners there).
|
|
50
|
+
private static final ArrayDeque<Runnable> pendingPreloads = new ArrayDeque<>();
|
|
51
|
+
private static int preloadsInFlight = 0;
|
|
52
|
+
|
|
53
|
+
// Starts pending preloads while there's room. A listener calls it again,
|
|
54
|
+
// sometimes from inside run() (Glide reports a memory-cached image
|
|
55
|
+
// synchronously from preload()). That's fine: the counters are shared and
|
|
56
|
+
// updated before each run(), so the inner call starts what fits, and the
|
|
57
|
+
// outer loop sees the updated counters when it checks again.
|
|
58
|
+
private static void startPendingPreloads() {
|
|
59
|
+
while (preloadsInFlight < PRELOAD_LIMIT && !pendingPreloads.isEmpty()) {
|
|
60
|
+
preloadsInFlight++;
|
|
61
|
+
pendingPreloads.poll().run();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Resolves with a result per source, in order, once all have loaded or
|
|
66
|
+
// failed: { ok, width, height } or { ok: false, error }. Never rejects.
|
|
31
67
|
@ReactMethod
|
|
32
|
-
public void preload(final ReadableArray sources) {
|
|
33
|
-
final
|
|
34
|
-
|
|
35
|
-
activity.runOnUiThread(new Runnable() {
|
|
68
|
+
public void preload(final ReadableArray sources, final Promise promise) {
|
|
69
|
+
final ReactApplicationContext context = getReactApplicationContext();
|
|
70
|
+
UiThreadUtil.runOnUiThread(new Runnable() {
|
|
36
71
|
@Override
|
|
37
72
|
public void run() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
final int count = sources.size();
|
|
74
|
+
final WritableMap[] results = new WritableMap[count];
|
|
75
|
+
// Sources of this call still to finish. It starts at the
|
|
76
|
+
// number of sources, so the promise can't resolve before
|
|
77
|
+
// they've all been added.
|
|
78
|
+
final int[] remaining = {count};
|
|
79
|
+
final Runnable finishOne = new Runnable() {
|
|
80
|
+
@Override
|
|
81
|
+
public void run() {
|
|
82
|
+
if (--remaining[0] > 0) return;
|
|
83
|
+
WritableArray array = Arguments.createArray();
|
|
84
|
+
for (WritableMap result : results) array.pushMap(result);
|
|
85
|
+
promise.resolve(array);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
for (int i = 0; i < count; i++) {
|
|
89
|
+
final int index = i;
|
|
90
|
+
final ReadableMap source = sources.isNull(i) ? null : sources.getMap(i);
|
|
91
|
+
// Glide throws on an empty url. Invalid sources fail
|
|
92
|
+
// without taking a slot.
|
|
93
|
+
if (!FastImageViewConverter.hasUri(source)) {
|
|
94
|
+
results[i] = failure("Invalid source: no uri");
|
|
95
|
+
finishOne.run();
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
final FastImageSource imageSource = FastImageViewConverter.getImageSource(context, source);
|
|
99
|
+
// A uri that can't be resolved (e.g. a relative path) resolves
|
|
100
|
+
// to an empty one.
|
|
101
|
+
if (imageSource.getUri().toString().isEmpty()) {
|
|
102
|
+
results[i] = failure("Invalid source: can't resolve " + source.getString("uri"));
|
|
103
|
+
finishOne.run();
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
RequestOptions options = FastImageViewConverter.getOptions(context, imageSource, source);
|
|
107
|
+
// Low priority unless the source sets one, as on iOS (the
|
|
108
|
+
// prefetcher's options), so the images the app shows load
|
|
109
|
+
// first.
|
|
110
|
+
final RequestOptions preloadOptions = source.hasKey("priority") && !source.isNull("priority")
|
|
111
|
+
? options
|
|
112
|
+
: options.priority(Priority.LOW);
|
|
113
|
+
pendingPreloads.add(new Runnable() {
|
|
114
|
+
@Override
|
|
115
|
+
public void run() {
|
|
116
|
+
Glide
|
|
117
|
+
.with(context)
|
|
118
|
+
// Load it the way the view does, so local images
|
|
119
|
+
// (file://, content://, asset:/) work too.
|
|
120
|
+
.load(imageSource.getSourceForLoad())
|
|
121
|
+
.apply(preloadOptions)
|
|
122
|
+
.listener(new RequestListener<Drawable>() {
|
|
123
|
+
@Override
|
|
124
|
+
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
|
125
|
+
results[index] = failure(FastImageRequestListener.errorMessage(e));
|
|
126
|
+
preloadsInFlight--;
|
|
127
|
+
finishOne.run();
|
|
128
|
+
startPendingPreloads();
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Override
|
|
133
|
+
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
|
134
|
+
// Preloaded at its original size, so this is
|
|
135
|
+
// the image's own size.
|
|
136
|
+
WritableMap result = Arguments.createMap();
|
|
137
|
+
result.putBoolean("ok", true);
|
|
138
|
+
result.putInt("width", resource.getIntrinsicWidth());
|
|
139
|
+
result.putInt("height", resource.getIntrinsicHeight());
|
|
140
|
+
results[index] = result;
|
|
141
|
+
preloadsInFlight--;
|
|
142
|
+
finishOne.run();
|
|
143
|
+
startPendingPreloads();
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.preload();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (count == 0) {
|
|
152
|
+
promise.resolve(Arguments.createArray());
|
|
153
|
+
return;
|
|
56
154
|
}
|
|
155
|
+
startPendingPreloads();
|
|
57
156
|
}
|
|
58
157
|
});
|
|
59
158
|
}
|
|
60
159
|
|
|
160
|
+
private static WritableMap failure(String error) {
|
|
161
|
+
WritableMap result = Arguments.createMap();
|
|
162
|
+
result.putBoolean("ok", false);
|
|
163
|
+
result.putString("error", error);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
61
167
|
@ReactMethod
|
|
62
168
|
public void clearMemoryCache(final Promise promise) {
|
|
63
169
|
final Activity activity = getCurrentActivity();
|