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()) {