Skip to content

Commit

Permalink
Drop shadow overhaul: improve correctness and performance (#2548)
Browse files Browse the repository at this point in the history
## High-level summary

This PR introduces a large change to how drop shadows are rendered, introducing an `applyShadowsToLayers` flag which, by analogy to `applyOpacitiesToLayers`, allows layers to be treated as a whole for the purposes of drop shadows, improving the accuracy and bringing lottie-android in line with other renderers (lottie-web and lottie-ios).

Several different codepaths for different hardware/software combinations are introduced to ensure the fastest rendering available, even on legacy devices.

The calculation of shadow direction with respect to transforms is improved so that the output matches lottie-web and lottie-ios.

Image layers now cast shadows correctly thanks to a workaround to device-specific issues when combining `Paint.setShadowLayer()` and bitmap rendering.

Even in non-`applyShadowsToLayers` mode, correctness is improved by allowing the shadow-to-be-applied to propagate in a similar way as alpha. This allows some amount of visual fidelity to be recovered for animations or environments where enabling `applyShadowsToLayers` is not possible.

A number of issues that caused incorrect rendering in some other cases have been fixed.

## Background

### Drop shadows in Lottie

Lottie specifies drop shadows as a tuple of (angle, distance, radius, color, alpha), with each element being animatable.

The consensus behavior for the rendering of a layer with a drop shadow, which seems to be mostly respected in lottie-web and lottie-ios, seems to be:

1. Evaluate the values at the current frame for angle (`theta`), distance (`d`), radius (`r`), color with alpha (`C`).
2. Apply the layer transform and render the layer normally to a surface `So` (original layer).
3. Copy `So` to new surface `Ss` (shadow).
4. Apply a gaussian blur of radius `r' = c * r` to `Ss`, where `c` is some platform-specific constant intended to normalize blur implementations between platforms. (Ours is 0.33, lottie-web's is 0.25; see #2541).
5. Tint `Ss` with the color and combine the alpha by applying the following for each pixel `P`: `P.rgb = C.rgb * P.a; P.a = C.a * P.a`.
6. Now the shadow is ready on `Ss`, and needs to be drawn into its final position.
7. Convert from polar coordinates `theta` and `d` into `dx` and `dy`, with the 0 position at 12 o'clock: `dx = d * cos(theta - pi/2); dy = d*sin(theta - pi/2)`.
8. Draw `Ss` onto `Si` (intermediate surface) with a translation of `(dx, dy)`.
9. Draw `So` (original layer) onto `Si` with identity transform.
10. Compose `Si` into the framebuffer using the layer's alpha and blend mode.

Some non-obvious consequences of the definition above:
- The angle, distance, and radius are relative to the layer post-transform, not pre-transform. That is, rotating the layer (via its transform) still keeps the same screen-space direction of the shadow, and scaling the layer (via its transform) still keeps the same screen-space shadow blur radius.
- The drop shadow is not based on any derived outline, so a layer's drop shadow can be seen through its non-fully-opaque pixels. At the same time, reducing the alpha of a pixel in a layer reduces its alpha in the drop shadow.
- A layer's shadow and the layer do not blend on top of each other on the final canvas in case the layer has a blend mode or alpha. Instead, the shadow and the layer are alpha-blended with each other, and the result is then composited onto the canvas.
  - In case the layer has a normal blend mode, this is equivalent to alpha-blending the layer's shadow and then the shadow onto the canvas separately.

### Drop shadows in lottie-android currently

lottie-android's current implementation of drop shadows differs in important ways:
1. **Shadows are applied per-shape.** This means that a case like a shape with both fill and stroke has incorrect shadows, since both the fill and the stroke render a separate shadow on top of each other.
2. **Precomp layer shadows are ignored.** This means that a precomp cannot cause any of its child shapes to cast a shadow. This is a consequence of the current implementation of (1).
3. **Image layers do not render correct shadows,** due to the minefield that is the support matrix (or in Android's case, a more apt name would be a support tensor) of Android's graphics stack - `setShadowLayer()` simply doesn't work for images consistently. (See the last image in #2523 (comment).)

## Contributions of this PR

This PR introduces the following improvements and additions.

1. **Move the drop shadow model from individual content elements to layers,** and add some missing keypath callbacks. This is a prerequisite for handling drop shadows on a layer level.
2. **An `OffscreenLayer` implementation,** which serves as an abstraction that can replace `canvas.saveLayer()` for off-screen rendering and composition onto the final bitmap, but with the important distinction that it can also handle drop shadows, and possibly use hardware-accelerated `RenderNode`s and `RenderEffects` where available.
    - To use an `OffscreenLayer`, call its `.start()` method with a parent canvas and a `ComposeOp`, and draw on the *returned canvas.* Once finished, call `OffscreenLayer.finish()` to compose everything from the returned canvas to the parent canvas, applying alpha, blend mode, drop shadows, and color filters.
    - `OffscreenLayer` makes a dynamic decision on what to use for rendering - a no-op, forward to `.saveLayer()`, a HW-accelerated `RenderNode`, or a software bitmap, depending on the requested `ComposeOp` and hardware/SDK support.
    - The hope is that `OffscreenLayer` becomes a useful abstraction that can be extended to e.g. support hardware blurs, multiple drop shadows, or to support mattes in a hardware-accelerated fashion where possible. 
3. **The `applyShadowsToLayers` flag** which, by analogy to `applyOpacityToLayers`, turns on a more accurate mode that implements the drop shadow algorithm described above.
    - `OffscreenLayer` is used to apply alpha if `applyOpacityToLayers` is enabled, and to apply shadows if `applyShadowsToLayers` is enabled. The cost is paid only once if both alpha and drop shadows are present on a layer.
    - Not all `saveLayer()` calls in the code have been rewritten to use `OffscreenLayer` - the blast radius is minimized. `OffscreenLayer` is presently used only to apply alpha and drop shadows, and blend mode and color filters are still applied in `BaseLayer` using `saveLayer()` directly.
4. **More accurate shadow transformations.** Previously, the angle and distance were pre-transform, and only the radius was post-transform (contrary to step (2) of the algorithm). We correct this to match other renderers.
5. **More complete shadow handling even when `applyShadowsToLayers` is `false`:** we plumb the shadow through `.draw()` and `drawLayer()` calls similarly to alpha, and this allows us to render per-shape shadows on children of composition layers too.
6. ***Workaround for drop shadows on image layers.**
    - The workaround relies on `OffscreenLayer` as well, and image layers now render shadows properly in all cases.
7. **Fixes to a few subtle issues** causing incorrect rendering in other cases. (will be marked using PR comments, I might have forgotten some)

## Open questions

* **Should `applyShadowsToLayers` be `true` by default?** Some codepaths, such as when rendering purely via software, can be slow if shadow-casting layers are exceedingly large. But, the performance is still acceptable, and in the vast majority of cases everything is quite snappy.
* **Have I introduced any regressions?** The snapshot tests should answer this.
* **How does this perform on older devices?** `applyShadowsToLayers` plus an old device should trigger the purely-software shadow rendering mode. Simulating this in condition manually yields accurate results, and the performance seems surprisingly good, but it's unclear what will happen on a lower-end phone. There's also always the possibility of some device subtlety being missed. I don't have access to an older Android device.

## Testcases

These files now match between lottie-web and lottie-android:

[drop_shadow_comparator.json](https://github.com/user-attachments/files/16997070/drop_shadow_comparator.json)

[simple_shadow_casters_ll2.json](https://github.com/user-attachments/files/16997084/simple_shadow_casters_ll2.json)

The files from this earlier PR still all render the same: #2523, with the exception of the fix for image layer bug, which fixes the rendering of the Map icon as mentioned in the comment of that PR.

This file has been used as a perf stress test with many <255 opacity precomps, some stacked inside each other, that must all be blended separately: [precomp_opacity_killer.json](https://github.com/user-attachments/files/16997261/precomp_opacity_killer.json)
  • Loading branch information
geomaster authored Oct 19, 2024
1 parent e320c99 commit 5deb2de
Show file tree
Hide file tree
Showing 24 changed files with 1,011 additions and 181 deletions.
6 changes: 3 additions & 3 deletions lottie-compose/api/lottie-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ public final class com/airbnb/lottie/compose/LottieAnimatableKt {
}

public final class com/airbnb/lottie/compose/LottieAnimationKt {
public static final fun LottieAnimation (Lcom/airbnb/lottie/LottieComposition;FLandroidx/compose/ui/Modifier;ZZZLcom/airbnb/lottie/RenderMode;ZLcom/airbnb/lottie/compose/LottieDynamicProperties;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ZZLcom/airbnb/lottie/AsyncUpdates;Landroidx/compose/runtime/Composer;III)V
public static final fun LottieAnimation (Lcom/airbnb/lottie/LottieComposition;Landroidx/compose/ui/Modifier;ZZLcom/airbnb/lottie/compose/LottieClipSpec;FIZZZLcom/airbnb/lottie/RenderMode;ZZLcom/airbnb/lottie/compose/LottieDynamicProperties;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ZZLjava/util/Map;ZLcom/airbnb/lottie/AsyncUpdates;Landroidx/compose/runtime/Composer;IIII)V
public static final fun LottieAnimation (Lcom/airbnb/lottie/LottieComposition;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZZZLcom/airbnb/lottie/RenderMode;ZLcom/airbnb/lottie/compose/LottieDynamicProperties;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ZZLjava/util/Map;Lcom/airbnb/lottie/AsyncUpdates;ZLandroidx/compose/runtime/Composer;III)V
public static final fun LottieAnimation (Lcom/airbnb/lottie/LottieComposition;FLandroidx/compose/ui/Modifier;ZZZZLcom/airbnb/lottie/RenderMode;ZLcom/airbnb/lottie/compose/LottieDynamicProperties;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ZZLcom/airbnb/lottie/AsyncUpdates;Landroidx/compose/runtime/Composer;III)V
public static final fun LottieAnimation (Lcom/airbnb/lottie/LottieComposition;Landroidx/compose/ui/Modifier;ZZLcom/airbnb/lottie/compose/LottieClipSpec;FIZZZZLcom/airbnb/lottie/RenderMode;ZZLcom/airbnb/lottie/compose/LottieDynamicProperties;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ZZLjava/util/Map;ZLcom/airbnb/lottie/AsyncUpdates;Landroidx/compose/runtime/Composer;IIII)V
public static final fun LottieAnimation (Lcom/airbnb/lottie/LottieComposition;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZZZZLcom/airbnb/lottie/RenderMode;ZLcom/airbnb/lottie/compose/LottieDynamicProperties;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ZZLjava/util/Map;Lcom/airbnb/lottie/AsyncUpdates;ZLandroidx/compose/runtime/Composer;III)V
}

public abstract interface class com/airbnb/lottie/compose/LottieAnimationState : androidx/compose/runtime/State {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ fun LottieAnimation(
modifier: Modifier = Modifier,
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
applyShadowToLayers: Boolean = true,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
maintainOriginalImageBounds: Boolean = false,
Expand Down Expand Up @@ -130,6 +131,7 @@ fun LottieAnimation(
}
drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
drawable.isApplyingShadowToLayersEnabled = applyShadowToLayers
drawable.maintainOriginalImageBounds = maintainOriginalImageBounds
drawable.clipToCompositionBounds = clipToCompositionBounds
drawable.clipTextToBoundingBox = clipTextToBoundingBox
Expand Down Expand Up @@ -158,6 +160,7 @@ fun LottieAnimation(
modifier: Modifier = Modifier,
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
applyShadowToLayers: Boolean = true,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
maintainOriginalImageBounds: Boolean = false,
Expand All @@ -174,6 +177,7 @@ fun LottieAnimation(
modifier = modifier,
outlineMasksAndMattes = outlineMasksAndMattes,
applyOpacityToLayers = applyOpacityToLayers,
applyShadowToLayers = applyShadowToLayers,
enableMergePaths = enableMergePaths,
renderMode = renderMode,
maintainOriginalImageBounds = maintainOriginalImageBounds,
Expand Down Expand Up @@ -205,6 +209,7 @@ fun LottieAnimation(
iterations: Int = 1,
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
applyShadowToLayers: Boolean = true,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
reverseOnRepeat: Boolean = false,
Expand Down Expand Up @@ -233,6 +238,7 @@ fun LottieAnimation(
modifier = modifier,
outlineMasksAndMattes = outlineMasksAndMattes,
applyOpacityToLayers = applyOpacityToLayers,
applyShadowToLayers = applyShadowToLayers,
enableMergePaths = enableMergePaths,
renderMode = renderMode,
maintainOriginalImageBounds = maintainOriginalImageBounds,
Expand Down
23 changes: 23 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ private void init(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {

enableMergePathsForKitKatAndAbove(ta.getBoolean(
R.styleable.LottieAnimationView_lottie_enableMergePathsForKitKatAndAbove, false));
setApplyingOpacityToLayersEnabled(ta.getBoolean(
R.styleable.LottieAnimationView_lottie_applyOpacityToLayers, false));
setApplyingShadowToLayersEnabled(ta.getBoolean(
R.styleable.LottieAnimationView_lottie_applyShadowToLayers, true));

if (ta.hasValue(R.styleable.LottieAnimationView_lottie_colorFilter)) {
int colorRes = ta.getResourceId(R.styleable.LottieAnimationView_lottie_colorFilter, -1);
ColorStateList csl = AppCompatResources.getColorStateList(getContext(), colorRes);
Expand Down Expand Up @@ -1247,6 +1252,24 @@ public void setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersE
lottieDrawable.setApplyingOpacityToLayersEnabled(isApplyingOpacityToLayersEnabled);
}

/**
* Sets whether to apply drop shadows to each layer instead of shape.
* <p>
* When true, the behavior will be more correct: it will mimic lottie-web and other renderers, in that drop shadows will be applied to a layer
* as a whole, no matter its contents.
* <p>
* When false, the performance will be better at the expense of correctness: for each shape element individually, the first drop shadow upwards
* in the hierarchy is applied to it directly. Visually, this may manifest as phantom shadows or artifacts where the artist has intended to treat a
* layer as a whole, and this option exposes its internal structure.
* <p>
* The default value is true.
*
* @see LottieAnimationView::setApplyingOpacityToLayersEnabled
*/
public void setApplyingShadowToLayersEnabled(boolean isApplyingShadowToLayersEnabled) {
lottieDrawable.setApplyingShadowToLayersEnabled(isApplyingShadowToLayersEnabled);
}

/**
* @see #setClipTextToBoundingBox(boolean)
*/
Expand Down
36 changes: 33 additions & 3 deletions lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.airbnb.lottie.utils.LottieThreadFactory;
import com.airbnb.lottie.utils.LottieValueAnimator;
import com.airbnb.lottie.utils.MiscUtils;
import com.airbnb.lottie.utils.Utils;
import com.airbnb.lottie.value.LottieFrameInfo;
import com.airbnb.lottie.value.LottieValueCallback;
import com.airbnb.lottie.value.SimpleLottieValueCallback;
Expand Down Expand Up @@ -154,6 +155,7 @@ private enum OnVisibleAction {
private boolean performanceTrackingEnabled;
private boolean outlineMasksAndMattes;
private boolean isApplyingOpacityToLayersEnabled;
private boolean isApplyingShadowToLayersEnabled;
private boolean clipTextToBoundingBox = false;

private RenderMode renderMode = RenderMode.AUTOMATIC;
Expand All @@ -172,6 +174,7 @@ private enum OnVisibleAction {
private RectF softwareRenderingDstBoundsRectF;
private RectF softwareRenderingTransformedBounds;
private Matrix softwareRenderingOriginalCanvasMatrix;
private float[] softwareRenderingOriginalCanvasMatrixElements = new float[9];
private Matrix softwareRenderingOriginalCanvasMatrixInverse;

/**
Expand Down Expand Up @@ -568,6 +571,24 @@ public void setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersE
this.isApplyingOpacityToLayersEnabled = isApplyingOpacityToLayersEnabled;
}

/**
* Sets whether to apply drop shadows to each layer instead of shape.
* <p>
* When true, the behavior will be more correct: it will mimic lottie-web and other renderers, in that drop shadows will be applied to a layer
* as a whole, no matter its contents.
* <p>
* When false, the performance will be better at the expense of correctness: for each shape element individually, the first drop shadow upwards
* in the hierarchy is applied to it directly. Visually, this may manifest as phantom shadows or artifacts where the artist has intended to treat a
* layer as a whole, and this option exposes its internal structure.
* <p>
* The default value is true.
*
* @see LottieDrawable::setApplyingOpacityToLayersEnabled
*/
public void setApplyingShadowToLayersEnabled(boolean isApplyingShadowsToLayersEnabled) {
this.isApplyingShadowToLayersEnabled = isApplyingShadowsToLayersEnabled;
}

/**
* This API no longer has any effect.
*/
Expand All @@ -579,6 +600,8 @@ public boolean isApplyingOpacityToLayersEnabled() {
return isApplyingOpacityToLayersEnabled;
}

public boolean isApplyingShadowToLayersEnabled() { return isApplyingShadowToLayersEnabled; }

/**
* @see #setClipTextToBoundingBox(boolean)
*/
Expand Down Expand Up @@ -800,7 +823,7 @@ private void draw(Canvas canvas, Matrix matrix, CompositionLayer compositionLaye
renderAndDrawAsBitmap(canvas, compositionLayer);
canvas.restore();
} else {
compositionLayer.draw(canvas, matrix, alpha);
compositionLayer.draw(canvas, matrix, alpha, null);
}
}

Expand Down Expand Up @@ -1725,7 +1748,7 @@ private void drawDirectlyToCanvas(Canvas canvas) {
renderingMatrix.preScale(scaleX, scaleY);
renderingMatrix.preTranslate(bounds.left, bounds.top);
}
compositionLayer.draw(canvas, renderingMatrix, alpha);
compositionLayer.draw(canvas, renderingMatrix, alpha, null);
}

/**
Expand Down Expand Up @@ -1782,14 +1805,21 @@ private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compo
ensureSoftwareRenderingBitmap(renderWidth, renderHeight);

if (isDirty) {
softwareRenderingOriginalCanvasMatrix.getValues(softwareRenderingOriginalCanvasMatrixElements);
float preExistingScaleX = softwareRenderingOriginalCanvasMatrixElements[Matrix.MSCALE_X];
float preExistingScaleY = softwareRenderingOriginalCanvasMatrixElements[Matrix.MSCALE_Y];

renderingMatrix.set(softwareRenderingOriginalCanvasMatrix);
renderingMatrix.preScale(scaleX, scaleY);
// We want to render the smallest bitmap possible. If the animation doesn't start at the top left, we translate the canvas and shrink the
// bitmap to avoid allocating and copying the empty space on the left and top. renderWidth and renderHeight take this into account.
renderingMatrix.postTranslate(-softwareRenderingTransformedBounds.left, -softwareRenderingTransformedBounds.top);
renderingMatrix.postScale(1.0f / preExistingScaleX, 1.0f / preExistingScaleY);

softwareRenderingBitmap.eraseColor(0);
compositionLayer.draw(softwareRenderingCanvas, renderingMatrix, alpha);
softwareRenderingCanvas.setMatrix(Utils.IDENTITY_MATRIX);
softwareRenderingCanvas.scale(preExistingScaleX, preExistingScaleY);
compositionLayer.draw(softwareRenderingCanvas, renderingMatrix, alpha, null);

// Calculate the dst bounds.
// We need to map the rendered coordinates back to the canvas's coordinates. To do so, we need to invert the transform
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.animation.LPaint;
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
import com.airbnb.lottie.animation.keyframe.DropShadowKeyframeAnimation;
import com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation;
import com.airbnb.lottie.animation.keyframe.IntegerKeyframeAnimation;
import com.airbnb.lottie.animation.keyframe.ValueCallbackKeyframeAnimation;
import com.airbnb.lottie.model.KeyPath;
import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
import com.airbnb.lottie.model.content.ShapeTrimPath;
import com.airbnb.lottie.model.layer.BaseLayer;
import com.airbnb.lottie.utils.DropShadow;
import com.airbnb.lottie.utils.MiscUtils;
import com.airbnb.lottie.utils.Utils;
import com.airbnb.lottie.value.LottieValueCallback;
Expand Down Expand Up @@ -58,8 +57,6 @@ public abstract class BaseStrokeContent
@Nullable private BaseKeyframeAnimation<Float, Float> blurAnimation;
float blurMaskFilterRadius = 0f;

@Nullable private DropShadowKeyframeAnimation dropShadowAnimation;

BaseStrokeContent(final LottieDrawable lottieDrawable, BaseLayer layer, Paint.Cap cap,
Paint.Join join, float miterLimit, AnimatableIntegerValue opacity, AnimatableFloatValue width,
List<AnimatableFloatValue> dashPattern, AnimatableFloatValue offset) {
Expand Down Expand Up @@ -110,9 +107,6 @@ public abstract class BaseStrokeContent
blurAnimation.addUpdateListener(this);
layer.addAnimation(blurAnimation);
}
if (layer.getDropShadowEffect() != null) {
dropShadowAnimation = new DropShadowKeyframeAnimation(this, layer, layer.getDropShadowEffect());
}
}

@Override public void onValueChanged() {
Expand Down Expand Up @@ -154,7 +148,7 @@ public abstract class BaseStrokeContent
}
}

@Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
@Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha, @Nullable DropShadow shadowToApply) {
if (L.isTraceEnabled()) {
L.beginSection("StrokeContent#draw");
}
Expand All @@ -164,8 +158,10 @@ public abstract class BaseStrokeContent
}
return;
}
int alpha = (int) ((parentAlpha / 255f * ((IntegerKeyframeAnimation) opacityAnimation).getIntValue() / 100f) * 255);
paint.setAlpha(clamp(alpha, 0, 255));
float strokeAlpha = opacityAnimation.getValue() / 100f;
int alpha = (int) (parentAlpha * strokeAlpha);
alpha = clamp(alpha, 0, 255);
paint.setAlpha(alpha);
paint.setStrokeWidth(((FloatKeyframeAnimation) widthAnimation).getFloatValue());
if (paint.getStrokeWidth() <= 0) {
// Android draws a hairline stroke for 0, After Effects doesn't.
Expand All @@ -190,8 +186,8 @@ public abstract class BaseStrokeContent
}
blurMaskFilterRadius = blurRadius;
}
if (dropShadowAnimation != null) {
dropShadowAnimation.applyTo(paint, parentMatrix, Utils.mixOpacities(parentAlpha, alpha));
if (shadowToApply != null) {
shadowToApply.applyWithAlpha((int)(strokeAlpha * 255), paint);
}

canvas.save();
Expand Down Expand Up @@ -407,16 +403,6 @@ public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> ca
blurAnimation.addUpdateListener(this);
layer.addAnimation(blurAnimation);
}
} else if (property == LottieProperty.DROP_SHADOW_COLOR && dropShadowAnimation != null) {
dropShadowAnimation.setColorCallback((LottieValueCallback<Integer>) callback);
} else if (property == LottieProperty.DROP_SHADOW_OPACITY && dropShadowAnimation != null) {
dropShadowAnimation.setOpacityCallback((LottieValueCallback<Float>) callback);
} else if (property == LottieProperty.DROP_SHADOW_DIRECTION && dropShadowAnimation != null) {
dropShadowAnimation.setDirectionCallback((LottieValueCallback<Float>) callback);
} else if (property == LottieProperty.DROP_SHADOW_DISTANCE && dropShadowAnimation != null) {
dropShadowAnimation.setDistanceCallback((LottieValueCallback<Float>) callback);
} else if (property == LottieProperty.DROP_SHADOW_RADIUS && dropShadowAnimation != null) {
dropShadowAnimation.setRadiusCallback((LottieValueCallback<Float>) callback);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;

import androidx.annotation.Nullable;

import com.airbnb.lottie.LottieComposition;
import com.airbnb.lottie.LottieDrawable;
import com.airbnb.lottie.animation.LPaint;
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
import com.airbnb.lottie.animation.keyframe.TransformKeyframeAnimation;
import com.airbnb.lottie.model.KeyPath;
Expand All @@ -19,7 +17,8 @@
import com.airbnb.lottie.model.content.ContentModel;
import com.airbnb.lottie.model.content.ShapeGroup;
import com.airbnb.lottie.model.layer.BaseLayer;
import com.airbnb.lottie.utils.Utils;
import com.airbnb.lottie.utils.DropShadow;
import com.airbnb.lottie.utils.OffscreenLayer;
import com.airbnb.lottie.value.LottieValueCallback;

import java.util.ArrayList;
Expand All @@ -28,8 +27,9 @@
public class ContentGroup implements DrawingContent, PathContent,
BaseKeyframeAnimation.AnimationListener, KeyPathElement {

private final Paint offScreenPaint = new LPaint();
private final OffscreenLayer.ComposeOp offscreenOp = new OffscreenLayer.ComposeOp();
private final RectF offScreenRectF = new RectF();
private final OffscreenLayer offscreenLayer = new OffscreenLayer();

private static List<Content> contentsFromModels(LottieDrawable drawable, LottieComposition composition, BaseLayer layer,
List<ContentModel> contentModels) {
Expand Down Expand Up @@ -160,7 +160,7 @@ Matrix getTransformationMatrix() {
return path;
}

@Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
@Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha, @Nullable DropShadow shadowToApply) {
if (hidden) {
return;
}
Expand All @@ -175,24 +175,40 @@ Matrix getTransformationMatrix() {
}

// Apply off-screen rendering only when needed in order to improve rendering performance.
boolean isRenderingWithOffScreen = lottieDrawable.isApplyingOpacityToLayersEnabled() && hasTwoOrMoreDrawableContent() && layerAlpha != 255;
boolean isRenderingWithOffScreen =
(lottieDrawable.isApplyingOpacityToLayersEnabled() && hasTwoOrMoreDrawableContent() && layerAlpha != 255) ||
(shadowToApply != null && lottieDrawable.isApplyingShadowToLayersEnabled() && hasTwoOrMoreDrawableContent());
int childAlpha = isRenderingWithOffScreen ? 255 : layerAlpha;

Canvas contentCanvas = canvas;
if (isRenderingWithOffScreen) {
offScreenRectF.set(0, 0, 0, 0);
getBounds(offScreenRectF, matrix, true);
offScreenPaint.setAlpha(layerAlpha);
Utils.saveLayerCompat(canvas, offScreenRectF, offScreenPaint);
getBounds(offScreenRectF, parentMatrix, true);
offscreenOp.alpha = layerAlpha;
if (shadowToApply != null) {
shadowToApply.applyTo(offscreenOp);
shadowToApply = null; // Don't pass it to children - OffscreenLayer now takes care of this
} else {
offscreenOp.shadow = null;
}

contentCanvas = offscreenLayer.start(canvas, offScreenRectF, offscreenOp);
} else {
if (shadowToApply != null) {
shadowToApply = new DropShadow(shadowToApply);
shadowToApply.multiplyOpacity(childAlpha);
}
}

int childAlpha = isRenderingWithOffScreen ? 255 : layerAlpha;
for (int i = contents.size() - 1; i >= 0; i--) {
Object content = contents.get(i);
if (content instanceof DrawingContent) {
((DrawingContent) content).draw(canvas, matrix, childAlpha);
((DrawingContent) content).draw(contentCanvas, matrix, childAlpha, shadowToApply);
}
}

if (isRenderingWithOffScreen) {
canvas.restore();
offscreenLayer.finish();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.RectF;
import androidx.annotation.Nullable;
import com.airbnb.lottie.utils.DropShadow;

public interface DrawingContent extends Content {
void draw(Canvas canvas, Matrix parentMatrix, int alpha);
void draw(Canvas canvas, Matrix parentMatrix, int alpha, @Nullable DropShadow shadowToApply);

void getBounds(RectF outBounds, Matrix parentMatrix, boolean applyParents);
}
Loading

0 comments on commit 5deb2de

Please sign in to comment.