Skip to content

Commit 33d7e1c

Browse files
allenchen1154gpeal
andauthored
Improve drop shadow effect accuracy (#2523)
These changes make several improvements to how drop shadow effects are displayed: - Drop shadows now take parent alpha into account. This means that if a layer or fill has an animated opacity, the drop shadow will multiply that opacity with its own. - Adds drop shadow support to Image layers. There are some visual bugs with how the shadows are rendered, however. - Applies a constant scale factor to the distance and softness values from the Lottie file before passing them to `Paint.setShadowLayer()` to more closely match how they are displayed in After Effects. See airbnb/lottie-ios#2175 for similar changes on iOS. - Adds three snapshot test files for distance, softness, and alpha validation. Distance test file: <img src="https://github.com/user-attachments/assets/aa559e60-0d8f-403b-acd0-fa571d6dcff5" width=400> Softness test file: <img src="https://github.com/user-attachments/assets/faa9819f-6584-49f5-91b7-7462213044f5" width=400> Example of Image layer shadow bug, right side is how it should look (capture from After Effects): <img width="1824" alt="image" src="https://github.com/user-attachments/assets/a3fb85c3-a5d1-4a6b-bbda-ff4e5ebd2fb5"> Co-authored-by: Gabriel Peal <gabriel@gpeal.com>
1 parent 3f39884 commit 33d7e1c

File tree

13 files changed

+3626
-28
lines changed

13 files changed

+3626
-28
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ public abstract class BaseStrokeContent
191191
blurMaskFilterRadius = blurRadius;
192192
}
193193
if (dropShadowAnimation != null) {
194-
dropShadowAnimation.applyTo(paint);
194+
dropShadowAnimation.applyTo(paint, parentMatrix, Utils.mixOpacities(parentAlpha, alpha));
195195
}
196196

197197
for (int i = 0; i < pathGroups.size(); i++) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.airbnb.lottie.model.content.ShapeFill;
2525
import com.airbnb.lottie.model.layer.BaseLayer;
2626
import com.airbnb.lottie.utils.MiscUtils;
27+
import com.airbnb.lottie.utils.Utils;
2728
import com.airbnb.lottie.value.LottieValueCallback;
2829

2930
import java.util.ArrayList;
@@ -120,7 +121,7 @@ public FillContent(final LottieDrawable lottieDrawable, BaseLayer layer, ShapeFi
120121
blurMaskFilterRadius = blurRadius;
121122
}
122123
if (dropShadowAnimation != null) {
123-
dropShadowAnimation.applyTo(paint);
124+
dropShadowAnimation.applyTo(paint, parentMatrix, Utils.mixOpacities(parentAlpha, alpha));
124125
}
125126

126127
path.reset();

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.airbnb.lottie.model.content.GradientType;
3333
import com.airbnb.lottie.model.layer.BaseLayer;
3434
import com.airbnb.lottie.utils.MiscUtils;
35+
import com.airbnb.lottie.utils.Utils;
3536
import com.airbnb.lottie.value.LottieValueCallback;
3637

3738
import java.util.ArrayList;
@@ -150,13 +151,14 @@ public GradientFillContent(final LottieDrawable lottieDrawable, LottieCompositio
150151
}
151152
blurMaskFilterRadius = blurRadius;
152153
}
153-
if (dropShadowAnimation != null) {
154-
dropShadowAnimation.applyTo(paint);
155-
}
156154

157155
int alpha = (int) ((parentAlpha / 255f * opacityAnimation.getValue() / 100f) * 255);
158156
paint.setAlpha(clamp(alpha, 0, 255));
159157

