From e6414dc162e123c620f67c02a5234f74725eb48f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Davidovi=C4=87?= <david@davidovic.io>
Date: Tue, 23 Jul 2024 10:41:14 +0200
Subject: [PATCH 1/4] Apply blend modes on layer instead of fill level

---
 .../java/com/airbnb/lottie/animation/content/FillContent.java | 3 ---
 .../main/java/com/airbnb/lottie/model/layer/BaseLayer.java    | 4 +++-
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java
index 1690989c05..8dc05b877c 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java
@@ -11,7 +11,6 @@
 import android.graphics.RectF;
 
 import androidx.annotation.Nullable;
-import androidx.core.graphics.PaintCompat;
 
 import com.airbnb.lottie.L;
 import com.airbnb.lottie.LottieDrawable;
@@ -68,8 +67,6 @@ public FillContent(final LottieDrawable lottieDrawable, BaseLayer layer, ShapeFi
       return;
     }
 
-    PaintCompat.setBlendMode(paint, layer.getBlendMode().toNativeBlendMode());
-
     path.setFillType(fill.getFillType());
 
     colorAnimation = fill.getColor().createAnimation();
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
index 5a4afc01b8..f0c76a39c1 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
@@ -14,6 +14,7 @@
 import androidx.annotation.CallSuper;
 import androidx.annotation.FloatRange;
 import androidx.annotation.Nullable;
+import androidx.core.graphics.PaintCompat;
 
 import com.airbnb.lottie.L;
 import com.airbnb.lottie.LottieComposition;
@@ -258,7 +259,7 @@ public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
       }
     }
     int alpha = (int) ((parentAlpha / 255f * (float) opacity / 100f) * 255);
