Skip to content

Commit

Permalink
Apply blend modes on layer level and add Multiply blend mode (#2519)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
geomaster authored Jul 24, 2024
1 parent c4cb225 commit 328fc72
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ public BlendModeCompat toNativeBlendMode() {
switch (this) {
case NORMAL:
return null;
case 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:
Expand All @@ -48,7 +62,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:
Expand Down
25 changes: 23 additions & 2 deletions lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -117,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;
Expand Down Expand Up @@ -258,7 +261,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");
Expand Down Expand Up @@ -307,13 +310,31 @@ 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");
}

// 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.
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()) {
L.beginSection("Layer#drawLayer");
}
Expand Down

0 comments on commit 328fc72

Please sign in to comment.