Skip to content

Commit 328fc72

Browse files
authored
Apply blend modes on layer level and add Multiply blend mode (#2519)
This commit improves blend mode support in lottie-android in two ways: * Applying blend modes on layer-level, instead of fill level * Adding support for the Multiply blend mode ## Applying blend modes on layer level The Lottie format defines blend modes as attributes on a layer. However, lottie-android is presently applying the layer blend modes on a solid color fill only. Notably, this causes any stroked or gradient-filled shapes or image layers to blend incorrectly, such as in this file: [stroke-blending-test.json](https://github.com/user-attachments/files/16346206/stroke-blending-test.json) (The file contains a filled + stroked shape that renders as a pink square on other platforms, but renders with a visible stroke on lottie-android since its blend mode is applied only on the fill.) Instead, we move this decision to `BaseLayer` by analogy to transparent layer handling, which is closer to how the format specifies the property and fixes these cases. ## Multiply support `BlendModeCompat` is designed to resolve to either a `BlendMode` (added in Android Q, supporting most modern blend modes) or `PorterDuff.Mode` (always available, but a smaller choice of modes as it is mostly focused on alpha compositing). We use `BlendModeCompat` to support Lottie layer blend modes (`bm` key) to ensure compatibility on all platforms. For consistency, we don't support values which don't have a `PorterDuff.Mode` equivalent. Our support for Lottie blend modes did not include Multiply due to a slightly different behavior between the `PorterDuff.MULTIPLY` (exposed as `BlendModeCompat.MODULATE`) and `BlendModeCompat.MULTIPLY` variants. Namely, the formula used for `PorterDuff.MODULATE`, combined with alpha-premultiplication done by Skia, means that a layer with an alpha < 1.0 and multiply blend mode will also darken the destination: ![Incorrect-Blend](https://github.com/user-attachments/assets/6a2113ef-4bac-4bbc-830b-1353adf4ee2b) (Multiply-blended layers with < 1.0 alpha on the left, Screen-blended layers with < 1.0 alpha on the right) However, what we can do instead is clear the canvas with a solid white color instead of transparent before drawing the layer's contents as normal. When blending the resulting bitmap over an opaque background using `PorterDuff.MULTIPLY` (i.e. `BlendModeCompat.MODULATE`), the end result will be as if we had used `BlendModeCompat.MULTIPLY`, since all-1.0 (white) is a multiplication identity: ![Correct-Blend](https://github.com/user-attachments/assets/126022ef-6e47-48ee-b803-1d9800ca2c75) This PR implements the latter solution and adds a consistent support for the Multiply blend mode for all Android versions. *Test file used:* [blendmode-tests-multiply+screen+bg.zip](https://github.com/user-attachments/files/16365843/blendmode-tests-multiply%2Bscreen%2Bbg.zip)
1 parent c4cb225 commit 328fc72

File tree

3 files changed

+37
-6
lines changed

3 files changed

+37
-6
lines changed

lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import android.graphics.RectF;
1212

1313
import androidx.annotation.Nullable;
14-
import androidx.core.graphics.PaintCompat;
1514

1615
import com.airbnb.lottie.L;
1716
import com.airbnb.lottie.LottieDrawable;
@@ -68,8 +67,6 @@ public FillContent(final LottieDrawable lottieDrawable, BaseLayer layer, ShapeFi
6867
return;
6968
}
7069

71-
PaintCompat.setBlendMode(paint, layer.getBlendMode().toNativeBlendMode());
72-
7370
path.setFillType(fill.getFillType());
7471

7572
colorAnimation = fill.getColor().createAnimation();

lottie/src/main/java/com/airbnb/lottie/model/content/LBlendMode.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ public BlendModeCompat toNativeBlendMode() {
3333
switch (this) {
3434
case NORMAL:
3535
return null;
36+
case MULTIPLY:
37+
// BlendModeCompat.MULTIPLY does not exist on Android < Q. Instead, there's
38+
// BlendModeCompat.MODULATE, which maps to PorterDuff.Mode.MODULATE and not
39+
// PorterDuff.Mode.MULTIPLY.
40+
//
41+
// MODULATE differs from MULTIPLY in that it doesn't perform
42+
// any alpha blending. It just does a component-wise multiplication
43+
// of the colors.
44+
//
45+
// For proper results on all platforms, we will map the MULTIPLY
46+
// blend mode to MODULATE, and then do a slight adjustment to
47+
// how we render such layers to still achieve the correct result.
48+
// See BaseLayer.draw().
49+
return BlendModeCompat.MODULATE;
3650
case SCREEN:
3751
return BlendModeCompat.SCREEN;
3852
case OVERLAY:
@@ -48,7 +62,6 @@ public BlendModeCompat toNativeBlendMode() {
4862
// To prevent unexpected issues where animations look correct
4963
// during development but silently break for users with older devices
5064
// we won't support any of these until Q is widely used.
51-
case MULTIPLY:
5265
case COLOR_DODGE:
5366
case COLOR_BURN:
5467
case HARD_LIGHT:

lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import androidx.annotation.CallSuper;
1515
import androidx.annotation.FloatRange;
1616
import androidx.annotation.Nullable;
17+
import androidx.core.graphics.PaintCompat;
1718

1819
import com.airbnb.lottie.L;
1920
import com.airbnb.lottie.LottieComposition;
@@ -117,6 +118,8 @@ static BaseLayer forModel(
117118
float blurMaskFilterRadius = 0f;
118119
@Nullable BlurMaskFilter blurMaskFilter;
119120

121+
@Nullable LPaint solidWhitePaint;
122+
120123
BaseLayer(LottieDrawable lottieDrawable, Layer layerModel) {
121124
this.lottieDrawable = lottieDrawable;
122125
this.layerModel = layerModel;
@@ -258,7 +261,7 @@ public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
258261
}
259262
}
260263
int alpha = (int) ((parentAlpha / 255f * (float) opacity / 100f) * 255);
261-
if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
264+
if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer() && getBlendMode() == LBlendMode.NORMAL) {
262265
matrix.preConcat(transform.getMatrix());
263266
if (L.isTraceEnabled()) {
264267
L.beginSection("Layer#drawLayer");
@@ -307,13 +310,31 @@ public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
307310
L.beginSection("Layer#saveLayer");
308311
}
309312
contentPaint.setAlpha(255);
313+
PaintCompat.setBlendMode(contentPaint, getBlendMode().toNativeBlendMode());
310314
Utils.saveLayerCompat(canvas, rect, contentPaint);
311315
if (L.isTraceEnabled()) {
312316
L.endSection("Layer#saveLayer");
313317
}
314318

315319
// Clear the off screen buffer. This is necessary for some phones.
316-
clearCanvas(canvas);
320+
if (getBlendMode() != LBlendMode.MULTIPLY) {
321+
clearCanvas(canvas);
322+
} else {
323+
// Due to the difference between PorterDuffMode.MULTIPLY (which we use for compatibility
324+
// with Android < Q) and BlendMode.MULTIPLY (which is the correct, alpha-blended mode),
325+
// we will alpha-blend the contents of this layer on top of a white background before
326+
// we multiply it with the opaque substrate below (with canvas.restore()).
327+
//
328+
// Since white is the identity color for multiplication, this will behave as if we
329+
// had correctly performed an alpha-blended multiply (such as BlendMode.MULTIPLY), but
330+
// will work pre-Q as well.
331+
if (solidWhitePaint == null) {
332+
solidWhitePaint = new LPaint();
333+
solidWhitePaint.setColor(0xffffffff);
334+
}
335+
canvas.drawRect(rect.left - 1, rect.top - 1, rect.right + 1, rect.bottom + 1, solidWhitePaint);
336+
}
337+
317338
if (L.isTraceEnabled()) {
318339
L.beginSection("Layer#drawLayer");
319340
}

0 commit comments

Comments
 (0)