-    if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
+    if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer() && getBlendMode() == LBlendMode.NORMAL) {
       matrix.preConcat(transform.getMatrix());
       if (L.isTraceEnabled()) {
         L.beginSection("Layer#drawLayer");
@@ -307,6 +308,7 @@ public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
         L.beginSection("Layer#saveLayer");
       }
       contentPaint.setAlpha(255);
+      PaintCompat.setBlendMode(contentPaint, getBlendMode().toNativeBlendMode());
       Utils.saveLayerCompat(canvas, rect, contentPaint);
       if (L.isTraceEnabled()) {
         L.endSection("Layer#saveLayer");

From 32166a61fc18cdfc6e1867922259bb58501ade5f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Davidovi=C4=87?= <david@davidovic.io>
Date: Tue, 23 Jul 2024 10:51:32 +0200
Subject: [PATCH 2/4] Add support for Multiply blend mode

BlendModeCompat is designed to use either a BlendMode (added in Android
Q) or PorterDuff.Mode (always available).

Our support for Lottie blend modes did not include Multiply due to a
slightly different formula between the PorterDuff and BlendMode
variants.

However, we did include support for Screen, which suffers from a similar
behavior. Therefore, we are not breaking any consistency by including
Multiply too, and including it provides benefits in terms of more
complete support, as Multiply is a foundational blend mode.
---
 .../main/java/com/airbnb/lottie/model/content/LBlendMode.java  | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/LBlendMode.java b/lottie/src/main/java/com/airbnb/lottie/model/content/LBlendMode.java
index e1c91a0acd..3ec6d20647 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/LBlendMode.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/LBlendMode.java
@@ -33,6 +33,8 @@ public BlendModeCompat toNativeBlendMode() {
     switch (this) {
       case NORMAL:
         return null;
+      case MULTIPLY:
+        return BlendModeCompat.MULTIPLY;
       case SCREEN:
         return BlendModeCompat.SCREEN;
       case OVERLAY:
@@ -48,7 +50,6 @@ public BlendModeCompat toNativeBlendMode() {
       // To prevent unexpected issues where animations look correct
       // during development but silently break for users with older devices
       // we won't support any of these until Q is widely used.
-      case MULTIPLY:
       case COLOR_DODGE:
       case COLOR_BURN:
       case HARD_LIGHT:

From 89afb07c0391566f92103d0293d226412ac29f83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Davidovi=C4=87?= <david@davidovic.io>
Date: Wed, 24 Jul 2024 19:50:11 +0200
Subject: [PATCH 3/4] Use BlendModeCompat.MODULATE instead of MULTIPLY

"Proper" MULTIPLY seems to have been added in Android Q, so use the
older MODULATE with a slight hack to ensure proper rendering in our
usecase.
---
 .../airbnb/lottie/model/content/LBlendMode.java | 14 +++++++++++++-
 .../airbnb/lottie/model/layer/BaseLayer.java    | 17 ++++++++++++++++-
 2 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/LBlendMode.java b/lottie/src/main/java/com/airbnb/lottie/model/content/LBlendMode.java
index 3ec6d20647..4a4012b05d 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/LBlendMode.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/LBlendMode.java
@@ -34,7 +34,19 @@ public BlendModeCompat toNativeBlendMode() {
       case NORMAL:
         return null;
       case MULTIPLY:
-        return BlendModeCompat.MULTIPLY;
+        // BlendModeCompat.MULTIPLY does not exist on Android < Q. Instead, there's
+        // BlendModeCompat.MODULATE, which maps to PorterDuff.Mode.MODULATE and not
+        // PorterDuff.Mode.MULTIPLY.
+        //
+        // MODULATE differs from MULTIPLY in that it doesn't perform
+        // any alpha blending. It just does a component-wise multiplication
+        // of the colors.
+        //
+        // For proper results on all platforms, we will map the MULTIPLY
+        // blend mode to MODULATE, and then do a slight adjustment to
+        // how we render such layers to still achieve the correct result.
+        // See BaseLayer.draw().
+        return BlendModeCompat.MODULATE;
       case SCREEN:
         return BlendModeCompat.SCREEN;
       case OVERLAY:
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
index f0c76a39c1..b4004c1272 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
@@ -315,7 +315,22 @@ public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
       }
 
       // Clear the off screen buffer. This is necessary for some phones.
-      clearCanvas(canvas);
+      if (getBlendMode() != LBlendMode.MULTIPLY) {
+        clearCanvas(canvas);
+      } else {
+        // Due to the difference between PorterDuffMode.MULTIPLY (which we use for compatibility
+        // with Android < Q) and BlendMode.MULTIPLY (which is the correct, alpha-blended mode),
+        // we will alpha-blend the contents of this layer on top of a white background before
+        // we multiply it with the opaque substrate below (with canvas.restore()).
+        //
+        // Since white is the identity color for multiplication, this will behave as if we
+        // had correctly performed an alpha-blended multiply (such as BlendMode.MULTIPLY), but
+        // will work pre-Q as well.
+        Paint solidWhite = new Paint();
+        solidWhite.setColor(0xffffffff);
+        canvas.drawRect(rect.left - 1, rect.top - 1, rect.right + 1, rect.bottom + 1, solidWhite);
+      }
+
       if (L.isTraceEnabled()) {
         L.beginSection("Layer#drawLayer");
       }

From 22db8bbb552dfbe7c2ca31b8005cc521cf8bc429 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Davidovi=C4=87?= <david@davidovic.io>
Date: Wed, 24 Jul 2024 21:21:55 +0200
Subject: [PATCH 4/4] Don't allocate solidWhite all of the time

---
 .../java/com/airbnb/lottie/model/layer/BaseLayer.java  | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
index b4004c1272..5e8c48953a 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
@@ -118,6 +118,8 @@ static BaseLayer forModel(
   float blurMaskFilterRadius = 0f;
   @Nullable BlurMaskFilter blurMaskFilter;
 
+  @Nullable LPaint solidWhitePaint;
+
   BaseLayer(LottieDrawable lottieDrawable, Layer layerModel) {
     this.lottieDrawable = lottieDrawable;
     this.layerModel = layerModel;
@@ -326,9 +328,11 @@ public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
         // Since white is the identity color for multiplication, this will behave as if we
         // had correctly performed an alpha-blended multiply (such as BlendMode.MULTIPLY), but
         // will work pre-Q as well.
-        Paint solidWhite = new Paint();
-        solidWhite.setColor(0xffffffff);
-        canvas.drawRect(rect.left - 1, rect.top - 1, rect.right + 1, rect.bottom + 1, solidWhite);
+        if (solidWhitePaint == null) {
+          solidWhitePaint = new LPaint();
+          solidWhitePaint.setColor(0xffffffff);
+        }
+        canvas.drawRect(rect.left - 1, rect.top - 1, rect.right + 1, rect.bottom + 1, solidWhitePaint);
       }
 
       if (L.isTraceEnabled()) {