blob: 96497031131b67be16b0c329a852f35eef399694 [file] [log] [blame]
package com.airbnb.lottie;
import android.annotation.SuppressLint;
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.PixelFormat;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
class ShapeLayer extends AnimatableLayer {
private final KeyframeAnimation.AnimationListener<Path> pathChangedListener =
new KeyframeAnimation.AnimationListener<Path>() {
@Override
public void onValueChanged(Path value) {
onPathChanged();
}
};
private final KeyframeAnimation.AnimationListener<Integer> alphaChangedListener =
new KeyframeAnimation.AnimationListener<Integer>() {
@Override
public void onValueChanged(Integer value) {
invalidateSelf();
}
};
private final KeyframeAnimation.AnimationListener<Integer> colorChangedListener =
new KeyframeAnimation.AnimationListener<Integer>() {
@Override
public void onValueChanged(Integer value) {
onColorChanged();
}
};
private final KeyframeAnimation.AnimationListener<Float> lineWidthChangedListener =
new KeyframeAnimation.AnimationListener<Float>() {
@Override
public void onValueChanged(Float value) {
onLineWidthChanged();
}
};
private final KeyframeAnimation.AnimationListener<Float> dashPatternChangedListener =
new KeyframeAnimation.AnimationListener<Float>() {
@Override
public void onValueChanged(Float value) {
onDashPatternChanged();
}
};
private final KeyframeAnimation.AnimationListener<Float> strokeChangedListener =
new KeyframeAnimation.AnimationListener<Float>() {
@Override
public void onValueChanged(Float value) {
onPathPropertiesChanged();
}
};
private final KeyframeAnimation.AnimationListener<ScaleXY> scaleChangedListener =
new KeyframeAnimation.AnimationListener<ScaleXY>() {
@Override
public void onValueChanged(ScaleXY value) {
onPathPropertiesChanged();
}
};
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG) {{
setStyle(Paint.Style.FILL);
}};
private final Path tempPath = new Path();
private final Path currentPath = new Path();
/**
* Path for the trim path when it loops around the end back to the start
*/
private final Path extraTrimPath = new Path();
private final PathMeasure pathMeasure = new PathMeasure();
private float currentPathScaleX;
private float currentPathScaleY;
private float currentPathStrokeStart;
private float currentPathStrokeEnd = 100;
private float currentPathStrokeOffset = 0;
@Nullable private KeyframeAnimation<ScaleXY> scale;
private final RectF tempRect = new RectF();
private final Matrix scaleMatrix = new Matrix();
private BaseKeyframeAnimation<?, Path> path;
private BaseKeyframeAnimation<?, Integer> color;
private BaseKeyframeAnimation<?, Float> lineWidth;
@Nullable private BaseKeyframeAnimation<?, Float> strokeStart;
@Nullable private BaseKeyframeAnimation<?, Float> strokeEnd;
@Nullable private BaseKeyframeAnimation<?, Float> strokeOffset;
private BaseKeyframeAnimation<?, Integer> shapeOpacity;
private BaseKeyframeAnimation<?, Integer> transformOpacity;
private List<BaseKeyframeAnimation<?, Float>> lineDashPattern;
private BaseKeyframeAnimation<?, Float> lineDashPatternOffset;
private boolean pathPropertiesChanged = true;
private boolean dashPatternChanged;
ShapeLayer(Drawable.Callback callback) {
super(callback);
}
void setIsStroke() {
paint.setStyle(Paint.Style.STROKE);
invalidateSelf();
}
public void setColor(KeyframeAnimation<Integer> color) {
if (this.color != null) {
removeAnimation(this.color);
this.color.removeUpdateListener(colorChangedListener);
}
this.color = color;
addAnimation(color);
color.addUpdateListener(colorChangedListener);
onColorChanged();
}
private void onColorChanged() {
paint.setColor(color.getValue());
invalidateSelf();
}
public void setPath(BaseKeyframeAnimation<?, Path> path) {
if (this.path != null) {
removeAnimation(this.path);
this.path.removeUpdateListener(pathChangedListener);
}
this.path = path;
addAnimation(path);
// TODO: When the path changes, we probably have to scale it again.
path.addUpdateListener(pathChangedListener);
onPathChanged();
}
void onPathChanged() {
currentPath.reset();
currentPath.set(path.getValue());
currentPathStrokeStart = Float.NaN;
currentPathStrokeEnd = Float.NaN;
currentPathScaleX = Float.NaN;
currentPathScaleY = Float.NaN;
onPathPropertiesChanged();
invalidateSelf();
}
private void onPathPropertiesChanged() {
pathPropertiesChanged = true;
invalidateSelf();
}
@SuppressLint("NewApi")
@Override
public void draw(@NonNull Canvas canvas) {
if (pathPropertiesChanged) {
updateTrimPathPath();
}
if (dashPatternChanged) {
updateDashPattern();
}
if (paint.getStyle() == Paint.Style.STROKE && paint.getStrokeWidth() == 0f) {
return;
}
paint.setAlpha(getAlpha());
canvas.drawPath(currentPath, paint);
if (!extraTrimPath.isEmpty()) {
canvas.drawPath(extraTrimPath, paint);
}
}
private void updateTrimPathPath() {
pathPropertiesChanged = false;
boolean needsStrokeStart =
strokeStart != null && strokeStart.getValue() != currentPathStrokeStart;
boolean needsStrokeEnd = strokeEnd != null && strokeEnd.getValue() != currentPathStrokeEnd;
boolean needsStrokeOffset =
strokeOffset != null && strokeOffset.getValue() != currentPathStrokeOffset;
boolean needsScaleX = scale != null && scale.getValue().getScaleX() != currentPathScaleX;
boolean needsScaleY = scale != null && scale.getValue().getScaleY() != currentPathScaleY;
if (!needsStrokeStart && !needsStrokeEnd && !needsScaleX && !needsScaleY &&
!needsStrokeOffset) {
return;
}
currentPath.set(path.getValue());
if (needsScaleX || needsScaleY) {
currentPath.computeBounds(tempRect, false);
currentPathScaleX = scale.getValue().getScaleX();
currentPathScaleY = scale.getValue().getScaleY();
scaleMatrix
.setScale(currentPathScaleX, currentPathScaleY, tempRect.centerX(), tempRect.centerY());
currentPath.transform(scaleMatrix, currentPath);
}
if (needsStrokeStart || needsStrokeEnd || needsStrokeOffset) {
tempPath.set(currentPath);
pathMeasure.setPath(tempPath, false);
currentPathStrokeStart = strokeStart.getValue();
currentPathStrokeEnd = strokeEnd.getValue();
float length = pathMeasure.getLength();
float start = length * currentPathStrokeStart / 100f;
float end = length * currentPathStrokeEnd / 100f;
float newStart = Math.min(start, end);
float newEnd = Math.max(start, end);
currentPath.reset();
currentPathStrokeOffset = strokeOffset.getValue() / 360f * length;
newStart += currentPathStrokeOffset;
newEnd += currentPathStrokeOffset;
// If the trim path has rotated around the path, we need to shift it back.
if (newStart > length && newEnd > length) {
newStart %= length;
newEnd %= length;
}
if (newStart > newEnd) {
newStart -= length;
}
pathMeasure.getSegment(
newStart,
newEnd,
currentPath,
true);
extraTrimPath.reset();
if (newEnd > length) {
pathMeasure.getSegment(
0,
newEnd % length,
extraTrimPath,
true);
} else if (newStart < 0) {
pathMeasure.getSegment(
length + newStart,
length,
extraTrimPath,
true);
}
}
}
private void updateDashPattern() {
float[] values = new float[lineDashPattern.size()];
for (int i = 0; i < lineDashPattern.size(); i++) {
values[i] = lineDashPattern.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 (values[i] < 1f) {
values[i] = 1f;
}
} else {
if (values[i] < 0.1f) {
values[i] = 0.1f;
}
}
}
paint.setPathEffect(new DashPathEffect(values, lineDashPatternOffset.getValue()));
}
@Override public int getAlpha() {
int shapeOpacity = this.shapeOpacity == null ? 255 : this.shapeOpacity.getValue();
int transformOpacity = this.transformOpacity == null ? 255 : this.transformOpacity.getValue();
int layerOpacity = super.getAlpha();
return (int) ((shapeOpacity / 255f * transformOpacity / 255f * layerOpacity / 255f) * 255);
}
void setShapeOpacity(KeyframeAnimation<Integer> shapeOpacity) {
if (this.shapeOpacity != null) {
removeAnimation(this.shapeOpacity);
this.shapeOpacity.removeUpdateListener(alphaChangedListener);
}
this.shapeOpacity = shapeOpacity;
addAnimation(shapeOpacity);
shapeOpacity.addUpdateListener(alphaChangedListener);
invalidateSelf();
}
void setTransformOpacity(KeyframeAnimation<Integer> transformOpacity) {
this.transformOpacity = transformOpacity;
addAnimation(transformOpacity);
transformOpacity.addUpdateListener(alphaChangedListener);
invalidateSelf();
}
@Override public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
paint.setAlpha(alpha);
invalidateSelf();
}
@Override public void setColorFilter(ColorFilter colorFilter) {
}
@Override public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
void setLineWidth(KeyframeAnimation<Float> lineWidth) {
if (this.lineWidth != null) {
removeAnimation(this.lineWidth);
this.lineWidth.removeUpdateListener(lineWidthChangedListener);
}
this.lineWidth = lineWidth;
addAnimation(lineWidth);
lineWidth.addUpdateListener(lineWidthChangedListener);
onLineWidthChanged();
}
private void onLineWidthChanged() {
paint.setStrokeWidth(lineWidth.getValue());
invalidateSelf();
}
void setDashPattern(List<BaseKeyframeAnimation<?, Float>> lineDashPattern,
BaseKeyframeAnimation<?, Float> offset) {
if (this.lineDashPattern != null) {
removeAnimation(this.lineDashPattern.get(0));
this.lineDashPattern.get(0).removeUpdateListener(dashPatternChangedListener);
removeAnimation(this.lineDashPattern.get(1));
this.lineDashPattern.get(1).removeUpdateListener(dashPatternChangedListener);
}
if (this.lineDashPatternOffset != null) {
removeAnimation(this.lineDashPatternOffset);
this.lineDashPatternOffset.removeUpdateListener(dashPatternChangedListener);
}
if (lineDashPattern.isEmpty()) {
return;
}
this.lineDashPattern = lineDashPattern;
this.lineDashPatternOffset = offset;
for (int i = 0; i < lineDashPattern.size(); i++) {
BaseKeyframeAnimation<?, Float> dashPattern = lineDashPattern.get(i);
addAnimation(dashPattern);
dashPattern.addUpdateListener(dashPatternChangedListener);
}
addAnimation(offset);
offset.addUpdateListener(dashPatternChangedListener);
onDashPatternChanged();
}
private void onDashPatternChanged() {
dashPatternChanged = true;
invalidateSelf();
}
void setLineCapType(ShapeStroke.LineCapType lineCapType) {
switch (lineCapType) {
case Round:
paint.setStrokeCap(Paint.Cap.ROUND);
break;
case Butt:
default:
paint.setStrokeCap(Paint.Cap.BUTT);
}
invalidateSelf();
}
void setLineJoinType(ShapeStroke.LineJoinType lineJoinType) {
switch (lineJoinType) {
case Bevel:
paint.setStrokeJoin(Paint.Join.BEVEL);
break;
case Miter:
paint.setStrokeJoin(Paint.Join.MITER);
break;
case Round:
paint.setStrokeJoin(Paint.Join.ROUND);
break;
}
}
void setTrimPath(KeyframeAnimation<Float> strokeStart, KeyframeAnimation<Float> strokeEnd,
KeyframeAnimation<Float> strokeOffset) {
if (this.strokeStart != null) {
removeAnimation(this.strokeStart);
this.strokeStart.removeUpdateListener(strokeChangedListener);
}
if (this.strokeEnd != null) {
removeAnimation(this.strokeEnd);
this.strokeEnd.removeUpdateListener(strokeChangedListener);
}
if (this.strokeOffset != null) {
removeAnimation(this.strokeOffset);
this.strokeOffset.removeUpdateListener(strokeChangedListener);
}
this.strokeStart = strokeStart;
this.strokeEnd = strokeEnd;
this.strokeOffset = strokeOffset;
addAnimation(strokeStart);
strokeStart.addUpdateListener(strokeChangedListener);
addAnimation(strokeEnd);
strokeEnd.addUpdateListener(strokeChangedListener);
addAnimation(strokeOffset);
strokeOffset.addUpdateListener(strokeChangedListener);
onPathPropertiesChanged();
}
void setScale(@SuppressWarnings("NullableProblems") KeyframeAnimation<ScaleXY> scale) {
if (this.scale != null) {
removeAnimation(this.scale);
this.scale.removeUpdateListener(scaleChangedListener);
}
this.scale = scale;
addAnimation(scale);
scale.addUpdateListener(scaleChangedListener);
onPathPropertiesChanged();
}
}