Skip to content

Commit

Permalink
Drop shadow overhaul: improve correctness and perf
Browse files Browse the repository at this point in the history
This commit 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.
  • Loading branch information
geomaster committed Sep 13, 2024
1 parent f3d84db commit 1aa5977
Show file tree
Hide file tree
Showing 22 changed files with 933 additions and 154 deletions.
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.
*
* 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
27 changes: 24 additions & 3 deletions lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,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 Down Expand Up @@ -563,6 +564,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.
*
* 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 @@ -574,6 +593,8 @@ public boolean isApplyingOpacityToLayersEnabled() {
return isApplyingOpacityToLayersEnabled;
}

public boolean isApplyingShadowToLayersEnabled() { return isApplyingShadowToLayersEnabled; }

/**
* @see #setClipTextToBoundingBox(boolean)
*/
Expand Down Expand Up @@ -772,7 +793,7 @@ public void draw(Canvas canvas, Matrix matrix) {
renderAndDrawAsBitmap(canvas, compositionLayer);
canvas.restore();
} else {
compositionLayer.draw(canvas, matrix, alpha);
compositionLayer.draw(canvas, matrix, alpha, null);
}
isDirty = false;
} catch (InterruptedException e) {
Expand Down Expand Up @@ -1708,7 +1729,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 @@ -1772,7 +1793,7 @@ private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compo
renderingMatrix.postTranslate(-softwareRenderingTransformedBounds.left, -softwareRenderingTransformedBounds.top);

softwareRenderingBitmap.eraseColor(0);
compositionLayer.draw(softwareRenderingCanvas, renderingMatrix, alpha);
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 @@ -29,6 +29,7 @@
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 +59,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 +109,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 +150,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 +160,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 +188,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 +405,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 @@ -4,22 +4,24 @@
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
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.DropShadowKeyframeAnimation;
import com.airbnb.lottie.animation.keyframe.TransformKeyframeAnimation;
import com.airbnb.lottie.model.KeyPath;
import com.airbnb.lottie.model.KeyPathElement;
import com.airbnb.lottie.model.animatable.AnimatableTransform;
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 +30,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 +163,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 +178,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,12 @@
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.RectF;
import androidx.annotation.Nullable;
import com.airbnb.lottie.animation.keyframe.DropShadowKeyframeAnimation;
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 1aa5977

Please sign in to comment.