@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.
Files changed (36) hide show
  1. package/README.md +2 -2
  2. package/RNFastImage.podspec +5 -2
  3. package/android/build.gradle +13 -0
  4. package/android/src/main/AndroidManifestNew.xml +3 -0
  5. package/android/src/main/java/com/dylanvann/fastimage/FastImageCookieHandler.java +84 -0
  6. package/android/src/main/java/com/dylanvann/fastimage/FastImageEvent.java +45 -0
  7. package/android/src/main/java/com/dylanvann/fastimage/FastImageEvents.java +60 -0
  8. package/android/src/main/java/com/dylanvann/fastimage/FastImageGif.java +110 -0
  9. package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +65 -5
  10. package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +65 -22
  11. package/android/src/main/java/com/dylanvann/fastimage/FastImageShadowNode.java +21 -0
  12. package/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java +21 -1
  13. package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +237 -0
  14. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +14 -5
  15. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +66 -22
  16. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +130 -24
  17. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +359 -39
  18. package/android/src/main/java/com/dylanvann/fastimage/FastImageWebGlideUrl.java +13 -0
  19. package/changes.json +1 -1
  20. package/dist/index.cjs.js +166 -130
  21. package/dist/index.cjs.js.flow +33 -2
  22. package/dist/index.d.ts +77 -22
  23. package/dist/index.js +141 -121
  24. package/dist/index.js.flow +33 -2
  25. package/ios/FastImage/FFFDownsampledImage.h +28 -0
  26. package/ios/FastImage/FFFDownsampledImage.m +169 -0
  27. package/ios/FastImage/FFFastImageSource.h +6 -0
  28. package/ios/FastImage/FFFastImageSource.m +13 -0
  29. package/ios/FastImage/FFFastImageView.h +17 -0
  30. package/ios/FastImage/FFFastImageView.m +498 -71
  31. package/ios/FastImage/FFFastImageViewManager.m +106 -8
  32. package/ios/FastImage/RCTConvert+FFFastImage.m +4 -1
  33. package/package.json +31 -43
  34. package/dist/index.d.ts.map +0 -1
  35. package/dist/index.test.d.ts +0 -2
  36. 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
- return source == null
57
- ? null
58
- : new FastImageSource(context, source.getString("uri"), getHeaders(source));
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
- if (imageSource.isResource()) {
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.setScaleType(scaleType);
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
- if (view.glideUrl != null) {
92
- final String key = view.glideUrl.toString();
93
- FastImageOkHttpProgressGlideModule.forget(key);
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
- ThemedReactContext context = (ThemedReactContext) view.getContext();
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.load.model.GlideUrl;
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.views.imagehelper.ImageSource;
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 Activity activity = getCurrentActivity();
34
- if (activity == null) return;
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
- for (int i = 0; i < sources.size(); i++) {
39
- final ReadableMap source = sources.getMap(i);
40
- final FastImageSource imageSource = FastImageViewConverter.getImageSource(activity, source);
41
-
42
- Glide
43
- .with(activity.getApplicationContext())
44
- // This will make this work for remote and local images. e.g.
45
- // - file:///
46
- // - content://
47
- // - res:/
48
- // - android.resource://
49
- // - data:image/png;base64
50
- .load(
51
- imageSource.isBase64Resource() ? imageSource.getSource() :
52
- imageSource.isResource() ? imageSource.getUri() : imageSource.getGlideUrl()
53
- )
54
- .apply(FastImageViewConverter.getOptions(activity, imageSource, source))
55
- .preload();
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();