158+
if (dropShadowAnimation != null) {
159+
dropShadowAnimation.applyTo(paint, parentMatrix, Utils.mixOpacities(parentAlpha, alpha));
160+
}
161+
160162
canvas.drawPath(path, paint);
161163
if (L.isTraceEnabled()) {
162164
L.endSection("GradientFillContent#draw");

lottie/src/main/java/com/airbnb/lottie/animation/keyframe/DropShadowKeyframeAnimation.java

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
package com.airbnb.lottie.animation.keyframe;
22

33
import android.graphics.Color;
4+
import android.graphics.Matrix;
45
import android.graphics.Paint;
5-
66
import androidx.annotation.Nullable;
7-
87
import com.airbnb.lottie.model.layer.BaseLayer;
98
import com.airbnb.lottie.parser.DropShadowEffect;
109
import com.airbnb.lottie.value.LottieFrameInfo;
1110
import com.airbnb.lottie.value.LottieValueCallback;
1211

12+
1313
public class DropShadowKeyframeAnimation implements BaseKeyframeAnimation.AnimationListener {
14-
private static final double DEG_TO_RAD = Math.PI / 180.0;
14+
private static final float DEG_TO_RAD = (float) (Math.PI / 180.0);
1515

16+
private final BaseLayer layer;
1617
private final BaseKeyframeAnimation.AnimationListener listener;
1718
private final BaseKeyframeAnimation<Integer, Integer> color;
18-
private final BaseKeyframeAnimation<Float, Float> opacity;
19-
private final BaseKeyframeAnimation<Float, Float> direction;
20-
private final BaseKeyframeAnimation<Float, Float> distance;
21-
private final BaseKeyframeAnimation<Float, Float> radius;
19+
private final FloatKeyframeAnimation opacity;
20+
private final FloatKeyframeAnimation direction;
21+
private final FloatKeyframeAnimation distance;
22+
private final FloatKeyframeAnimation radius;
23+
24+
// Cached paint values.
25+
private float paintRadius = Float.NaN;
26+
private float paintX = Float.NaN;
27+
private float paintY = Float.NaN;
28+
// 0 is a valid color but it is transparent so it will not draw anything anyway.
29+
private int paintColor = 0;
2230

23-
private boolean isDirty = true;
31+
private final float[] matrixValues = new float[9];
2432

2533
public DropShadowKeyframeAnimation(BaseKeyframeAnimation.AnimationListener listener, BaseLayer layer, DropShadowEffect dropShadowEffect) {
2634
this.listener = listener;
35+
this.layer = layer;
2736
color = dropShadowEffect.getColor().createAnimation();
2837
color.addUpdateListener(this);
2938
layer.addAnimation(color);
@@ -42,24 +51,49 @@ public DropShadowKeyframeAnimation(BaseKeyframeAnimation.AnimationListener liste
4251
}
4352

4453
@Override public void onValueChanged() {
45-
isDirty = true;
4654
listener.onValueChanged();
4755
}
4856

49-
public void applyTo(Paint paint) {
50-
if (!isDirty) {
51-
return;
52-
}
53-
isDirty = false;
54-
55-
double directionRad = ((double) direction.getValue()) * DEG_TO_RAD;
57+
/**
58+
* Applies a shadow to the provided Paint object, which will be applied to the Canvas behind whatever is drawn
59+
* (a shape, bitmap, path, etc.)
60+
* @param parentAlpha A value between 0 and 255 representing the combined alpha of all parents of this drop shadow effect.
61+
* E.g. The layer via transform, the fill/stroke via its opacity, etc.
62+
*/
63+
public void applyTo(Paint paint, Matrix parentMatrix, int parentAlpha) {
64+
float directionRad = this.direction.getFloatValue() * DEG_TO_RAD;
5665
float distance = this.distance.getValue();
57-
float x = ((float) Math.sin(directionRad)) * distance;
58-
float y = ((float) Math.cos(directionRad + Math.PI)) * distance;
66+
float rawX = ((float) Math.sin(directionRad)) * distance;
67+
float rawY = ((float) Math.cos(directionRad + Math.PI)) * distance;
68+
69+
// The x and y coordinates are relative to the shape that is being drawn.
70+
// The distance in the animation is relative to the original size of the shape.
71+
// If the shape will be drawn scaled, we need to scale the distance we draw the shadow.
72+
layer.transform.getMatrix().getValues(matrixValues);
73+
float layerScaleX = matrixValues[Matrix.MSCALE_X];
74+
float layerScaleY = matrixValues[Matrix.MSCALE_Y];
75+
parentMatrix.getValues(matrixValues);
76+
float parentScaleX = matrixValues[Matrix.MSCALE_X];
77+
float parentScaleY = matrixValues[Matrix.MSCALE_Y];
78+
float scaleX = parentScaleX / layerScaleX;
79+
float scaleY = parentScaleY / layerScaleY;
80+
float x = rawX * scaleX;
81+
float y = rawY * scaleY;
82+
5983
int baseColor = color.getValue();
60-
int opacity = Math.round(this.opacity.getValue());
84+
int opacity = Math.round(this.opacity.getValue() * parentAlpha / 255f);
6185
int color = Color.argb(opacity, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor));
62-
float radius = this.radius.getValue();
86+
87+
// Paint.setShadowLayer() removes the shadow if radius is 0, so we use a small nonzero value in that case
88+
float radius = Math.max(this.radius.getValue() * scaleX, Float.MIN_VALUE);
89+
90+
if (paintRadius == radius && paintX == x && paintY == y && paintColor == color) {
91+
return;
92+
}
93+
paintRadius = radius;
94+
paintX = x;
95+
paintY = y;
96+
paintColor = color;
6397
paint.setShadowLayer(radius, x, y, color);
6498
}
6599

lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableFloatValue.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.airbnb.lottie.model.animatable;
22

3-
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
43
import com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation;
54
import com.airbnb.lottie.value.Keyframe;
65

@@ -12,7 +11,7 @@ public AnimatableFloatValue(List<Keyframe<Float>> keyframes) {
1211
super(keyframes);
1312
}
1413

15-
@Override public BaseKeyframeAnimation<Float, Float> createAnimation() {
14+
@Override public FloatKeyframeAnimation createAnimation() {
1615
return new FloatKeyframeAnimation(keyframes);
1716
}
1817
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ static BaseLayer forModel(
109109
private List<BaseLayer> parentLayers;
110110

111111
private final List<BaseKeyframeAnimation<?, ?>> animations = new ArrayList<>();
112-
final TransformKeyframeAnimation transform;
112+
public final TransformKeyframeAnimation transform;
113113
private boolean visible = true;
114114

115115
private boolean outlineMasksAndMattes;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.airbnb.lottie.LottieProperty;
1717
import com.airbnb.lottie.animation.LPaint;
1818
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
19+
import com.airbnb.lottie.animation.keyframe.DropShadowKeyframeAnimation;
1920
import com.airbnb.lottie.animation.keyframe.ValueCallbackKeyframeAnimation;
2021
import com.airbnb.lottie.utils.Utils;
2122
import com.airbnb.lottie.value.LottieValueCallback;
@@ -28,10 +29,15 @@ public class ImageLayer extends BaseLayer {
2829
@Nullable private final LottieImageAsset lottieImageAsset;
2930
@Nullable private BaseKeyframeAnimation<ColorFilter, ColorFilter> colorFilterAnimation;
3031
@Nullable private BaseKeyframeAnimation<Bitmap, Bitmap> imageAnimation;
32+
@Nullable private DropShadowKeyframeAnimation dropShadowAnimation;
3133

3234
ImageLayer(LottieDrawable lottieDrawable, Layer layerModel) {
3335
super(lottieDrawable, layerModel);
3436
lottieImageAsset = lottieDrawable.getLottieImageAssetForId(layerModel.getRefId());
37+
38+
if (getDropShadowEffect() != null) {
39+
dropShadowAnimation = new DropShadowKeyframeAnimation(this, this, getDropShadowEffect());
40+
}
3541
}
3642

3743
@Override public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
@@ -54,6 +60,10 @@ public class ImageLayer extends BaseLayer {
5460
dst.set(0, 0, (int) (bitmap.getWidth() * density), (int) (bitmap.getHeight() * density));
5561
}
5662

63+
if (dropShadowAnimation != null) {
64+
dropShadowAnimation.applyTo(paint, parentMatrix, parentAlpha);
65+
}
66+
5767
canvas.drawBitmap(bitmap, src, dst, paint);
5868
canvas.restore();
5969
}

lottie/src/main/java/com/airbnb/lottie/utils/Utils.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,13 @@ public static void saveLayerCompat(Canvas canvas, RectF rect, Paint paint, int f
316316
}
317317
}
318318

319+
/**
320+
* Multiplies 2 opacities that are 0-255.
321+
*/
322+
public static int mixOpacities(int opacity1, int opacity2) {
323+
return (int) ((opacity1 / 255f * opacity2 / 255f) * 255f);
324+
}
325+
319326
/**
320327
* For testing purposes only. DO NOT USE IN PRODUCTION.
321328
*/

0 commit comments

Comments
 (0)