react-native-fast-image 9.3.2 → 9.3.4

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.
@@ -280,6 +280,11 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
280
280
  .asFile()
281
281
  .load(model)
282
282
  .apply(options)
283
+ // Not kept in or taken from the memory cache (asFile() skips
284
+ // it, but the options turn it back on): the file is only there
285
+ // while the disk cache keeps it, and Glide gave the path of
286
+ // one that clearDiskCache had deleted.
287
+ .skipMemoryCache(true)
283
288
  .listener(new RequestListener<File>() {
284
289
  @Override
285
290
  public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
@@ -307,6 +312,8 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
307
312
  .asFile()
308
313
  .load(model)
309
314
  .apply(options)
315
+ // On disk only, as in loadFile.
316
+ .skipMemoryCache(true)
310
317
  .onlyRetrieveFromCache(true)
311
318
  .submit()
312
319
  .get();
@@ -322,8 +329,10 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
322
329
  return result;
323
330
  }
324
331
 
325
- // Where getCachePath looks in the caches and downloads `web` images
326
- // (blocking calls, off the UI and modules threads).
332
+ // Where getCachePath looks in the caches and downloads `web` images,
333
+ // writeToCache stores files, configureCache measures the disk cache and
334
+ // clearDiskCache empties it (blocking calls, off the UI and modules
335
+ // threads).
327
336
  private static final Executor cachePathExecutor = Executors.newSingleThreadExecutor();
328
337
 
329
338
  private static WritableMap pathResult(File file) {
@@ -590,24 +599,33 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
590
599
  });
591
600
  }
592
601
 
593
- // On the native modules' thread: Glide clears its disk cache off the main
594
- // thread.
602
+ // On cachePathExecutor: Glide clears its disk cache off the main thread,
603
+ // and the native modules' thread runs every module's async methods (e.g.
604
+ // a fetch()'s request), which would wait for it. It's also in order with
605
+ // the disk work of getCachePath, writeToCache and configureCache there.
595
606
  @ReactMethod
