blob: c2572382cfed2c4d87edf0d55a7c8a132d0aca44 [file] [log] [blame]
package com.airbnb.lottie.animation.content;
import static com.airbnb.lottie.utils.MiscUtils.clamp;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.DashPathEffect;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.RectF;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import com.airbnb.lottie.L;
import com.airbnb.lottie.LottieDrawable;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.animation.LPaint;
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
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.MiscUtils;
import com.airbnb.lottie.utils.Utils;
import com.airbnb.lottie.value.LottieValueCallback;
import java.util.ArrayList;
import java.util.List;
public abstract class BaseStrokeContent
implements BaseKeyframeAnimation.AnimationListener, KeyPathElementContent, DrawingContent {
private final PathMeasure pm = new PathMeasure();
private final Path path = new Path();
private final Path trimPathPath = new Path();
private final RectF rect = new RectF();
private final LottieDrawable lottieDrawable;
protected final BaseLayer layer;
private final List<PathGroup> pathGroups = new ArrayList<>();
private final float[] dashPatternValues;
final Paint paint = new LPaint(Paint.ANTI_ALIAS_FLAG);
private final BaseKeyframeAnimation<?, Float> widthAnimation;
private final BaseKeyframeAnimation<?, Integer> opacityAnimation;
private final List<BaseKeyframeAnimation<?, Float>> dashPatternAnimations;
@Nullable private final BaseKeyframeAnimation<?, Float> dashPatternOffsetAnimation;
@Nullable private BaseKeyframeAnimation<ColorFilter, ColorFilter> colorFilterAnimation;
@Nullable private BaseKeyframeAnimation<Float, Float> blurAnimation;
float blurMaskFilterRadius = 0f;
BaseStrokeContent(final LottieDrawable lottieDrawable, BaseLayer layer, Paint.Cap cap,
Paint.Join join, float miterLimit, AnimatableIntegerValue opacity, AnimatableFloatValue width,
List<AnimatableFloatValue> dashPattern, AnimatableFloatValue offset) {
this.lottieDrawable = lottieDrawable;
this.layer = layer;
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(cap);
paint.setStrokeJoin(join);
paint.setStrokeMiter(miterLimit);
opacityAnimation = opacity.createAnimation();
widthAnimation = width.createAnimation();
if (offset == null) {
dashPatternOffsetAnimation = null;
} else {
dashPatternOffsetAnimation = offset.createAnimation();
}
dashPatternAnimations = new ArrayList<>(dashPattern.size());
dashPatternValues = new float[dashPattern.size()];
for (int i = 0; i < dashPattern.size(); i++) {
dashPatternAnimations.add(dashPattern.get(i).createAnimation());
}
layer.addAnimation(opacityAnimation);
layer.addAnimation(widthAnimation);
for (int i = 0; i < dashPatternAnimations.size(); i++) {
layer.addAnimation(dashPatternAnimations.get(i));
}
if (dashPatternOffsetAnimation != null) {
layer.addAnimation(dashPatternOffsetAnimation);
}
opacityAnimation.addUpdateListener(this);
widthAnimation.addUpdateListener(this);
for (int i = 0; i < dashPattern.size(); i++) {
dashPatternAnimations.get(i).addUpdateListener(this);
}
if (dashPatternOffsetAnimation != null) {
dashPatternOffsetAnimation.addUpdateListener(this);
}
if (layer.getBlurEffect() == null) {
blurAnimation = null;
} else {
blurAnimation = layer.getBlurEffect().getBlurriness().createAnimation();
blurAnimation.addUpdateListener(this);
layer.addAnimation(blurAnimation);
}
}
@Override public void onValueChanged() {
lottieDrawable.invalidateSelf();
}
@Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
TrimPathContent trimPathContentBefore = null;
for (int i = contentsBefore.size() - 1; i >= 0; i--) {
Content content = contentsBefore.get(i);
if (content instanceof TrimPathContent &&
((TrimPathContent) content).getType() == ShapeTrimPath.Type.INDIVIDUALLY) {
trimPathContentBefore = (TrimPathContent) content;
}
}
if (trimPathContentBefore != null) {
trimPathContentBefore.addListener(this);
}
PathGroup currentPathGroup = null;
for (int i = contentsAfter.size() - 1; i >= 0; i--) {
Content content = contentsAfter.get(i);
if (content instanceof TrimPathContent &&
((TrimPathContent) content).getType() == ShapeTrimPath.Type.INDIVIDUALLY) {
if (currentPathGroup != null) {
pathGroups.add(currentPathGroup);
}
currentPathGroup = new PathGroup((TrimPathContent) content);
((TrimPathContent) content).addListener(this);
} else if (content instanceof PathContent) {
if (currentPathGroup == null) {
currentPathGroup = new PathGroup(trimPathContentBefore);
}
currentPathGroup.paths.add((PathContent) content);
}
}
if (currentPathGroup != null) {
pathGroups.add(currentPathGroup);
}
}
@Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
L.beginSection("StrokeContent#draw");
if (Utils.hasZeroScaleAxis(parentMatrix)) {
L.endSection("StrokeContent#draw");
return;
}
int alpha = (int) ((parentAlpha / 255f * ((IntegerKeyframeAnimation) opacityAnimation).getIntValue() / 100f) * 255);
paint.setAlpha(clamp(alpha, 0, 255));
paint.setStrokeWidth(((FloatKeyframeAnimation) widthAnimation).getFloatValue() * Utils.getScale(parentMatrix));
if (paint.getStrokeWidth() <= 0) {
// Android draws a hairline stroke for 0, After Effects doesn't.
L.endSection("StrokeContent#draw");
return;
}
applyDashPatternIfNeeded(parentMatrix);
if (colorFilterAnimation != null) {
paint.setColorFilter(colorFilterAnimation.getValue());
}
if (blurAnimation != null) {
float blurRadius = blurAnimation.getValue();
if (blurRadius == 0f) {
paint.setMaskFilter(null);
} else if (blurRadius != blurMaskFilterRadius){
BlurMaskFilter blur = layer.getBlurMaskFilter(blurRadius);
paint.setMaskFilter(blur);
}
blurMaskFilterRadius = blurRadius;
}
for (int i = 0; i < pathGroups.size(); i++) {
PathGroup pathGroup = pathGroups.get(i);
if (pathGroup.trimPath != null) {
applyTrimPath(canvas, pathGroup, parentMatrix);
} else {
L.beginSection("StrokeContent#buildPath");
path.reset();
for (int j = pathGroup.paths.size() - 1; j >= 0; j--) {
path.addPath(pathGroup.paths.get(j).getPath(), parentMatrix);
}
L.endSection("StrokeContent#buildPath");
L.beginSection("StrokeContent#drawPath");
canvas.drawPath(path, paint);
L.endSection("StrokeContent#drawPath");
}
}
L.endSection("StrokeContent#draw");
}
private void applyTrimPath(Canvas canvas, PathGroup pathGroup, Matrix parentMatrix) {
L.beginSection("StrokeContent#applyTrimPath");
if (pathGroup.trimPath == null) {
L.endSection("StrokeContent#applyTrimPath");
return;
}
path.reset();
for (int j = pathGroup.paths.size() - 1; j >= 0; j--) {
path.addPath(pathGroup.paths.get(j).getPath(), parentMatrix);
}
pm.setPath(path, false);
float totalLength = pm.getLength();
while (pm.nextContour()) {
totalLength += pm.getLength();
}
float offsetLength = totalLength * pathGroup.trimPath.getOffset().getValue() / 360f;
float startLength =
totalLength * pathGroup.trimPath.getStart().getValue() / 100f + offsetLength;
float endLength =
totalLength * pathGroup.trimPath.getEnd().getValue() / 100f + offsetLength;
float currentLength = 0;
for (int j = pathGroup.paths.size() - 1; j >= 0; j--) {
trimPathPath.set(pathGroup.paths.get(j).getPath());
trimPathPath.transform(parentMatrix);
pm.setPath(trimPathPath, false);
float length = pm.getLength();
if (endLength > totalLength && endLength - totalLength < currentLength + length &&
currentLength < endLength - totalLength) {
// Draw the segment when the end is greater than the length which wraps around to the
// beginning.
float startValue;
if (startLength > totalLength) {
startValue = (startLength - totalLength) / length;
} else {
startValue = 0;
}
float endValue = Math.min((endLength - totalLength) / length, 1);
Utils.applyTrimPathIfNeeded(trimPathPath, startValue, endValue, 0);
canvas.drawPath(trimPathPath, paint);
} else
//noinspection StatementWithEmptyBody
if (currentLength + length < startLength || currentLength > endLength) {
// Do nothing
} else if (currentLength + length <= endLength && startLength < currentLength) {
canvas.drawPath(trimPathPath, paint);
} else {
float startValue;
if (startLength < currentLength) {
startValue = 0;
} else {
startValue = (startLength - currentLength) / length;
}
float endValue;
if (endLength > currentLength + length) {
endValue = 1f;
} else {
endValue = (endLength - currentLength) / length;
}
Utils.applyTrimPathIfNeeded(trimPathPath, startValue, endValue, 0);
canvas.drawPath(trimPathPath, paint);
}
currentLength += length;
}
L.endSection("StrokeContent#applyTrimPath");
}
@Override public void getBounds(RectF outBounds, Matrix parentMatrix, boolean applyParents) {
L.beginSection("StrokeContent#getBounds");
path.reset();
for (int i = 0; i < pathGroups.size(); i++) {
PathGroup pathGroup = pathGroups.get(i);
for (int j = 0; j < pathGroup.paths.size(); j++) {
path.addPath(pathGroup.paths.get(j).getPath(), parentMatrix);
}
}
path.computeBounds(rect, false);
float width = ((FloatKeyframeAnimation) widthAnimation).getFloatValue();
rect.set(rect.left - width / 2f, rect.top - width / 2f,
rect.right + width / 2f, rect.bottom + width / 2f);
outBounds.set(rect);
// Add padding to account for rounding errors.
outBounds.set(
outBounds.left - 1,
outBounds.top - 1,
outBounds.right + 1,
outBounds.bottom + 1
);
L.endSection("StrokeContent#getBounds");
}
private void applyDashPatternIfNeeded(Matrix parentMatrix) {
L.beginSection("StrokeContent#applyDashPattern");
if (dashPatternAnimations.isEmpty()) {
L.endSection("StrokeContent#applyDashPattern");
return;
}
float scale = Utils.getScale(parentMatrix);
for (int i = 0; i < dashPatternAnimations.size(); i++) {
dashPatternValues[i] = dashPatternAnimations.get(i).getValue();
// If the value of the dash pattern or gap is too small, the number of individual sections
// approaches infinity as the value approaches 0.
// To mitigate this, we essentially put a minimum value on the dash pattern size of 1px
// and a minimum gap size of 0.01.
if (i % 2 == 0) {
if (dashPatternValues[i] < 1f) {
dashPatternValues[i] = 1f;
}
} else {
if (dashPatternValues[i] < 0.1f) {
dashPatternValues[i] = 0.1f;
}
}
dashPatternValues[i] *= scale;
}
float offset = dashPatternOffsetAnimation == null ? 0f : dashPatternOffsetAnimation.getValue() * scale;
paint.setPathEffect(new DashPathEffect(dashPatternValues, offset));
L.endSection("StrokeContent#applyDashPattern");
}
@Override public void resolveKeyPath(
KeyPath keyPath, int depth, List<KeyPath> accumulator, KeyPath currentPartialKeyPath) {
MiscUtils.resolveKeyPath(keyPath, depth, accumulator, currentPartialKeyPath, this);
}
@SuppressWarnings("unchecked")
@Override
@CallSuper
public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> callback) {
if (property == LottieProperty.OPACITY) {
opacityAnimation.setValueCallback((LottieValueCallback<Integer>) callback);
} else if (property == LottieProperty.STROKE_WIDTH) {
widthAnimation.setValueCallback((LottieValueCallback<Float>) callback);
} else if (property == LottieProperty.COLOR_FILTER) {
if (colorFilterAnimation != null) {
layer.removeAnimation(colorFilterAnimation);
}
if (callback == null) {
colorFilterAnimation = null;
} else {
colorFilterAnimation =
new ValueCallbackKeyframeAnimation<>((LottieValueCallback<ColorFilter>) callback);
colorFilterAnimation.addUpdateListener(this);
layer.addAnimation(colorFilterAnimation);
}
} else if (property == LottieProperty.BLUR_RADIUS) {
if (blurAnimation != null) {
blurAnimation.setValueCallback((LottieValueCallback<Float>) callback);
} else {
blurAnimation =
new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Float>) callback);
blurAnimation.addUpdateListener(this);
layer.addAnimation(blurAnimation);
}
}
}
/**
* Data class to help drawing trim paths individually.
*/
private static final class PathGroup {
private final List<PathContent> paths = new ArrayList<>();
@Nullable private final TrimPathContent trimPath;
private PathGroup(@Nullable TrimPathContent trimPath) {
this.trimPath = trimPath;
}
}
}