Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop shadow overhaul: improve correctness and performance #2548

Merged
merged 14 commits into from
Oct 19, 2024
Merged
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applying this clamp right away, here and elsewhere, fixes issues with >255 or <0 alpha values, which caused shadows to not appear.

Such alpha values are possible when interpolating using an easing that evaluates to <0 or >1 at some point along its curve.

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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one got lost in the diff, but is very important - matrix was being passed here, causing incorrect transformations being applied when applyOpacitiesToLayers is true; see the precomp_opacity_killer.json testcase.

offscreenOp.alpha = layerAlpha;
if (shadowToApply != null) {
shadowToApply.applyTo(offscreenOp);
shadowToApply = null; // Don't pass it to children - OffscreenLayer now takes care of this
geomaster marked this conversation as resolved.
Show resolved Hide resolved
} 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
Loading