596
- public void clearDiskCache(Promise promise) {
597
- try {
598
- FastImageGlide.getRegistered(getReactApplicationContext()).clearDiskCache();
599
- } catch (RuntimeException e) {
600
- promise.resolve(failure(e));
601
- return;
602
- }
603
- // And the HTTP cache of `cache: 'web'` images, which Glide doesn't
604
- // cache (set up when FastImage's components are registered).
605
- try {
606
- FastImageOkHttpProgressGlideModule.clearWebCache();
607
- } catch (IOException e) {
608
- // Cleared as far as it could.
609
- }
610
- promise.resolve(cleared());
607
+ public void clearDiskCache(final Promise promise) {
608
+ final ReactApplicationContext context = getReactApplicationContext();
609
+ cachePathExecutor.execute(new Runnable() {
610
+ @Override
611
+ public void run() {
612
+ try {
613
+ FastImageGlide.getRegistered(context).clearDiskCache();
614
+ } catch (RuntimeException e) {
615
+ promise.resolve(failure(e));
616
+ return;
617
+ }
618
+ // And the HTTP cache of `cache: 'web'` images, which Glide
619
+ // doesn't cache (set up when FastImage's components are
620
+ // registered).
621
+ try {
622
+ FastImageOkHttpProgressGlideModule.clearWebCache();
623
+ } catch (IOException e) {
624
+ // Cleared as far as it could.
625
+ }
626
+ promise.resolve(cleared());
627
+ }
628
+ });
611
629
  }
612
630
 
613
631
  private static WritableMap cleared() {
@@ -11,13 +11,18 @@ import android.graphics.Bitmap;
11
11
  import android.graphics.Canvas;
12
12
  import android.graphics.Matrix;
13
13
  import android.graphics.Paint;
14
+ import android.graphics.PixelFormat;
14
15
  import android.graphics.PorterDuff;
15
16
  import android.graphics.PorterDuffXfermode;
17
+ import android.graphics.RectF;
16
18
  import android.graphics.Shader;
17
19
  import android.graphics.drawable.AnimatedImageDrawable;
18
20
  import android.graphics.drawable.BitmapDrawable;
19
21
  import android.graphics.drawable.Drawable;
22
+ import android.graphics.drawable.GradientDrawable;
20
23
  import android.os.Build;
24
+ import android.view.View;
25
+ import android.view.ViewParent;
21
26
 
22
27
  import androidx.annotation.NonNull;
23
28
  import androidx.annotation.Nullable;
@@ -342,6 +347,14 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
342
347
  private ValueAnimator mFade;
343
348
  @Nullable
344
349
  private Bitmap mFadeFrom;
350
+ // Or, instead of a copy, the defaultSource the view showed, with the
351
+ // matrix it drew it with: FastImage's own drawable, which Glide doesn't
352
+ // recycle, so it's drawn again as it was, rather than copied into a
353
+ // bitmap the size of the view for every image that fades in over it.
354
+ @Nullable
355
+ private Drawable mFadeFromDrawable;
356
+ @Nullable
357
+ private Matrix mFadeFromMatrix;
345
358
  private float mFadeProgress = 1f;
346
359
  private final Paint mFadeFromPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
347
360
  private final Paint mFadeInPaint = new Paint();
@@ -357,7 +370,14 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
357
370
  if (width <= 0 || height <= 0) return;
358
371
  // What the view shows now, including a fade in progress.
359
372
  Bitmap from = null;
360
- if (getDrawable() != null) {
373
+ Drawable fromDrawable = null;
374
+ Matrix fromMatrix = null;
375
+ Drawable shown = getDrawable();
376
+ if (shown != null && mShowsDefaultSource && mFade == null) {
377
+ // Its own state, so its alpha (see drawFadeFrom) is only its own.
378
+ fromDrawable = shown.mutate();
379
+ fromMatrix = new Matrix(getImageMatrix());
380
+ } else if (shown != null) {
361
381
  try {
362
382
  from = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
363
383
  onDraw(new Canvas(from));
@@ -371,6 +391,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
371
391
  }
372
392
  endFade();
373
393
  mFadeFrom = from;
394
+ mFadeFromDrawable = fromDrawable;
395
+ mFadeFromMatrix = fromMatrix;
374
396
  mFadeProgress = 0f;
375
397
  final ValueAnimator fade = ValueAnimator.ofFloat(0f, 1f);
376
398
  fade.setDuration(duration);
@@ -401,10 +423,45 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
401
423
  mFadeFrom.recycle();
402
424
  mFadeFrom = null;
403
425
  }
426
+ mFadeFromDrawable = null;
427
+ mFadeFromMatrix = null;
404
428
  mFadeProgress = 1f;
405
429
  invalidate();
406
430
  }
407
431
 
432
+ // Draws the defaultSource the view showed when the fade started (with
433
+ // `alpha`), as ImageView draws its drawable: inside the padding
434
+ // (cropToPadding), with the matrix it had then.
435
+ private void drawFadeFrom(Canvas canvas, Drawable drawable, int alpha) {
436
+ int count = canvas.save();
437
+ if (getCropToPadding()) {
438
+ int scrollX = getScrollX();
439
+ int scrollY = getScrollY();
440
+ canvas.clipRect(scrollX + getPaddingLeft(), scrollY + getPaddingTop(),
441
+ scrollX + getWidth() - getPaddingRight(), scrollY + getHeight() - getPaddingBottom());
442
+ }
443
+ canvas.translate(getPaddingLeft(), getPaddingTop());
444
+ if (mFadeFromMatrix != null) canvas.concat(mFadeFromMatrix);
445
+ if (alpha == 255) {
446
+ drawable.draw(canvas);
447
+ } else if (drawable instanceof BitmapDrawable || drawable instanceof GradientDrawable) {
448
+ // A bitmap or a shape applies its alpha as it's drawn.
449
+ int previous = drawable.getAlpha();
450
+ drawable.setAlpha(alpha);
451
+ drawable.draw(canvas);
452
+ drawable.setAlpha(previous);
453
+ } else {
454
+ // Others through a layer: a vector drawable, or an animated GIF or
455
+ // WebP resource (an AnimatedImageDrawable on Android 9+), keeps
456
+ // its alpha as a property the RenderThread reads when it syncs
457
+ // the frame, after it's set back here.
458
+ int layer = canvas.saveLayerAlpha(null, alpha);
459
+ drawable.draw(canvas);
460
+ canvas.restoreToCount(layer);
461
+ }
462
+ canvas.restoreToCount(count);
463
+ }
464
+
408
465
  @SuppressWarnings("deprecation")
409
466
  @Override
410
467
  protected void onDraw(Canvas canvas) {
@@ -415,23 +472,118 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
415
472
  int width = getWidth();
416
473
  int height = getHeight();
417
474
  int alpha = Math.round(255 * mFadeProgress);
418
- if (mFadeFrom == null) {
475
+ if (drawFadeWithoutLayers(canvas, alpha)) return;
476
+ if (mFadeFrom == null && mFadeFromDrawable == null) {
419
477
  // Fading in over nothing: one layer.
420
478
  int count = canvas.saveLayerAlpha(0, 0, width, height, alpha, Canvas.ALL_SAVE_FLAG);
421
479
  super.onDraw(canvas);
422
480
  canvas.restoreToCount(count);
423
481
  return;
424
482
  }
483
+ // Two layers, which add up to the cross-dissolve whatever the image's
484
+ // transparency.
425
485
  // Canvas.ALL_SAVE_FLAG: the saveLayer without flags is API 21+.
426
486
  int count = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
427
- mFadeFromPaint.setAlpha(255 - alpha);
428
- canvas.drawBitmap(mFadeFrom, 0, 0, mFadeFromPaint);
487
+ if (mFadeFromDrawable != null) {
488
+ drawFadeFrom(canvas, mFadeFromDrawable, 255 - alpha);
489
+ } else {
490
+ mFadeFromPaint.setAlpha(255 - alpha);
491
+ canvas.drawBitmap(mFadeFrom, 0, 0, mFadeFromPaint);
492
+ }
429
493
  mFadeInPaint.setAlpha(alpha);
430
494
  canvas.saveLayer(0, 0, width, height, mFadeInPaint, Canvas.ALL_SAVE_FLAG);
431
495
  super.onDraw(canvas);
432
496
  canvas.restoreToCount(count);
433
497
  }
434
498
 
499
+ // Draws the fade without layers, each of which is a buffer drawn again
500
+ // every frame, when the image takes an alpha in one drawing (a bitmap or
501
+ // GIF): over nothing, the image with the fade's alpha; over
502
+ // what the view showed, if the image is opaque and covers it, that and
503
+ // the image over it with the alpha, which is the same cross-dissolve,
504
+ // from * (1 - t) + image * t: photos without transparency, most images.
505
+ // Over what the view showed, only when nothing draws the view with an
506
+ // alpha (see drawnWithAlpha).
507
+ // Not an animated WebP or AVIF (an AnimatedImageDrawable): its alpha is a
508
+ // property the RenderThread reads when it syncs the frame, after it's set
509
+ // back here, so it would show without the fade.
510
+ // Returns false, having drawn nothing, otherwise.
511
+ private boolean drawFadeWithoutLayers(Canvas canvas, int alpha) {
512
+ Drawable image = getDrawable();
513
+ boolean drawsOnce = image instanceof BitmapDrawable || image instanceof GifDrawable;
514
+ if (!drawsOnce) return false;
515
+ boolean over = mFadeFrom != null || mFadeFromDrawable != null;
516
+ if (over && (!coversOpaquely(image) || drawnWithAlpha())) return false;
517
+ if (mFadeFromDrawable != null) {
518
+ drawFadeFrom(canvas, mFadeFromDrawable, 255);
519
+ } else if (mFadeFrom != null) {
520
+ mFadeFromPaint.setAlpha(255);
521
+ canvas.drawBitmap(mFadeFrom, 0, 0, mFadeFromPaint);
522
+ }
523
+ // Without the drawable asking the view to draw again.
524
+ Drawable.Callback callback = image.getCallback();
525
+ image.setCallback(null);
526
+ int previous = image.getAlpha();
527
+ image.setAlpha(alpha);
528
+ super.onDraw(canvas);
529
+ image.setAlpha(previous);
530
+ image.setCallback(callback);
531
+ return true;
532
+ }
533
+
534
+ // Whether the view, or a view it's in, has an alpha (an opacity), or a
535
+ // view animation (android.view.animation, e.g. an AlphaAnimation), which
536
+ // can apply one without changing the view's.
537
+ // React Native's View (unless it has needsOffscreenAlphaCompositing) and
538
+ // an ImageView without a background have no layer of their own for it,
539
+ // so it applies to each drawing in them: what the view showed, drawn at
540
+ // full strength, would show through the image over it until the fade
541
+ // ends. The layers are right with any alpha: it applies to the
542
+ // cross-dissolve they add up to. Checked on every frame of the fade, as
543
+ // an opacity can change during it.
544
+ // Not the transition alpha, which only Android's Transition fades
545
+ // (android.transition.Fade) set: getTransitionAlpha is public from API 29
546
+ // only (hidden on Android 5 to 9, and the library compiles against 28
547
+ // when an app doesn't set compileSdkVersion). React Native's opacity,
548
+ // Animated, LayoutAnimation and Reanimated set the view's alpha, and
549
+ // react-native-screens' fragment animations run on a container with a
550
+ // layer of its own.
551
+ private boolean drawnWithAlpha() {
552
+ View view = this;
553
+ while (true) {
554
+ if (view.getAlpha() < 1 || view.getAnimation() != null) {
555
+ return true;
556
+ }
557
+ ViewParent parent = view.getParent();
558
+ if (!(parent instanceof View)) return false;
559
+ view = (View) parent;
560
+ }
561
+ }
562
+
563
+ private final RectF mFadeImageRect = new RectF();
564
+
565
+ // Whether the image, as the view draws it, is opaque and covers all of
566
+ // what the view showed: inside the padding (cropToPadding), or the whole
567
+ // view.
568
+ private boolean coversOpaquely(Drawable image) {
569
+ if (image.getOpacity() != PixelFormat.OPAQUE || getColorFilter() != null) return false;
570
+ mFadeImageRect.set(image.getBounds());
571
+ getImageMatrix().mapRect(mFadeImageRect);
572
+ mFadeImageRect.offset(getPaddingLeft(), getPaddingTop());
573
+ boolean crop = getCropToPadding();
574
+ float left = crop ? getPaddingLeft() : 0;
575
+ float top = crop ? getPaddingTop() : 0;
576
+ float right = getWidth() - (crop ? getPaddingRight() : 0);
577
+ float bottom = getHeight() - (crop ? getPaddingBottom() : 0);
578
+ // Within float error only: an edge half a pixel in leaves a row or
579
+ // column of pixels uncovered (which one depends on how it's
580
+ // rasterized; e.g. contain, with the image a pixel shorter than the
581
+ // view), where what the view showed would stay until the fade ends.
582
+ float error = 0.01f;
583
+ return mFadeImageRect.left <= left + error && mFadeImageRect.top <= top + error
584
+ && mFadeImageRect.right >= right - error && mFadeImageRect.bottom >= bottom - error;
585
+ }
586
+
435
587
  @Override
436
588
  protected void onDetachedFromWindow() {
437
589
  endFade();
@@ -651,9 +803,15 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
651
803
  updateImageMatrix();
652
804
  }
653
805
 
806
+ // Whether the view shows its defaultSource (or its tiled copy, which has
807
+ // its bitmap): FastImage's own drawable, which Glide doesn't recycle (see
808
+ // startFade).
809
+ private boolean mShowsDefaultSource = false;
810
+
654
811
  // Repeats what the view shows (the loaded image, and defaultSource).
655
812
  @Override
656
813
  public void setImageDrawable(@Nullable Drawable drawable) {
814
+ mShowsDefaultSource = drawable != null && drawable == mDefaultSource;
657
815
  super.setImageDrawable(mRepeat ? tiled(drawable) : drawable);
658
816
  updateImageMatrix();
659
817
  }
@@ -905,6 +1063,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
905
1063
  mFadeFrom = null;
906
1064
  invalidate();
907
1065
  }
1066
+ if (mFadeFromDrawable != null && (w != oldw || h != oldh)) {
1067
+ mFadeFromDrawable = null;
1068
+ mFadeFromMatrix = null;
1069
+ invalidate();
1070
+ }
908
1071
  if (!switchSourceIfResized()) reloadIfResized();
909
1072
  }
910
1073
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "9.3.2",
3
+ "version": "9.3.4",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",