@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
@@ -1,23 +1,31 @@
1
1
  package com.dylanvann.fastimage;
2
2
 
3
3
  import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT;
4
+ import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_LOAD_END_EVENT;
4
5
 
5
6
  import android.annotation.SuppressLint;
6
7
  import android.content.Context;
8
+ import android.graphics.drawable.BitmapDrawable;
7
9
  import android.graphics.drawable.Drawable;
8
10
 
11
+ import androidx.annotation.NonNull;
9
12
  import androidx.annotation.Nullable;
10
13
  import androidx.appcompat.widget.AppCompatImageView;
14
+ import androidx.core.view.ViewCompat;
11
15
 
12
16
  import com.bumptech.glide.RequestBuilder;
13
17
  import com.bumptech.glide.RequestManager;
14
18
  import com.bumptech.glide.load.model.GlideUrl;
19
+ import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
20
+ import com.bumptech.glide.load.resource.gif.GifDrawable;
15
21
  import com.bumptech.glide.request.Request;
22
+ import com.bumptech.glide.request.RequestOptions;
23
+ import com.bumptech.glide.request.target.DrawableImageViewTarget;
24
+ import com.bumptech.glide.request.target.SizeReadyCallback;
25
+ import com.bumptech.glide.request.transition.Transition;
16
26
  import com.facebook.react.bridge.ReadableMap;
17
27
  import com.facebook.react.bridge.WritableMap;
18
28
  import com.facebook.react.bridge.WritableNativeMap;
19
- import com.facebook.react.uimanager.ThemedReactContext;
20
- import com.facebook.react.uimanager.events.RCTEventEmitter;
21
29
 
22
30
  import java.util.ArrayList;
23
31
  import java.util.Collections;
