blob: 06a3bb15f62876515fe44d515d5ec72a236dab94 [file] [log] [blame]
package com.airbnb.lottie.animation.content;
import android.graphics.PointF;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.airbnb.lottie.LottieDrawable;
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
import com.airbnb.lottie.model.CubicCurveData;
import com.airbnb.lottie.model.content.RoundedCorners;
import com.airbnb.lottie.model.content.ShapeData;
import com.airbnb.lottie.model.layer.BaseLayer;
import java.util.ArrayList;
import java.util.List;
public class RoundedCornersContent implements ShapeModifierContent, BaseKeyframeAnimation.AnimationListener {
/**
* Copied from:
* https://github.com/airbnb/lottie-web/blob/bb71072a26e03f1ca993da60915860f39aae890b/player/js/utils/common.js#L47
*/
private static final float ROUNDED_CORNER_MAGIC_NUMBER = 0.5519f;
private final LottieDrawable lottieDrawable;
private final String name;
private final BaseKeyframeAnimation<Float, Float> roundedCorners;
@Nullable private ShapeData shapeData;
public RoundedCornersContent(LottieDrawable lottieDrawable, BaseLayer layer, RoundedCorners roundedCorners) {
this.lottieDrawable = lottieDrawable;
this.name = roundedCorners.getName();
this.roundedCorners = roundedCorners.getCornerRadius().createAnimation();
layer.addAnimation(this.roundedCorners);
this.roundedCorners.addUpdateListener(this);
}
@Override public String getName() {
return name;
}
@Override public void onValueChanged() {
lottieDrawable.invalidateSelf();
}
@Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
// Do nothing.
}
public BaseKeyframeAnimation<Float, Float> getRoundedCorners() {
return roundedCorners;
}
/**
* Rounded corner algorithm:
* Iterate through each vertex.
* If a vertex is a sharp corner, it rounds it.
* If a vertex has control points, it is already rounded, so it does nothing.
* <p>
* To round a vertex:
* Split the vertex into two.
* Move vertex 1 directly towards the previous vertex.
* Set vertex 1's in control point to itself so it is not rounded on that side.
* Extend vertex 1's out control point towards the original vertex.
* <p>
* Repeat for vertex 2:
* Move vertex 2 directly towards the next vertex.
* Set vertex 2's out point to itself so it is not rounded on that side.
* Extend vertex 2's in control point towards the original vertex.
* <p>
* The distance that the vertices and control points are moved are relative to the
* shape's vertex distances and the roundedness set in the animation.
*/
@Override public ShapeData modifyShape(ShapeData startingShapeData) {
List<CubicCurveData> startingCurves = startingShapeData.getCurves();
if (startingCurves.size() <= 2) {
return startingShapeData;
}
float roundedness = roundedCorners.getValue();
if (roundedness == 0f) {
return startingShapeData;
}
ShapeData modifiedShapeData = getShapeData(startingShapeData);
modifiedShapeData.setInitialPoint(startingShapeData.getInitialPoint().x, startingShapeData.getInitialPoint().y);
List<CubicCurveData> modifiedCurves = modifiedShapeData.getCurves();
int modifiedCurvesIndex = 0;
boolean isClosed = startingShapeData.isClosed();
// i represents which vertex we are currently on. Refer to the docs of CubicCurveData prior to working with
// this code.
// When i == 0
// vertex=ShapeData.initialPoint
// inCp=if closed vertex else curves[size - 1].cp2
// outCp=curves[0].cp1
// When i == 1
// vertex=curves[0].vertex
// inCp=curves[0].cp2
// outCp=curves[1].cp1.
// When i == size - 1
// vertex=curves[size - 1].vertex
// inCp=curves[size - 1].cp2
// outCp=if closed vertex else curves[0].cp1
for (int i = 0; i < startingCurves.size(); i++) {
CubicCurveData startingCurve = startingCurves.get(i);
CubicCurveData previousCurve = startingCurves.get(floorMod(i - 1, startingCurves.size()));
CubicCurveData previousPreviousCurve = startingCurves.get(floorMod(i - 2, startingCurves.size()));
PointF vertex = (i == 0 && !isClosed) ? startingShapeData.getInitialPoint() : previousCurve.getVertex();
PointF inPoint = (i == 0 && !isClosed) ? vertex : previousCurve.getControlPoint2();
PointF outPoint = startingCurve.getControlPoint1();
PointF previousVertex = previousPreviousCurve.getVertex();
PointF nextVertex = startingCurve.getVertex();
// We can't round the corner of the end of a non-closed curve.
boolean isEndOfCurve = !startingShapeData.isClosed() && (i == 0 && i == startingCurves.size() - 1);
if (inPoint.equals(vertex) && outPoint.equals(vertex) && !isEndOfCurve) {
// This vertex is a point. Round its corners
float dxToPreviousVertex = vertex.x - previousVertex.x;
float dyToPreviousVertex = vertex.y - previousVertex.y;
float dxToNextVertex = nextVertex.x - vertex.x;
float dyToNextVertex = nextVertex.y - vertex.y;
float dToPreviousVertex = (float) Math.hypot(dxToPreviousVertex, dyToPreviousVertex);
float dToNextVertex = (float) Math.hypot(dxToNextVertex, dyToNextVertex);
float previousVertexPercent = Math.min(roundedness / dToPreviousVertex, 0.5f);
float nextVertexPercent = Math.min(roundedness / dToNextVertex, 0.5f);
// Split the vertex into two and move each vertex towards the previous/next vertex.
float newVertex1X = vertex.x + (previousVertex.x - vertex.x) * previousVertexPercent;
float newVertex1Y = vertex.y + (previousVertex.y - vertex.y) * previousVertexPercent;
float newVertex2X = vertex.x + (nextVertex.x - vertex.x) * nextVertexPercent;
float newVertex2Y = vertex.y + (nextVertex.y - vertex.y) * nextVertexPercent;
// Extend the new vertex control point towards the original vertex.
float newVertex1OutPointX = newVertex1X - (newVertex1X - vertex.x) * ROUNDED_CORNER_MAGIC_NUMBER;
float newVertex1OutPointY = newVertex1Y - (newVertex1Y - vertex.y) * ROUNDED_CORNER_MAGIC_NUMBER;
float newVertex2InPointX = newVertex2X - (newVertex2X - vertex.x) * ROUNDED_CORNER_MAGIC_NUMBER;
float newVertex2InPointY = newVertex2Y - (newVertex2Y - vertex.y) * ROUNDED_CORNER_MAGIC_NUMBER;
// Remap vertex/in/out point to CubicCurveData.
// Refer to the docs for CubicCurveData for more info on the difference.
CubicCurveData previousCurveData = modifiedCurves.get(floorMod(modifiedCurvesIndex - 1, modifiedCurves.size()));
CubicCurveData currentCurveData = modifiedCurves.get(modifiedCurvesIndex);
previousCurveData.setControlPoint2(newVertex1X, newVertex1Y);
previousCurveData.setVertex(newVertex1X, newVertex1Y);
if (i == 0) {
modifiedShapeData.setInitialPoint(newVertex1X, newVertex1Y);
}
currentCurveData.setControlPoint1(newVertex1OutPointX, newVertex1OutPointY);
modifiedCurvesIndex++;
previousCurveData = currentCurveData;
currentCurveData = modifiedCurves.get(modifiedCurvesIndex);
previousCurveData.setControlPoint2(newVertex2InPointX, newVertex2InPointY);
previousCurveData.setVertex(newVertex2X, newVertex2Y);
currentCurveData.setControlPoint1(newVertex2X, newVertex2Y);
modifiedCurvesIndex++;
} else {
// This vertex is not a point. Don't modify it. Refer to the documentation above and for CubicCurveData for mapping a vertex
// oriented point to CubicCurveData (path segments).
CubicCurveData previousCurveData = modifiedCurves.get(floorMod(modifiedCurvesIndex - 1, modifiedCurves.size()));
CubicCurveData currentCurveData = modifiedCurves.get(modifiedCurvesIndex);
previousCurveData.setControlPoint2(previousCurve.getVertex().x, previousCurve.getVertex().y);
previousCurveData.setVertex(previousCurve.getVertex().x, previousCurve.getVertex().y);
currentCurveData.setControlPoint1(startingCurve.getVertex().x, startingCurve.getVertex().y);
modifiedCurvesIndex++;
}
}
return modifiedShapeData;
}
/**
* Returns a shape data with the correct number of vertices for the rounded corners shape.
* This just returns the object. It does not update any values within the shape.
*/
@NonNull
private ShapeData getShapeData(ShapeData startingShapeData) {
List<CubicCurveData> startingCurves = startingShapeData.getCurves();
boolean isClosed = startingShapeData.isClosed();
int vertices = 0;
for (int i = startingCurves.size() - 1; i >= 0; i--) {
CubicCurveData startingCurve = startingCurves.get(i);
CubicCurveData previousCurve = startingCurves.get(floorMod(i - 1, startingCurves.size()));
PointF vertex = (i == 0 && !isClosed) ? startingShapeData.getInitialPoint() : previousCurve.getVertex();
PointF inPoint = (i == 0 && !isClosed) ? vertex : previousCurve.getControlPoint2();
PointF outPoint = startingCurve.getControlPoint1();
boolean isEndOfCurve = !startingShapeData.isClosed() && (i == 0 && i == startingCurves.size() - 1);
if (inPoint.equals(vertex) && outPoint.equals(vertex) && !isEndOfCurve) {
vertices += 2;
} else {
vertices += 1;
}
}
if (shapeData == null || shapeData.getCurves().size() != vertices) {
List<CubicCurveData> newCurves = new ArrayList<>(vertices);
for (int i = 0; i < vertices; i++) {
newCurves.add(new CubicCurveData());
}
shapeData = new ShapeData(new PointF(0f, 0f), false, newCurves);
}
shapeData.setClosed(isClosed);
return shapeData;
}
/**
* Copied from the API 24+ AOSP source.
*/
private static int floorMod(int x, int y) {
return x - floorDiv(x, y) * y;
}
/**
* Copied from the API 24+ AOSP source.
*/
private static int floorDiv(int x, int y) {
int r = x / y;
// if the signs are different and modulo not zero, round down
if ((x ^ y) < 0 && (r * y != x)) {
r--;
}
return r;
}
}