@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
package/README.md CHANGED
@@ -13,8 +13,8 @@ npm install @depup/react-native-fast-image
13
13
 
14
14
  | Field | Value |
15
15
  |-------|-------|
16
- | Original | [react-native-fast-image](https://www.npmjs.com/package/react-native-fast-image) @ 8.6.3 |
17
- | Processed | 2026-03-18 |
16
+ | Original | [react-native-fast-image](https://www.npmjs.com/package/react-native-fast-image) @ 8.13.1 |
17
+ | Processed | 2026-09-27 |
18
18
  | Smoke test | failed |
19
19
  | Deps updated | 0 |
20
20
 
@@ -16,6 +16,9 @@ Pod::Spec.new do |s|
16
16
  s.source_files = "ios/**/*.{h,m}"
17
17
 
18
18
  s.dependency 'React-Core'
19
- s.dependency 'SDWebImage', '~> 5.11.1'
20
- s.dependency 'SDWebImageWebPCoder', '~> 0.8.4'
19
+ # Any 5.x from 5.11.1, so apps can use newer versions (and share them with
20
+ # other libraries) without having to update. 5.18.7+ has the privacy
21
+ # manifest Apple requires.
22
+ s.dependency 'SDWebImage', '>= 5.11.1', '< 6.0'
23
+ s.dependency 'SDWebImageWebPCoder', '>= 0.8.4', '< 1.0'
21
24
  end
@@ -19,7 +19,15 @@ buildscript {
19
19
 
20
20
  apply plugin: 'com.android.library'
21
21
 
22
+ // Newer versions of the Android Gradle plugin (7.3+) take the namespace from
23
+ // build.gradle and warn about the package attribute in AndroidManifest.xml;
24
+ // older versions need that attribute.
25
+ def supportsNamespace = project.android.hasProperty("namespace")
26
+
22
27
  android {
28
+ if (supportsNamespace) {
29
+ namespace "com.dylanvann.fastimage"
30
+ }
23
31
  compileSdkVersion safeExtGet('compileSdkVersion', 28)
24
32
  buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3')
25
33
  defaultConfig {
@@ -30,6 +38,9 @@ android {
30
38
  }
31
39
  sourceSets {
32
40
  main {
41
+ if (supportsNamespace) {
42
+ manifest.srcFile "src/main/AndroidManifestNew.xml"
43
+ }
33
44
  java {
34
45
  if (safeExtGet('excludeAppGlideModule', false)) {
35
46
  srcDir "src"
@@ -63,6 +74,8 @@ dependencies {
63
74
  //noinspection GradleDynamicVersion
64
75
  implementation 'com.facebook.react:react-native:+' // From node_modules
65
76
  implementation "com.github.bumptech.glide:glide:${glideVersion}"
77
+ // FastImageGlideModule and FastImageOkHttpProgressGlideModule use @GlideModule.
78
+ implementation "com.github.bumptech.glide:annotations:${glideVersion}"
66
79
  implementation "com.github.bumptech.glide:okhttp3-integration:${glideVersion}"
67
80
  annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
68
81
  }
@@ -0,0 +1,3 @@
1
+ <!-- Used instead of AndroidManifest.xml when the namespace is set in build.gradle. -->
2
+ <manifest>
3
+ </manifest>
@@ -0,0 +1,84 @@
1
+ package com.dylanvann.fastimage;
2
+
3
+ import android.content.Context;
4
+ import android.os.Build;
5
+ import android.text.TextUtils;
6
+ import android.webkit.CookieManager;
7
+ import android.webkit.CookieSyncManager;
8
+
9
+ import androidx.annotation.Nullable;
10
+
11
+ import java.net.CookieHandler;
12
+ import java.net.URI;
13
+ import java.util.Collections;
14
+ import java.util.List;
15
+ import java.util.Map;
16
+
17
+ // Reads and stores cookies in Android's cookie store (the WebView's
18
+ // CookieManager), which React Native's networking (fetch) and its Image
19
+ // component use. Image requests then send the cookies the app's other requests
20
+ // got, as on iOS. Does what React Native's ForwardingCookieHandler does,
21
+ // without the ReactContext that one needs (up to at least 0.77). Used through
22
+ // OkHttp's JavaNetCookieJar, which parses the cookies.
23
+ final class FastImageCookieHandler extends CookieHandler {
24
+ private final Context context;
25
+ @Nullable
26
+ private CookieManager cookieManager;
27
+
28
+ FastImageCookieHandler(Context context) {
29
+ this.context = context.getApplicationContext();
30
+ }
31
+
32
+ @Override
33
+ public Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) {
34
+ CookieManager cookieManager = cookieManager();
35
+ if (cookieManager == null) return Collections.emptyMap();
36
+ String cookies = cookieManager.getCookie(uri.toString());
37
+ if (TextUtils.isEmpty(cookies)) return Collections.emptyMap();
38
+ return Collections.singletonMap("Cookie", Collections.singletonList(cookies));
39
+ }
40
+
41
+ @Override
42
+ public void put(URI uri, Map<String, List<String>> responseHeaders) {
43
+ CookieManager cookieManager = cookieManager();
44
+ if (cookieManager == null) return;
45
+ String url = uri.toString();
46
+ boolean added = false;
47
+ for (Map.Entry<String, List<String>> header : responseHeaders.entrySet()) {
48
+ String name = header.getKey();
49
+ if (!"Set-Cookie".equalsIgnoreCase(name) && !"Set-Cookie2".equalsIgnoreCase(name)) continue;
50
+ for (String cookie : header.getValue()) {
51
+ cookieManager.setCookie(url, cookie);
52
+ added = true;
53
+ }
54
+ }
55
+ if (added) flush(cookieManager);
56
+ }
57
+
58
+ @SuppressWarnings("deprecation")
59
+ private static void flush(CookieManager cookieManager) {
60
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
61
+ cookieManager.flush();
62
+ } else {
63
+ CookieSyncManager.getInstance().sync();
64
+ }
65
+ }
66
+
67
+ // null while the WebView isn't available (not installed, or being
68
+ // updated): requests go without cookies then, instead of failing.
69
+ @Nullable
70
+ @SuppressWarnings("deprecation")
71
+ private synchronized CookieManager cookieManager() {
72
+ if (cookieManager == null) {
73
+ try {
74
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
75
+ CookieSyncManager.createInstance(context);
76
+ }
77
+ cookieManager = CookieManager.getInstance();
78
+ } catch (Exception e) {
79
+ return null;
80
+ }
81
+ }
82
+ return cookieManager;
83
+ }
84
+ }
@@ -0,0 +1,45 @@
1
+ package com.dylanvann.fastimage;
2
+
3
+ import com.facebook.react.bridge.WritableMap;
4
+ import com.facebook.react.uimanager.events.Event;
5
+ import com.facebook.react.uimanager.events.RCTEventEmitter;
6
+
7
+ // An event for a FastImage view, sent with the EventDispatcher (see
8
+ // FastImageEvents) instead of the RCTEventEmitter JS module, which the New
9
+ // Architecture warns about on every event.
10
+ class FastImageEvent extends Event<FastImageEvent> {
11
+ private final String name;
12
+ private final WritableMap data;
13
+
14
+ // Event(viewTag) works back to React Native 0.60. Without a surface id,
15
+ // Fabric finds the view by its tag.
16
+ @SuppressWarnings("deprecation")
17
+ FastImageEvent(int viewTag, String name, WritableMap data) {
18
+ super(viewTag);
19
+ this.name = name;
20
+ this.data = data;
21
+ }
22
+
23
+ @Override
24
+ public String getEventName() {
25
+ return name;
26
+ }
27
+
28
+ // Every event reaches JS, e.g. each progress update, as before.
29
+ @Override
30
+ public boolean canCoalesce() {
31
+ return false;
32
+ }
33
+
34
+ // Overrides Event.getEventData on React Native 0.65+ (not on older ones,
35
+ // hence no @Override).
36
+ protected WritableMap getEventData() {
37
+ return data;
38
+ }
39
+
40
+ @Override
41
+ @SuppressWarnings("deprecation")
42
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
43
+ rctEventEmitter.receiveEvent(getViewTag(), name, data);
44
+ }
45
+ }
@@ -0,0 +1,60 @@
1
+ package com.dylanvann.fastimage;
2
+
3
+ import android.view.View;
4
+
5
+ import androidx.annotation.Nullable;
6
+
7
+ import com.facebook.react.bridge.Arguments;
8
+ import com.facebook.react.bridge.ReactContext;
9
+ import com.facebook.react.bridge.WritableMap;
10
+ import com.facebook.react.uimanager.UIManagerModule;
11
+ import com.facebook.react.uimanager.events.EventDispatcher;
12
+
13
+ import java.lang.reflect.Method;
14
+
15
+ // Sends FastImage view events through React Native's EventDispatcher.
16
+ final class FastImageEvents {
17
+ // UIManagerHelper.getEventDispatcherForReactTag (React Native 0.63+, also
18
+ // bridgeless). Looked up by reflection so older versions still compile.
19
+ @Nullable
20
+ private static final Method GET_EVENT_DISPATCHER = findGetEventDispatcher();
21
+
22
+ private FastImageEvents() {
23
+ }
24
+
25
+ static void send(View view, String name) {
26
+ send(view, name, Arguments.createMap());
27
+ }
28
+
29
+ static void send(View view, String name, WritableMap data) {
30
+ ReactContext context = FastImageViewManager.getReactContext(view.getContext());
31
+ if (context == null) return;
32
+ EventDispatcher dispatcher = eventDispatcher(context, view.getId());
33
+ if (dispatcher != null) {
34
+ dispatcher.dispatchEvent(new FastImageEvent(view.getId(), name, data));
35
+ }
36
+ }
37
+
38
+ @Nullable
39
+ private static EventDispatcher eventDispatcher(ReactContext context, int viewTag) {
40
+ if (GET_EVENT_DISPATCHER != null) {
41
+ try {
42
+ return (EventDispatcher) GET_EVENT_DISPATCHER.invoke(null, context, viewTag);
43
+ } catch (Exception ignored) {
44
+ // Fall back to the UIManagerModule's.
45
+ }
46
+ }
47
+ UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class);
48
+ return uiManager == null ? null : uiManager.getEventDispatcher();
49
+ }
50
+
51
+ @Nullable
52
+ private static Method findGetEventDispatcher() {
53
+ try {
54
+ return Class.forName("com.facebook.react.uimanager.UIManagerHelper")
55
+ .getMethod("getEventDispatcherForReactTag", ReactContext.class, int.class);
56
+ } catch (Exception e) {
57
+ return null;
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,110 @@
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.lang.reflect.Field;
22
+ import java.nio.ByteBuffer;
23
+
24
+ // Gives a view its own animation of a GIF. Glide gives every view that shows
25
+ // the same GIF (same url and size) a GifDrawable over one frame loader (it's
26
+ // in the cached drawable's constant state), so they animate as one: a view
27
+ // whose GIF stopped (the `loop` prop) keeps drawing another view's frames, and
28
+ // restarting one throws while another view plays it. A copy has its own
29
+ // decoder and frame loader over the same data, so each view animates on its
30
+ // own, as with React Native's Image and on iOS. Decoded like Glide's
31
+ // ByteBufferGifDecoder does, at the size the image was loaded at, and cropped
32
+ // or fitted with the same transformation.
33
+ final class FastImageGif {
34
+ private FastImageGif() {}
35
+
36
+ // Null if the GIF can't be decoded again (then the view shows Glide's).
37
+ @Nullable
38
+ static GifDrawable copy(Context context, GifDrawable gif, int width, int height) {
39
+ if (width <= 0 || height <= 0) return null;
40
+ ByteBuffer data = gif.getBuffer();
41
+ GifHeader header = new GifHeaderParser().setData(data).parseHeader();
42
+ if (header.getNumFrames() <= 0 || header.getStatus() != GifDecoder.STATUS_OK) return null;
43
+ Glide glide = Glide.get(context);
44
+ BitmapPool bitmapPool = glide.getBitmapPool();
45
+ GifDecoder decoder = new StandardGifDecoder(
46
+ new GifBitmapProvider(bitmapPool, glide.getArrayPool()),
47
+ header, gif.getBuffer(), sampleSize(header, width, height));
48
+ decoder.setDefaultBitmapConfig(Bitmap.Config.ARGB_8888);
49
+ decoder.advance();
50
+ Bitmap firstFrame = decoder.getNextFrame();
51
+ if (firstFrame == null) {
52
+ decoder.clear();
53
+ return null;
54
+ }
55
+ GifDrawable copy = new GifDrawable(
56
+ context, decoder, UnitTransformation.<Bitmap>get(), width, height, firstFrame);
57
+ Transformation<Bitmap> transformation = gif.getFrameTransformation();
58
+ if (transformation != null && !(transformation instanceof UnitTransformation)) {
59
+ // As GifDrawableTransformation does.
60
+ Resource<Bitmap> frame = new BitmapResource(firstFrame, bitmapPool);
61
+ Resource<Bitmap> transformed = transformation.transform(context, frame, width, height);
62
+ if (!frame.equals(transformed)) frame.recycle();
63
+ copy.setFrameTransformation(transformation, transformed.get());
64
+ }
65
+ return copy;
66
+ }
67
+
68
+ // GifDrawable.start() also starts counting plays again, so a paused GIF
69
+ // would play its whole loop count again when resumed; iOS continues it.
70
+ // The count isn't public: keep it across start() by reflection. A GIF
71
+ // that already finished its plays starts over, as on iOS. Without the
72
+ // fields (another Glide version), it counts again, as start() does.
73
+ @Nullable
74
+ private static final Field LOOP_COUNT = field("loopCount");
75
+ @Nullable
76
+ private static final Field MAX_LOOP_COUNT = field("maxLoopCount");
77
+
78
+ static void resume(GifDrawable gif) {
79
+ if (LOOP_COUNT == null || MAX_LOOP_COUNT == null) {
80
+ gif.start();
81
+ return;
82
+ }
83
+ try {
84
+ int played = LOOP_COUNT.getInt(gif);
85
+ int max = MAX_LOOP_COUNT.getInt(gif);
86
+ gif.start();
87
+ if (max == GifDrawable.LOOP_FOREVER || played < max) LOOP_COUNT.setInt(gif, played);
88
+ } catch (IllegalAccessException e) {
89
+ gif.start();
90
+ }
91
+ }
92
+
93
+ @Nullable
94
+ private static Field field(String name) {
95
+ try {
96
+ Field field = GifDrawable.class.getDeclaredField(name);
97
+ field.setAccessible(true);
98
+ return field.getType() == int.class ? field : null;
99
+ } catch (Exception e) {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ // ByteBufferGifDecoder's.
105
+ private static int sampleSize(GifHeader header, int width, int height) {
106
+ int exact = Math.min(header.getHeight() / height, header.getWidth() / width);
107
+ int powerOfTwo = exact == 0 ? 0 : Integer.highestOneBit(exact);
108
+ return Math.max(1, powerOfTwo);
109
+ }
110
+ }
@@ -7,17 +7,25 @@ import com.bumptech.glide.Glide;
7
7
  import com.bumptech.glide.Registry;
8
8
  import com.bumptech.glide.annotation.GlideModule;
9
9
  import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
10
+ import com.bumptech.glide.load.Options;
10
11
  import com.bumptech.glide.load.model.GlideUrl;
12
+ import com.bumptech.glide.load.model.ModelLoader;
13
+ import com.bumptech.glide.load.model.ModelLoaderFactory;
14
+ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
11
15
  import com.bumptech.glide.module.LibraryGlideModule;
16
+ import com.facebook.react.modules.network.CookieJarContainer;
12
17
  import com.facebook.react.modules.network.OkHttpClientProvider;
13
18
 
19
+ import java.io.File;
14
20
  import java.io.IOException;
15
21
  import java.io.InputStream;
16
22
  import java.util.HashMap;
17
23
  import java.util.Map;
18
24
  import java.util.WeakHashMap;
19
25
 
26
+ import okhttp3.Cache;
20
27
  import okhttp3.Interceptor;
28
+ import okhttp3.JavaNetCookieJar;
21
29
  import okhttp3.MediaType;
22
30
  import okhttp3.OkHttpClient;
23
31
  import okhttp3.Request;
@@ -33,6 +41,7 @@ import okio.Source;
33
41
  public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
34
42
 
35
43
  private static final DispatchingProgressListener progressListener = new DispatchingProgressListener();
44
+ private static final long WEB_CACHE_SIZE = 50 * 1024 * 1024;
36
45
 
37
46
  @Override
38
47
  public void registerComponents(
@@ -40,13 +49,61 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
40
49
  @NonNull Glide glide,
41
50
  @NonNull Registry registry
42
51
  ) {
43
- OkHttpClient client = OkHttpClientProvider
44
- .getOkHttpClient()
52
+ OkHttpClient sharedClient = OkHttpClientProvider.getOkHttpClient();
53
+ OkHttpClient.Builder builder = sharedClient
45
54
  .newBuilder()
46
- .addInterceptor(createInterceptor(progressListener))
47
- .build();
55
+ .addInterceptor(createInterceptor(progressListener));
56
+ // React Native's shared client comes with an empty cookie jar (React
57
+ // Native only fills it in for its networking and Image clients), so
58
+ // images were loaded without the app's cookies, unlike on iOS. Use the
59
+ // same cookie store. A cookie jar the app set up itself is kept.
60
+ if (sharedClient.cookieJar() instanceof CookieJarContainer) {
61
+ builder.cookieJar(new JavaNetCookieJar(new FastImageCookieHandler(context)));
62
+ }
63
+ OkHttpClient client = builder.build();
48
64
  OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(client);
49
65
  registry.replace(GlideUrl.class, InputStream.class, factory);
66
+
67
+ // `cache: 'web'` skips Glide's caches and relies on HTTP caching, but
68
+ // React Native's shared client has no HTTP cache (unless the app gave
69
+ // it one), so those urls get a client with one (#280). Other urls are
70
+ // cached by Glide, so they don't use it (that would store them twice).
71
+ OkHttpClient webClient = client.cache() != null
72
+ ? client
73
+ : client.newBuilder()
74
+ .cache(new Cache(new File(context.getCacheDir(), "fast-image-http-cache"), WEB_CACHE_SIZE))
75
+ .build();
76
+ registry.prepend(FastImageWebGlideUrl.class, InputStream.class, new WebUrlLoaderFactory(webClient));
77
+ }
78
+
79
+ // Loads FastImageWebGlideUrls with the given client.
80
+ private static class WebUrlLoaderFactory implements ModelLoaderFactory<FastImageWebGlideUrl, InputStream> {
81
+ private final OkHttpClient client;
82
+
83
+ WebUrlLoaderFactory(OkHttpClient client) {
84
+ this.client = client;
85
+ }
86
+
87
+ @NonNull
88
+ @Override
89
+ public ModelLoader<FastImageWebGlideUrl, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
90
+ final OkHttpUrlLoader loader = new OkHttpUrlLoader(client);
91
+ return new ModelLoader<FastImageWebGlideUrl, InputStream>() {
92
+ @Override
93
+ public LoadData<InputStream> buildLoadData(@NonNull FastImageWebGlideUrl model, int width, int height, @NonNull Options options) {
94
+ return loader.buildLoadData(model, width, height, options);
95
+ }
96
+
97
+ @Override
98
+ public boolean handles(@NonNull FastImageWebGlideUrl model) {
99
+ return true;
100
+ }
101
+ };
102
+ }
103
+
104
+ @Override
105
+ public void teardown() {
106
+ }
50
107
  }
51
108
 
52
109
  private static Interceptor createInterceptor(final ResponseProgressListener listener) {
@@ -92,7 +149,10 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
92
149
  @Override
93
150
  public void update(final String key, final long bytesRead, final long contentLength) {
94
151
  final FastImageProgressListener listener = LISTENERS.get(key);
95
- if (listener == null) {
152
+ // Without a Content-Length the total is unknown (-1), and a
153
+ // percentage can't be worked out from it, so don't send those. (It
154
+ // also looked like the last update, which stopped all updates.)
155
+ if (listener == null || contentLength <= 0) {
96
156
  return;
97
157
  }
98
158
  if (contentLength <= bytesRead) {
@@ -4,57 +4,100 @@ import android.graphics.drawable.Drawable;
4
4
 
5
5
  import com.bumptech.glide.load.DataSource;
6
6
  import com.bumptech.glide.load.engine.GlideException;
7
+ import com.bumptech.glide.load.model.GlideUrl;
8
+ import com.bumptech.glide.load.resource.gif.GifDrawable;
9
+ import com.bumptech.glide.request.Request;
7
10
  import com.bumptech.glide.request.RequestListener;
8
11
  import com.bumptech.glide.request.target.ImageViewTarget;
9
12
  import com.bumptech.glide.request.target.Target;
10
13
  import com.facebook.react.bridge.WritableMap;
11
14
  import com.facebook.react.bridge.WritableNativeMap;
12
- import com.facebook.react.uimanager.ThemedReactContext;
13
- import com.facebook.react.uimanager.events.RCTEventEmitter;
14
15
 
15
16
  public class FastImageRequestListener implements RequestListener<Drawable> {
16
17
  static final String REACT_ON_ERROR_EVENT = "onFastImageError";
17
18
  static final String REACT_ON_LOAD_EVENT = "onFastImageLoad";
18
19
  static final String REACT_ON_LOAD_END_EVENT = "onFastImageLoadEnd";
19
20
  private final String key;
21
+ private final FastImageSource source;
22
+ // Whether the request shows the previous image as a thumbnail meanwhile.
23
+ private final boolean thumbnail;
24
+ // False when loading the image that's showing again at a new size: the
25
+ // view shows it at its old size if that fails.
26
+ private final boolean events;
20
27
 
21
- FastImageRequestListener(String key) {
28
+ FastImageRequestListener(String key, FastImageSource source, boolean thumbnail, boolean events) {
22
29
  this.key = key;
23
- }
24
-
25
- private static WritableMap mapFromResource(Drawable resource) {
26
- WritableMap resourceData = new WritableNativeMap();
27
- resourceData.putInt("width", resource.getIntrinsicWidth());
28
- resourceData.putInt("height", resource.getIntrinsicHeight());
29
- return resourceData;
30
+ this.source = source;
31
+ this.thumbnail = thumbnail;
32
+ this.events = events;
30
33
  }
31
34
 
32
35
  @Override
33
36
  public boolean onLoadFailed(@androidx.annotation.Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
37
+ if (!events) return false;
34
38
  FastImageOkHttpProgressGlideModule.forget(key);
35
39
  if (!(target instanceof ImageViewTarget)) {
36
40
  return false;
37
41
  }
38
42
  FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
39
- ThemedReactContext context = (ThemedReactContext) view.getContext();
40
- RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class);
41
- int viewId = view.getId();
42
- eventEmitter.receiveEvent(viewId, REACT_ON_ERROR_EVENT, new WritableNativeMap());
43
- eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_END_EVENT, new WritableNativeMap());
43
+ view.onImageFailed(thumbnail);
44
+ WritableMap event = new WritableNativeMap();
45
+ event.putString("error", errorMessage(e));
46
+ FastImageEvents.send(view, REACT_ON_ERROR_EVENT, event);
47
+ FastImageEvents.send(view, REACT_ON_LOAD_END_EVENT);
44
48
  return false;
45
49
  }
46
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
+
47
64
  @Override
48
- public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
65
+ public boolean onResourceReady(Drawable resource, Object model, final Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
49
66
  if (!(target instanceof ImageViewTarget)) {
50
67
  return false;
51
68
  }
52
- FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
53
- ThemedReactContext context = (ThemedReactContext) view.getContext();
54
- RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class);
55
- int viewId = view.getId();
56
- eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_EVENT, mapFromResource(resource));
57
- eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_END_EVENT, new WritableNativeMap());
69
+ final FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
70
+ view.onImageLoaded();
71
+ if (resource instanceof GifDrawable) {
72
+ // The `loop` prop, or the file's own loop count as on iOS. Glide
73
+ // loops every GIF forever by default (#651).
74
+ view.applyLoopCount((GifDrawable) resource);
75
+ }
76
+ if (!events) return false;
77
+ boolean local = !(model instanceof GlideUrl);
78
+ int[] size = FastImageSourceSize.get(resource, model, local,
79
+ dataSource == DataSource.RESOURCE_DISK_CACHE);
80
+ if (size != null) {
81
+ sendLoad(view, size);
82
+ return false;
83
+ }
84
+ // A local image whose size isn't known: read it from the image first,
85
+ // unless the view starts another load meanwhile.
86
+ final Request request = target.getRequest();
87
+ FastImageSourceSize.readLocal(view.getContext(), source, resource, model, new FastImageSourceSize.Callback() {
88
+ @Override
89
+ public void onSize(int[] size) {
90
+ if (target.getRequest() == request) sendLoad(view, size);
91
+ }
92
+ });
58
93
  return false;
59
94
  }
95
+
96
+ private static void sendLoad(FastImageViewWithUrl view, int[] size) {
97
+ WritableMap event = new WritableNativeMap();
98
+ event.putInt("width", size[0]);
99
+ event.putInt("height", size[1]);
100
+ FastImageEvents.send(view, REACT_ON_LOAD_EVENT, event);
101
+ FastImageEvents.send(view, REACT_ON_LOAD_END_EVENT);
102
+ }
60
103
  }
@@ -0,0 +1,21 @@
1
+ package com.dylanvann.fastimage;
2
+
3
+ import com.facebook.react.uimanager.LayoutShadowNode;
4
+ import com.facebook.react.uimanager.UIViewOperationQueue;
5
+
6
+ // Legacy architecture only. React Native only lays out a view when its frame
7
+ // changes, so a view that stays 0×0 at its parent's origin is never laid out,
8
+ // and Glide waits for its size forever (#865). When the computed size is zero,
9
+ // tell the view, so it can lay itself out and load as it does on the New
10
+ // Architecture. Remove with legacy architecture support.
11
+ public class FastImageShadowNode extends LayoutShadowNode {
12
+ static final Object ZERO_LAYOUT = new Object();
13
+
14
+ @Override
15
+ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
16
+ super.onCollectExtraUpdates(uiViewOperationQueue);
17
+ if (Math.round(getLayoutWidth()) == 0 && Math.round(getLayoutHeight()) == 0) {
18
+ uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), ZERO_LAYOUT);
19
+ }
20
+ }
21
+ }
@@ -17,6 +17,7 @@ public class FastImageSource extends ImageSource {
17
17
  private static final String ANDROID_RESOURCE_SCHEME = "android.resource";
18
18
  private static final String ANDROID_CONTENT_SCHEME = "content";
19
19
  private static final String LOCAL_FILE_SCHEME = "file";
20
+ private static final String ASSET_SCHEME = "asset";
20
21
  private final Headers mHeaders;
21
22
  private Uri mUri;
22
23
 
@@ -40,6 +41,10 @@ public class FastImageSource extends ImageSource {
40
41
  return LOCAL_FILE_SCHEME.equals(uri.getScheme());
41
42
  }
42
43
 
44
+ public static boolean isAssetUri(Uri uri) {
45
+ return ASSET_SCHEME.equals(uri.getScheme());
46
+ }
47
+
43
48
  public FastImageSource(Context context, String source) {
44
49
  this(context, source, null);
45
50
  }
@@ -62,6 +67,12 @@ public class FastImageSource extends ImageSource {
62
67
  // glide can understand the uri.
63
68
  mUri = Uri.parse(mUri.toString().replace("res:/", ANDROID_RESOURCE_SCHEME + "://" + context.getPackageName() + "/"));
64
69
  }
70
+
71
+ if (isAssetUri(mUri) && mUri.getPath() != null) {
72
+ // Convert asset:/ (a file in the app's assets, as React Native's
73
+ // Image supports) to file:///android_asset/, which Glide loads.
74
+ mUri = Uri.parse(LOCAL_FILE_SCHEME + ":///android_asset" + mUri.getPath());
75
+ }
65
76
  }
66
77
 
67
78
 
@@ -106,7 +117,16 @@ public class FastImageSource extends ImageSource {
106
117
  return mHeaders;
107
118
  }
108
119
 
120
+ // Set for `cache: 'web'`.
121
+ private boolean mWebCache = false;
122
+
123
+ void setWebCache(boolean webCache) {
124
+ mWebCache = webCache;
125
+ }
126
+
109
127
  public GlideUrl getGlideUrl() {
110
- return new GlideUrl(getUri().toString(), getHeaders());
128
+ return mWebCache
129
+ ? new FastImageWebGlideUrl(getUri().toString(), getHeaders())
130
+ : new GlideUrl(getUri().toString(), getHeaders());
111
131
  }
112
132
  }