@@ -32,9 +40,13 @@ class FastImageViewWithUrl extends AppCompatImageView {
32
40
  private Drawable mDefaultSource = null;
33
41
 
34
42
  public GlideUrl glideUrl;
43
+ // Null when the view was created in a destroyed Activity (nothing loads).
44
+ @Nullable
45
+ final RequestManager requestManager;
35
46
 
36
- public FastImageViewWithUrl(Context context) {
47
+ public FastImageViewWithUrl(Context context, @Nullable RequestManager requestManager) {
37
48
  super(context);
49
+ this.requestManager = requestManager;
38
50
  }
39
51
 
40
52
  public void setSource(@Nullable ReadableMap source) {
@@ -47,8 +59,286 @@ class FastImageViewWithUrl extends AppCompatImageView {
47
59
  mDefaultSource = source;
48
60
  }
49
61
 
50
- private boolean isNullOrEmpty(final String url) {
51
- return url == null || url.trim().isEmpty();
62
+ // Legacy architecture only (see FastImageShadowNode): the view's layout is
63
+ // 0×0 at its parent's origin, which React Native never applies. Lay it out
64
+ // at that size, as the New Architecture does, so Glide stops waiting for a
65
+ // size and loads it (#865).
66
+ void onZeroLayout() {
67
+ if (!ViewCompat.isLaidOut(this)) layout(0, 0, 0, 0);
68
+ }
69
+
70
+ // How many times GIFs play: -1 for the file's own loop count (the `loop`
71
+ // prop not set), 0 for forever, or a number of times.
72
+ private int mLoopCount = -1;
73
+
74
+ public void setLoopCount(int loopCount) {
75
+ if (loopCount == mLoopCount) return;
76
+ mLoopCount = loopCount;
77
+ // Apply it to the GIF that's showing, and play it again. Only this
78
+ // view's own animation: restarting one Glide shares throws while
79
+ // another view plays it.
80
+ if (mOwnGif != null && getDrawable() == mOwnGif) {
81
+ applyLoopCount(mOwnGif);
82
+ if (!mPaused) {
83
+ mOwnGif.stop();
84
+ mOwnGif.startFromFirstFrame();
85
+ }
86
+ }
87
+ }
88
+
89
+ // Pauses GIFs on the frame they're showing (the view's own animation).
90
+ private boolean mPaused = false;
91
+
92
+ public void setPaused(boolean paused) {
93
+ if (paused == mPaused) return;
94
+ mPaused = paused;
95
+ if (mOwnGif != null && getDrawable() == mOwnGif) {
96
+ if (paused) {
97
+ mOwnGif.stop();
98
+ } else {
99
+ // Continues the loop count, as on iOS.
100
+ FastImageGif.resume(mOwnGif);
101
+ }
102
+ }
103
+ }
104
+
105
+ // The GIF this view shows as its own animation (FastImageGif), recycled
106
+ // when the view stops showing it.
107
+ @Nullable
108
+ private GifDrawable mOwnGif;
109
+
110
+ // Shows each GIF as this view's own animation. Glide's target also starts
111
+ // and stops it with the Activity, as it does Glide's own GifDrawable.
112
+ private final class OwnGifTarget extends DrawableImageViewTarget {
113
+ OwnGifTarget() {
114
+ super(FastImageViewWithUrl.this);
115
+ }
116
+
117
+ @Override
118
+ public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
119
+ if (resource instanceof BitmapDrawable) {
120
+ if (mPixelated) {
121
+ // Scaled by the view without filtering: sharp pixels.
122
+ resource = resource.mutate();
123
+ resource.setFilterBitmap(false);
124
+ }
125
+ }
126
+ if (resource instanceof GifDrawable) {
127
+ GifDrawable own = FastImageGif.copy(
128
+ getContext(), (GifDrawable) resource, mLoadingWidth, mLoadingHeight);
129
+ if (own != null) {
130
+ applyLoopCount(own);
131
+ super.onResourceReady(own, transition);
132
+ mOwnGif = own;
133
+ // The target starts it; paused, it waits on its first frame.
134
+ if (mPaused) own.stop();
135
+ return;
136
+ }
137
+ }
138
+ super.onResourceReady(resource, transition);
139
+ }
140
+
141
+ // The target starts animations again when the Activity does: not a
142
+ // paused one, and the view's own GIF continues its loop count (start()
143
+ // would count again).
144
+ @Override
145
+ public void onStart() {
146
+ if (mPaused) return;
147
+ if (mOwnGif != null && getDrawable() == mOwnGif) {
148
+ FastImageGif.resume(mOwnGif);
149
+ } else {
150
+ super.onStart();
151
+ }
152
+ }
153
+
154
+ @Override
155
+ protected void setResource(@Nullable Drawable resource) {
156
+ super.setResource(resource);
157
+ // Not shown anymore (replaced, cleared or failed): free its frames.
158
+ if (mOwnGif != null && mOwnGif != resource) {
159
+ mOwnGif.stop();
160
+ mOwnGif.recycle();
161
+ mOwnGif = null;
162
+ }
163
+ }
164
+ }
165
+
166
+ void applyLoopCount(GifDrawable gif) {
167
+ gif.setLoopCount(mLoopCount == -1 ? GifDrawable.LOOP_INTRINSIC
168
+ : mLoopCount == 0 ? GifDrawable.LOOP_FOREVER
169
+ : mLoopCount);
170
+ }
171
+
172
+ // Glide crops or fits the bitmap for the scale type when it loads, so a
173
+ // new resizeMode needs a reload to take effect (#762).
174
+ public void setResizeMode(ScaleType scaleType) {
175
+ if (scaleType == getScaleType()) return;
176
+ setScaleType(scaleType);
177
+ mNeedsReload = true;
178
+ }
179
+
180
+ // imageRendering="pixelated": no resampling by Glide, and drawn without
181
+ // filtering (sharp pixels). "smooth" is iOS only; here it's the same as
182
+ // "auto": Glide already decodes a large image at about the view's size, and
183
+ // averaging it properly would need a full-size decode. Part of the
184
+ // request, so a change reloads.
185
+ private boolean mPixelated = false;
186
+
187
+ public void setImageRendering(@Nullable String imageRendering) {
188
+ boolean pixelated = "pixelated".equals(imageRendering);
189
+ if (pixelated == mPixelated) return;
190
+ mPixelated = pixelated;
191
+ mNeedsReload = true;
192
+ }
193
+
194
+ // The scale type's options (what into() would apply), or none when
195
+ // pixelated.
196
+ private RequestOptions renderingOptions(Object model) {
197
+ if (mPixelated) {
198
+ // Decoded as it is, then scaled by the view without filtering (see
199
+ // OwnGifTarget).
200
+ return new RequestOptions()
201
+ .downsample(new FastImageSourceSize.Capture(DownsampleStrategy.NONE, String.valueOf(model)))
202
+ .dontTransform();
203
+ }
204
+ return FastImageSourceSize.scaleTypeOptions(getScaleType(), FastImageSourceSize.capture(getScaleType(), model));
205
+ }
206
+
207
+ // The request for the image the view shows once it has loaded, and the one
208
+ // loading. A new source starts with the shown one as a thumbnail, from the
209
+ // cache only, so the image stays until the new one has loaded instead of
210
+ // flashing blank (#747). This is Glide's safe way to do that: the previous
211
+ // bitmap can't be kept in the view itself, since Glide reuses it once its
212
+ // request is cleared.
213
+ @Nullable
214
+ private RequestBuilder<Drawable> mShownRequest;
215
+ @Nullable
216
+ private RequestBuilder<Drawable> mLoadingRequest;
217
+ // Counts loads, to tell whether a posted update is for the current one.
218
+ private int mLoadCount = 0;
219
+ // The size Glide loaded the shown image at, and is loading the loading
220
+ // one at (0 until it has one).
221
+ private int mShownWidth = 0;
222
+ private int mShownHeight = 0;
223
+ private int mLoadingWidth = 0;
224
+ private int mLoadingHeight = 0;
225
+ private boolean mResizeReloadPosted = false;
226
+
227
+ @Nullable
228
+ private String mRecyclingKey;
229
+
230
+ // When it changes, the next image doesn't replace the current one: the
231
+ // view clears first (for views reused for other content, like list rows).
232
+ void setRecyclingKey(@Nullable String recyclingKey) {
233
+ if (recyclingKey == null ? mRecyclingKey == null : recyclingKey.equals(mRecyclingKey)) return;
234
+ boolean changed = mRecyclingKey != null;
235
+ mRecyclingKey = recyclingKey;
236
+ if (changed) {
237
+ // No thumbnail of the current image, so Glide clears the view to
238
+ // defaultSource (or nothing) while the next one loads. Reload even
239
+ // if the source is the same.
240
+ mShownRequest = null;
241
+ mNeedsReload = true;
242
+ }
243
+ }
244
+
245
+ // The loading image loaded (FastImageRequestListener).
246
+ void onImageLoaded() {
247
+ mShownRequest = mLoadingRequest;
248
+ mShownWidth = mLoadingWidth;
249
+ mShownHeight = mLoadingHeight;
250
+ reloadIfResized();
251
+ }
252
+
253
+ // Glide crops or scales the image to the view's size when it loads, so if
254
+ // the view's size changes after that, load it again at the new size.
255
+ // Otherwise a view that gets taller shows a zoomed-in slice of it (#983).
256
+ @Override
257
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
258
+ super.onSizeChanged(w, h, oldw, oldh);
259
+ reloadIfResized();
260
+ }
261
+
262
+ private void reloadIfResized() {
263
+ if (mResizeReloadPosted || !isResized()) return;
264
+ mResizeReloadPosted = true;
265
+ // After layout (once per frame), and not from within Glide's callback.
266
+ post(new Runnable() {
267
+ @Override
268
+ public void run() {
269
+ mResizeReloadPosted = false;
270
+ if (isResized()) reloadForSize();
271
+ }
272
+ });
273
+ }
274
+
275
+ // Whether the image loaded, at a size the view doesn't have now.
276
+ private boolean isResized() {
277
+ int width = getWidth() - getPaddingLeft() - getPaddingRight();
278
+ int height = getHeight() - getPaddingTop() - getPaddingBottom();
279
+ return requestManager != null && !mNeedsReload
280
+ && mShownRequest != null && mShownRequest == mLoadingRequest
281
+ && mShownWidth > 0 && mShownHeight > 0 && width > 0 && height > 0
282
+ && (width != mShownWidth || height != mShownHeight);
283
+ }
284
+
285
+ // The same image at the view's new size. Meanwhile, and if that fails, the
286
+ // view shows the image at its old size, from the cache. It's the image
287
+ // that's already showing, so this sends no events.
288
+ @SuppressLint("CheckResult")
289
+ private void reloadForSize() {
290
+ RequestBuilder<Drawable> shown = mShownRequest;
291
+ RequestBuilder<Drawable> current = fromCache(shown);
292
+ mLoadCount++;
293
+ clearView(requestManager);
294
+ into(shown, shown.clone()
295
+ .thumbnail(current)
296
+ .error(current.clone())
297
+ .listener(new FastImageRequestListener(null, null, true, false)));
298
+ }
299
+
300
+ // The shown image, from the cache only (never loaded again), at the size
301
+ // it was loaded at: the key it's in Glide's memory cache under.
302
+ @SuppressLint("CheckResult")
303
+ private RequestBuilder<Drawable> fromCache(RequestBuilder<Drawable> shown) {
304
+ RequestBuilder<Drawable> request = shown.clone().onlyRetrieveFromCache(true);
305
+ if (mShownWidth > 0 && mShownHeight > 0) request = request.override(mShownWidth, mShownHeight);
306
+ return request;
307
+ }
308
+
309
+ // Starts loading the request (built by builder), recording the size Glide
310
+ // loads it at. The size callback is added first, so it has the size by
311
+ // the time the image loads, even from the memory cache.
312
+ private void into(RequestBuilder<Drawable> request, RequestBuilder<Drawable> builder) {
313
+ final int load = mLoadCount;
314
+ mLoadingRequest = request;
315
+ mLoadingWidth = 0;
316
+ mLoadingHeight = 0;
317
+ OwnGifTarget target = new OwnGifTarget();
318
+ target.getSize(new SizeReadyCallback() {
319
+ @Override
320
+ public void onSizeReady(int width, int height) {
321
+ if (load != mLoadCount) return;
322
+ mLoadingWidth = width;
323
+ mLoadingHeight = height;
324
+ }
325
+ });
326
+ builder.into(target);
327
+ }
328
+
329
+ // The loading image failed (FastImageRequestListener). Glide shows
330
+ // defaultSource then, but not over a thumbnail: show it here, as iOS does.
331
+ void onImageFailed(boolean hadThumbnail) {
332
+ mShownRequest = null;
333
+ if (!hadThumbnail) return;
334
+ final int load = mLoadCount;
335
+ // Not from within Glide's callback.
336
+ post(new Runnable() {
337
+ @Override
338
+ public void run() {
339
+ if (load == mLoadCount) setImageDrawable(mDefaultSource);
340
+ }
341
+ });
52
342
  }
53
343
 
54
344
  @SuppressLint("CheckResult")
@@ -58,18 +348,20 @@ class FastImageViewWithUrl extends AppCompatImageView {
58
348
  @Nonnull Map<String, List<FastImageViewWithUrl>> viewsForUrlsMap) {
59
349
  if (!mNeedsReload)
60
350
  return;
351
+ // Only reload for changes that affect the request (source,
352
+ // defaultSource, resizeMode), not for every prop update.
353
+ mNeedsReload = false;
354
+ mLoadCount++;
355
+ RequestBuilder<Drawable> shownRequest = mShownRequest;
356
+ mShownRequest = null;
357
+ mLoadingRequest = null;
61
358
 
62
- if ((mSource == null ||
63
- !mSource.hasKey("uri") ||
64
- isNullOrEmpty(mSource.getString("uri"))) &&
65
- mDefaultSource == null) {
66
-
359
+ // Nothing to show.
360
+ if (mSource == null && mDefaultSource == null) {
67
361
  // Cancel existing requests.
68
362
  clearView(requestManager);
69
363
 
70
- if (glideUrl != null) {
71
- FastImageOkHttpProgressGlideModule.forget(glideUrl.toStringUrl());
72
- }
364
+ untrackUrl(viewsForUrlsMap);
73
365
 
74
366
  // Clear the image.
75
367
  setImageDrawable(null);
@@ -77,36 +369,41 @@ class FastImageViewWithUrl extends AppCompatImageView {
77
369
  }
78
370
 
79
371
  //final GlideUrl glideUrl = FastImageViewConverter.getGlideUrl(view.getContext(), mSource);
80
- final FastImageSource imageSource = FastImageViewConverter.getImageSource(getContext(), mSource);
372
+ final FastImageSource imageSource = FastImageViewConverter.hasUri(mSource)
373
+ ? FastImageViewConverter.getImageSource(getContext(), mSource)
374
+ : null;
81
375
 
82
- if (imageSource != null && imageSource.getUri().toString().length() == 0) {
83
- ThemedReactContext context = (ThemedReactContext) getContext();
84
- RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class);
85
- int viewId = getId();
376
+ // A source without a uri (empty, missing or null), or one that can't be
377
+ // resolved: fire onError and onLoadEnd and show defaultSource, as on iOS
378
+ // (#1028, #945).
379
+ if (mSource != null && (imageSource == null || imageSource.getUri().toString().length() == 0)) {
86
380
  WritableMap event = new WritableNativeMap();
87
- event.putString("message", "Invalid source prop:" + mSource);
88
- eventEmitter.receiveEvent(viewId, REACT_ON_ERROR_EVENT, event);
381
+ event.putString("error", "Invalid source: " + mSource);
382
+ FastImageEvents.send(this, REACT_ON_ERROR_EVENT, event);
383
+ FastImageEvents.send(this, REACT_ON_LOAD_END_EVENT);
89
384
 
90
385
  // Cancel existing requests.
91
386
  clearView(requestManager);
92
387
 
93
- if (glideUrl != null) {
94
- FastImageOkHttpProgressGlideModule.forget(glideUrl.toStringUrl());
95
- }
96
- // Clear the image.
97
- setImageDrawable(null);
388
+ untrackUrl(viewsForUrlsMap);
389
+ setImageDrawable(mDefaultSource);
98
390
  return;
99
391
  }
100
392
 
101
393
  // `imageSource` may be null and we still continue, if `defaultSource` is not null
102
394
  final GlideUrl glideUrl = imageSource == null ? null : imageSource.getGlideUrl();
103
395
 
396
+ String key = glideUrl == null ? null : glideUrl.toStringUrl();
397
+
398
+ // Loading a different url: stop tracking the old one.
399
+ if (this.glideUrl != null && !this.glideUrl.toStringUrl().equals(key)) {
400
+ untrackUrl(viewsForUrlsMap);
401
+ }
402
+
104
403
  // Cancel existing request.
105
404
  this.glideUrl = glideUrl;
106
405
  clearView(requestManager);
107
406
 
108
- String key = glideUrl == null ? null : glideUrl.toStringUrl();
109
-
110
407
  if (glideUrl != null) {
111
408
  FastImageOkHttpProgressGlideModule.expect(key, manager);
112
409
  List<FastImageViewWithUrl> viewsForKey = viewsForUrlsMap.get(key);
@@ -118,18 +415,14 @@ class FastImageViewWithUrl extends AppCompatImageView {
118
415
  }
119
416
  }
120
417
 
121
- ThemedReactContext context = (ThemedReactContext) getContext();
122
418
  if (imageSource != null) {
123
419
  // This is an orphan even without a load/loadend when only loading a placeholder
124
- RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class);
125
- int viewId = this.getId();
126
-
127
- eventEmitter.receiveEvent(viewId,
128
- FastImageViewManager.REACT_ON_LOAD_START_EVENT,
129
- new WritableNativeMap());
420
+ FastImageEvents.send(this, FastImageViewManager.REACT_ON_LOAD_START_EVENT);
130
421
  }
131
422
 
132
423
  if (requestManager != null) {
424
+ // Records the image's own size when Glide decodes it, for onLoad.
425
+ Object model = imageSource == null ? null : imageSource.getSourceForLoad();
133
426
  RequestBuilder<Drawable> builder =
134
427
  requestManager
135
428
  // This will make this work for remote and local images. e.g.
@@ -138,17 +431,44 @@ class FastImageViewWithUrl extends AppCompatImageView {
138
431
  // - res:/
139
432
  // - android.resource://
140
433
  // - data:image/png;base64
141
- .load(imageSource == null ? null : imageSource.getSourceForLoad())
434
+ .load(model)
142
435
  .apply(FastImageViewConverter
143
- .getOptions(context, imageSource, mSource)
436
+ .getOptions(getContext(), imageSource, mSource)
144
437
  .placeholder(mDefaultSource) // show until loaded
145
- .fallback(mDefaultSource)); // null will not be treated as error
438
+ .fallback(mDefaultSource)) // null will not be treated as error
439
+ // What into() would apply for the scale type, with
440
+ // the size capture, for imageRendering.
441
+ .apply(renderingOptions(model));
442
+ RequestBuilder<Drawable> request = builder.clone();
443
+
444
+ boolean thumbnail = shownRequest != null && model != null;
445
+ if (thumbnail) {
446
+ builder = builder.thumbnail(fromCache(shownRequest));
447
+ }
146
448
 
147
449
  if (key != null)
148
- builder.listener(new FastImageRequestListener(key));
450
+ builder.listener(new FastImageRequestListener(key, imageSource, thumbnail, true));
149
451
 
150
- builder.into(this);
452
+ into(request, builder);
453
+ }
454
+ }
455
+
456
+ // Removes this view from the list of views for its current url (used to send
457
+ // progress events), which otherwise kept it, and its Activity, alive after
458
+ // its source changed (#384). The url's progress listener is only forgotten
459
+ // when no other view uses it.
460
+ void untrackUrl(@Nonnull Map<String, List<FastImageViewWithUrl>> viewsForUrlsMap) {
461
+ if (glideUrl == null) return;
462
+ String key = glideUrl.toStringUrl();
463
+ List<FastImageViewWithUrl> viewsForKey = viewsForUrlsMap.get(key);
464
+ if (viewsForKey != null) {
465
+ viewsForKey.remove(this);
466
+ if (viewsForKey.isEmpty()) viewsForUrlsMap.remove(key);
467
+ }
468
+ if (viewsForKey == null || viewsForKey.isEmpty()) {
469
+ FastImageOkHttpProgressGlideModule.forget(key);
151
470
  }
471
+ glideUrl = null;
152
472
  }
153
473
 
154
474
  public void clearView(@Nullable RequestManager requestManager) {
@@ -0,0 +1,13 @@
1
+ package com.dylanvann.fastimage;
2
+
3
+ import com.bumptech.glide.load.model.GlideUrl;
4
+ import com.bumptech.glide.load.model.Headers;
5
+
6
+ // A url loaded with `cache: 'web'`. Glide doesn't cache these, so they load
7
+ // through an OkHttp client with an HTTP cache (see
8
+ // FastImageOkHttpProgressGlideModule) to follow the server's caching headers.
9
+ class FastImageWebGlideUrl extends GlideUrl {
10
+ FastImageWebGlideUrl(String url, Headers headers) {
11
+ super(url, headers);
12
+ }
13
+ }
package/changes.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "bumped": {},
3
- "timestamp": "2026-03-18T22:43:43.986Z",
3
+ "timestamp": "2026-09-27T01:02:36.380Z",
4
4
  "totalUpdated": 0
5
5
  }