react-native-fast-image 9.3.3 → 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.
@@ -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.3",
3
+ "version": "9.3.4",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",