| package com.airbnb.lottie; |
| |
| import android.graphics.PointF; |
| import android.support.annotation.FloatRange; |
| |
| import org.json.JSONArray; |
| import org.json.JSONObject; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| class ShapeData { |
| private final List<CubicCurveData> curves = new ArrayList<>(); |
| private PointF initialPoint; |
| private boolean closed; |
| |
| private ShapeData(PointF initialPoint, boolean closed, List<CubicCurveData> curves) { |
| this.initialPoint = initialPoint; |
| this.closed = closed; |
| this.curves.addAll(curves); |
| } |
| |
| ShapeData() { |
| } |
| |
| private void setInitialPoint(float x, float y) { |
| if (initialPoint == null) { |
| initialPoint = new PointF(); |
| } |
| initialPoint.set(x, y); |
| } |
| |
| PointF getInitialPoint() { |
| return initialPoint; |
| } |
| |
| boolean isClosed() { |
| return closed; |
| } |
| |
| List<CubicCurveData> getCurves() { |
| return curves; |
| } |
| |
| void interpolateBetween(ShapeData shapeData1, ShapeData shapeData2, |
| @FloatRange(from = 0f, to = 1f) float percentage) { |
| if (initialPoint == null) { |
| initialPoint = new PointF(); |
| } |
| closed = shapeData1.isClosed() || shapeData2.isClosed(); |
| |
| if (!curves.isEmpty() && curves.size() != shapeData1.getCurves().size() |
| && curves.size() != shapeData2.getCurves().size()) { |
| throw new IllegalStateException("Curves must have the same number of control points. This: " |
| + getCurves().size() |
| + "\tShape 1: " + shapeData1.getCurves().size() + "\tShape 2: " |
| + shapeData2.getCurves().size()); |
| } else if (curves.isEmpty()) { |
| for (int i = shapeData1.getCurves().size() - 1; i >= 0; i--) { |
| curves.add(new CubicCurveData()); |
| } |
| } |
| |
| PointF initialPoint1 = shapeData1.getInitialPoint(); |
| PointF initialPoint2 = shapeData2.getInitialPoint(); |
| |
| setInitialPoint(MiscUtils.lerp(initialPoint1.x, initialPoint2.x, percentage), |
| MiscUtils.lerp(initialPoint1.y, initialPoint2.y, percentage)); |
| |
| for (int i = curves.size() - 1; i >= 0; i--) { |
| CubicCurveData curve1 = shapeData1.getCurves().get(i); |
| CubicCurveData curve2 = shapeData2.getCurves().get(i); |
| |
| PointF cp11 = curve1.getControlPoint1(); |
| PointF cp21 = curve1.getControlPoint2(); |
| PointF vertex1 = curve1.getVertex(); |
| |
| PointF cp12 = curve2.getControlPoint1(); |
| PointF cp22 = curve2.getControlPoint2(); |
| PointF vertex2 = curve2.getVertex(); |
| |
| curves.get(i).setControlPoint1( |
| MiscUtils.lerp(cp11.x, cp12.x, percentage), MiscUtils.lerp(cp11.y, cp12.y, |
| percentage)); |
| curves.get(i).setControlPoint2( |
| MiscUtils.lerp(cp21.x, cp22.x, percentage), MiscUtils.lerp(cp21.y, cp22.y, |
| percentage)); |
| curves.get(i).setVertex( |
| MiscUtils.lerp(vertex1.x, vertex2.x, percentage), MiscUtils.lerp(vertex1.y, vertex2.y, |
| percentage)); |
| } |
| } |
| |
| @Override public String toString() { |
| return "ShapeData{" + "numCurves=" + curves.size() + |
| "closed=" + closed + |
| '}'; |
| } |
| |
| static class Factory implements AnimatableValue.Factory<ShapeData> { |
| static final ShapeData.Factory INSTANCE = new Factory(); |
| |
| private Factory() { |
| } |
| |
| @Override public ShapeData valueFromObject(Object object, float scale) { |
| JSONObject pointsData = null; |
| if (object instanceof JSONArray) { |
| Object firstObject = ((JSONArray) object).opt(0); |
| if (firstObject instanceof JSONObject && ((JSONObject) firstObject).has("v")) { |
| pointsData = (JSONObject) firstObject; |
| } |
| } else if (object instanceof JSONObject && ((JSONObject) object).has("v")) { |
| pointsData = (JSONObject) object; |
| } |
| |
| if (pointsData == null) { |
| return null; |
| } |
| |
| JSONArray pointsArray = pointsData.optJSONArray("v"); |
| JSONArray inTangents = pointsData.optJSONArray("i"); |
| JSONArray outTangents = pointsData.optJSONArray("o"); |
| boolean closed = pointsData.optBoolean("c", false); |
| |
| if (pointsArray == null || inTangents == null || outTangents == null || |
| pointsArray.length() != inTangents.length() || |
| pointsArray.length() != outTangents.length()) { |
| throw new IllegalStateException( |
| "Unable to process points array or tangents. " + pointsData); |
| } else if (pointsArray.length() == 0) { |
| return new ShapeData(new PointF(), false, Collections.<CubicCurveData>emptyList()); |
| } |
| |
| int length = pointsArray.length(); |
| PointF vertex = vertexAtIndex(0, pointsArray); |
| vertex.x *= scale; |
| vertex.y *= scale; |
| PointF initialPoint = vertex; |
| List<CubicCurveData> curves = new ArrayList<>(length); |
| |
| for (int i = 1; i < length; i++) { |
| vertex = vertexAtIndex(i, pointsArray); |
| PointF previousVertex = vertexAtIndex(i - 1, pointsArray); |
| PointF cp1 = vertexAtIndex(i - 1, outTangents); |
| PointF cp2 = vertexAtIndex(i, inTangents); |
| PointF shapeCp1 = MiscUtils.addPoints(previousVertex, cp1); |
| PointF shapeCp2 = MiscUtils.addPoints(vertex, cp2); |
| |
| shapeCp1.x *= scale; |
| shapeCp1.y *= scale; |
| shapeCp2.x *= scale; |
| shapeCp2.y *= scale; |
| vertex.x *= scale; |
| vertex.y *= scale; |
| |
| curves.add(new CubicCurveData(shapeCp1, shapeCp2, vertex)); |
| } |
| |
| if (closed) { |
| vertex = vertexAtIndex(0, pointsArray); |
| PointF previousVertex = vertexAtIndex(length - 1, pointsArray); |
| PointF cp1 = vertexAtIndex(length - 1, outTangents); |
| PointF cp2 = vertexAtIndex(0, inTangents); |
| |
| PointF shapeCp1 = MiscUtils.addPoints(previousVertex, cp1); |
| PointF shapeCp2 = MiscUtils.addPoints(vertex, cp2); |
| |
| if (scale != 1f) { |
| shapeCp1.x *= scale; |
| shapeCp1.y *= scale; |
| shapeCp2.x *= scale; |
| shapeCp2.y *= scale; |
| vertex.x *= scale; |
| vertex.y *= scale; |
| } |
| |
| curves.add(new CubicCurveData(shapeCp1, shapeCp2, vertex)); |
| } |
| return new ShapeData(initialPoint, closed, curves); |
| } |
| |
| private static PointF vertexAtIndex(int idx, JSONArray points) { |
| if (idx >= points.length()) { |
| throw new IllegalArgumentException( |
| "Invalid index " + idx + ". There are only " + points.length() + " points."); |
| } |
| |
| JSONArray pointArray = points.optJSONArray(idx); |
| Object x = pointArray.opt(0); |
| Object y = pointArray.opt(1); |
| return new PointF( |
| x instanceof Double ? new Float((Double) x) : (int) x, |
| y instanceof Double ? new Float((Double) y) : (int) y); |
| } |
| } |
| } |