Added initial support for trim paths individually (#204)
diff --git a/LottieSample/screenshots/Tests_GradientFill.png b/LottieSample/screenshots/Tests_GradientFill.png
index d2cda77..68fdfe7 100644
--- a/LottieSample/screenshots/Tests_GradientFill.png
+++ b/LottieSample/screenshots/Tests_GradientFill.png
Binary files differ
diff --git a/LottieSample/screenshots/Tests_TrimPaths.png b/LottieSample/screenshots/Tests_TrimPaths.png
index ddf33ed..a0f6d9a 100644
--- a/LottieSample/screenshots/Tests_TrimPaths.png
+++ b/LottieSample/screenshots/Tests_TrimPaths.png
Binary files differ
diff --git a/lottie/src/main/java/com/airbnb/lottie/EllipseContent.java b/lottie/src/main/java/com/airbnb/lottie/EllipseContent.java
index 0ff636d..1450827 100644
--- a/lottie/src/main/java/com/airbnb/lottie/EllipseContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/EllipseContent.java
@@ -42,7 +42,8 @@
@Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
for (int i = 0; i < contentsBefore.size(); i++) {
Content content = contentsBefore.get(i);
- if (content instanceof TrimPathContent) {
+ if (content instanceof TrimPathContent &&
+ ((TrimPathContent) content).getType() == ShapeTrimPath.Type.Simultaneously) {
trimPath = (TrimPathContent) content;
trimPath.addListener(this);
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/PolystarContent.java b/lottie/src/main/java/com/airbnb/lottie/PolystarContent.java
index b109242..2f107d3 100644
--- a/lottie/src/main/java/com/airbnb/lottie/PolystarContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/PolystarContent.java
@@ -80,7 +80,8 @@
@Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
for (int i = 0; i < contentsBefore.size(); i++) {
Content content = contentsBefore.get(i);
- if (content instanceof TrimPathContent) {
+ if (content instanceof TrimPathContent &&
+ ((TrimPathContent) content).getType() == ShapeTrimPath.Type.Simultaneously) {
trimPath = (TrimPathContent) content;
trimPath.addListener(this);
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/RectangleContent.java b/lottie/src/main/java/com/airbnb/lottie/RectangleContent.java
index 09e20ae..7b865cb 100644
--- a/lottie/src/main/java/com/airbnb/lottie/RectangleContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/RectangleContent.java
@@ -46,7 +46,8 @@
@Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
for (int i = 0; i < contentsBefore.size(); i++) {
Content content = contentsBefore.get(i);
- if (content instanceof TrimPathContent) {
+ if (content instanceof TrimPathContent &&
+ ((TrimPathContent) content).getType() == ShapeTrimPath.Type.Simultaneously) {
trimPath = (TrimPathContent) content;
trimPath.addListener(this);
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapeContent.java b/lottie/src/main/java/com/airbnb/lottie/ShapeContent.java
index 4b3a65f..11ef25e 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapeContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapeContent.java
@@ -33,7 +33,9 @@
@Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
for (int i = 0; i < contentsBefore.size(); i++) {
Content content = contentsBefore.get(i);
- if (content instanceof TrimPathContent) {
+ if (content instanceof TrimPathContent &&
+ ((TrimPathContent) content).getType() == ShapeTrimPath.Type.Simultaneously) {
+ // Trim path individually will be handled by the stroke where paths are combined.
trimPath = (TrimPathContent) content;
trimPath.addListener(this);
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java b/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java
index a9da1e3..a72dea5 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java
@@ -3,12 +3,31 @@
import org.json.JSONObject;
class ShapeTrimPath {
+
+ enum Type {
+ Simultaneously,
+ Individually;
+
+ static Type forId(int id) {
+ switch (id) {
+ case 1:
+ return Simultaneously;
+ case 2:
+ return Individually;
+ default:
+ throw new IllegalArgumentException("Unknown trim path type " + id);
+ }
+ }
+ }
+
+ private final Type type;
private final AnimatableFloatValue start;
private final AnimatableFloatValue end;
private final AnimatableFloatValue offset;
- private ShapeTrimPath(AnimatableFloatValue start, AnimatableFloatValue end, AnimatableFloatValue
- offset) {
+ private ShapeTrimPath(Type type, AnimatableFloatValue start, AnimatableFloatValue end,
+ AnimatableFloatValue offset) {
+ this.type = type;
this.start = start;
this.end = end;
this.offset = offset;
@@ -20,12 +39,17 @@
static ShapeTrimPath newInstance(JSONObject json, LottieComposition composition) {
return new ShapeTrimPath(
+ Type.forId(json.optInt("m", 1)),
AnimatableFloatValue.Factory.newInstance(json.optJSONObject("s"), composition, false),
AnimatableFloatValue.Factory.newInstance(json.optJSONObject("e"), composition, false),
AnimatableFloatValue.Factory.newInstance(json.optJSONObject("o"), composition, false));
}
}
+ Type getType() {
+ return type;
+ }
+
AnimatableFloatValue getEnd() {
return end;
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/StrokeContent.java b/lottie/src/main/java/com/airbnb/lottie/StrokeContent.java
index 2eb5313..215342e 100644
--- a/lottie/src/main/java/com/airbnb/lottie/StrokeContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/StrokeContent.java
@@ -5,6 +5,7 @@
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
+import android.graphics.PathMeasure;
import android.graphics.RectF;
import android.support.annotation.Nullable;
@@ -12,11 +13,13 @@
import java.util.List;
class StrokeContent implements DrawingContent, BaseKeyframeAnimation.AnimationListener {
+ private final PathMeasure pm = new PathMeasure();
private final Path path = new Path();
+ private final Path trimPathPath = new Path();
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final RectF rect = new RectF();
private final LottieDrawable lottieDrawable;
- private final List<PathContent> paths = new ArrayList<>();
+ private final List<PathGroup> pathGroups = new ArrayList<>();
private final float[] dashPatternValues;
private final BaseKeyframeAnimation<?, Integer> colorAnimation;
@@ -75,12 +78,24 @@
}
@Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
- for (int i = 0; i < contentsAfter.size(); i++) {
+ PathGroup currentPathGroup = null;
+ for (int i = contentsAfter.size() - 1; i >= 0; i--) {
Content content = contentsAfter.get(i);
- if (content instanceof PathContent) {
- paths.add((PathContent) content);
+ 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(null);
+ }
+ currentPathGroup.paths.add((PathContent) content);
}
}
+ pathGroups.add(currentPathGroup);
}
@Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
@@ -94,18 +109,92 @@
}
applyDashPatternIfNeeded();
- path.reset();
- for (int i = 0; i < paths.size(); i++) {
- this.path.addPath(paths.get(i).getPath(), parentMatrix);
- }
+ for (int i = 0; i < pathGroups.size(); i++) {
+ PathGroup pathGroup = pathGroups.get(i);
- canvas.drawPath(path, paint);
+
+ if (pathGroup.trimPath != null) {
+ applyTrimPath(canvas, pathGroup, parentMatrix);
+ } else {
+ path.reset();
+ for (int j = pathGroup.paths.size() - 1; j >= 0; j--) {
+ path.addPath(pathGroup.paths.get(j).getPath(), parentMatrix);
+ }
+ canvas.drawPath(path, paint);
+ }
+ }
+ }
+
+ private void applyTrimPath(Canvas canvas, PathGroup pathGroup, Matrix parentMatrix) {
+ if (pathGroup.trimPath == null) {
+ 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;
+ }
}
@Override public void getBounds(RectF outBounds, Matrix parentMatrix) {
path.reset();
- for (int i = 0; i < paths.size(); i++) {
- this.path.addPath(paths.get(i).getPath(), parentMatrix);
+ 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(i).getPath(), parentMatrix);
+ }
}
path.computeBounds(rect, false);
@@ -148,4 +237,16 @@
float offset = dashPatternOffsetAnimation == null ? 0f : dashPatternOffsetAnimation.getValue();
paint.setPathEffect(new DashPathEffect(dashPatternValues, offset));
}
+
+ /**
+ * 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;
+ }
+ }
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/TrimPathContent.java b/lottie/src/main/java/com/airbnb/lottie/TrimPathContent.java
index 95517b2..e3a5a65 100644
--- a/lottie/src/main/java/com/airbnb/lottie/TrimPathContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/TrimPathContent.java
@@ -6,11 +6,13 @@
class TrimPathContent implements Content, BaseKeyframeAnimation.AnimationListener {
private final List<BaseKeyframeAnimation.AnimationListener> listeners = new ArrayList<>();
+ private final ShapeTrimPath.Type type;
private final BaseKeyframeAnimation<?, Float> startAnimation;
private final BaseKeyframeAnimation<?, Float> endAnimation;
private final BaseKeyframeAnimation<?, Float> offsetAnimation;
TrimPathContent(BaseLayer layer, ShapeTrimPath trimPath) {
+ type = trimPath.getType();
startAnimation = trimPath.getStart().createAnimation();
endAnimation = trimPath.getEnd().createAnimation();
offsetAnimation = trimPath.getOffset().createAnimation();
@@ -38,6 +40,10 @@
listeners.add(listener);
}
+ ShapeTrimPath.Type getType() {
+ return type;
+ }
+
public BaseKeyframeAnimation<?, Float> getStart() {
return startAnimation;
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/Utils.java b/lottie/src/main/java/com/airbnb/lottie/Utils.java
index a912aec..2ee4a6d 100644
--- a/lottie/src/main/java/com/airbnb/lottie/Utils.java
+++ b/lottie/src/main/java/com/airbnb/lottie/Utils.java
@@ -83,16 +83,21 @@
if (trimPath == null) {
return;
}
+ applyTrimPathIfNeeded(path, trimPath.getStart().getValue() / 100f,
+ trimPath.getEnd().getValue() / 100f, trimPath.getOffset().getValue() / 360f);
+ }
+ static void applyTrimPathIfNeeded(
+ Path path, float startValue, float endValue, float offsetValue) {
pathMeasure.setPath(path, false);
float length = pathMeasure.getLength();
- float start = length * trimPath.getStart().getValue() / 100f;
- float end = length * trimPath.getEnd().getValue() / 100f;
+ float start = length * startValue;
+ float end = length * endValue;
float newStart = Math.min(start, end);
float newEnd = Math.max(start, end);
- float offset = trimPath.getOffset().getValue() / 360f * length;
+ float offset = offsetValue * length;
newStart += offset;
newEnd += offset;