blob: dc9ab8f2f71bff6454d651c49081525c4b7b6d0a [file] [log] [blame]
package com.airbnb.lottie.animation.content;
import android.graphics.Path;
import android.graphics.PointF;
import androidx.annotation.Nullable;
import com.airbnb.lottie.LottieDrawable;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
import com.airbnb.lottie.model.KeyPath;
import com.airbnb.lottie.model.content.PolystarShape;
import com.airbnb.lottie.model.content.ShapeTrimPath;
import com.airbnb.lottie.model.layer.BaseLayer;
import com.airbnb.lottie.utils.MiscUtils;
import com.airbnb.lottie.value.LottieValueCallback;
import java.util.List;
public class PolystarContent
implements PathContent, BaseKeyframeAnimation.AnimationListener, KeyPathElementContent {
/**
* This was empirically derived by creating polystars, converting them to
* curves, and calculating a scale factor.
* It works best for polygons and stars with 3 points and needs more
* work otherwise.
*/
private static final float POLYSTAR_MAGIC_NUMBER = .47829f;
private static final float POLYGON_MAGIC_NUMBER = .25f;
private final Path path = new Path();
private final String name;
private final LottieDrawable lottieDrawable;
private final PolystarShape.Type type;
private final boolean hidden;
private final BaseKeyframeAnimation<?, Float> pointsAnimation;
private final BaseKeyframeAnimation<?, PointF> positionAnimation;
private final BaseKeyframeAnimation<?, Float> rotationAnimation;
@Nullable private final BaseKeyframeAnimation<?, Float> innerRadiusAnimation;
private final BaseKeyframeAnimation<?, Float> outerRadiusAnimation;
@Nullable private final BaseKeyframeAnimation<?, Float> innerRoundednessAnimation;
private final BaseKeyframeAnimation<?, Float> outerRoundednessAnimation;
private CompoundTrimPathContent trimPaths = new CompoundTrimPathContent();
private boolean isPathValid;
public PolystarContent(LottieDrawable lottieDrawable, BaseLayer layer,
PolystarShape polystarShape) {
this.lottieDrawable = lottieDrawable;
name = polystarShape.getName();
type = polystarShape.getType();
hidden = polystarShape.isHidden();
pointsAnimation = polystarShape.getPoints().createAnimation();
positionAnimation = polystarShape.getPosition().createAnimation();
rotationAnimation = polystarShape.getRotation().createAnimation();
outerRadiusAnimation = polystarShape.getOuterRadius().createAnimation();
outerRoundednessAnimation = polystarShape.getOuterRoundedness().createAnimation();
if (type == PolystarShape.Type.STAR) {
innerRadiusAnimation = polystarShape.getInnerRadius().createAnimation();
innerRoundednessAnimation = polystarShape.getInnerRoundedness().createAnimation();
} else {
innerRadiusAnimation = null;
innerRoundednessAnimation = null;
}
layer.addAnimation(pointsAnimation);
layer.addAnimation(positionAnimation);
layer.addAnimation(rotationAnimation);
layer.addAnimation(outerRadiusAnimation);
layer.addAnimation(outerRoundednessAnimation);
if (type == PolystarShape.Type.STAR) {
layer.addAnimation(innerRadiusAnimation);
layer.addAnimation(innerRoundednessAnimation);
}
pointsAnimation.addUpdateListener(this);
positionAnimation.addUpdateListener(this);
rotationAnimation.addUpdateListener(this);
outerRadiusAnimation.addUpdateListener(this);
outerRoundednessAnimation.addUpdateListener(this);
if (type == PolystarShape.Type.STAR) {
innerRadiusAnimation.addUpdateListener(this);
innerRoundednessAnimation.addUpdateListener(this);
}
}
@Override public void onValueChanged() {
invalidate();
}
private void invalidate() {
isPathValid = false;
lottieDrawable.invalidateSelf();
}
@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 &&
((TrimPathContent) content).getType() == ShapeTrimPath.Type.SIMULTANEOUSLY) {
TrimPathContent trimPath = (TrimPathContent) content;
trimPaths.addTrimPath(trimPath);
trimPath.addListener(this);
}
}
}
@Override public Path getPath() {
if (isPathValid) {
return path;
}
path.reset();
if (hidden) {
isPathValid = true;
return path;
}
switch (type) {
case STAR:
createStarPath();
break;
case POLYGON:
createPolygonPath();
break;
}
path.close();
trimPaths.apply(path);
isPathValid = true;
return path;
}
@Override public String getName() {
return name;
}
private void createStarPath() {
float points = pointsAnimation.getValue();
double currentAngle = rotationAnimation == null ? 0f : rotationAnimation.getValue();
// Start at +y instead of +x
currentAngle -= 90;
// convert to radians
currentAngle = Math.toRadians(currentAngle);
// adjust current angle for partial points
float anglePerPoint = (float) (2 * Math.PI / points);
float halfAnglePerPoint = anglePerPoint / 2.0f;
float partialPointAmount = points - (int) points;
if (partialPointAmount != 0) {
currentAngle += halfAnglePerPoint * (1f - partialPointAmount);
}
float outerRadius = outerRadiusAnimation.getValue();
//noinspection ConstantConditions
float innerRadius = innerRadiusAnimation.getValue();
float innerRoundedness = 0f;
if (innerRoundednessAnimation != null) {
innerRoundedness = innerRoundednessAnimation.getValue() / 100f;
}
float outerRoundedness = 0f;
if (outerRoundednessAnimation != null) {
outerRoundedness = outerRoundednessAnimation.getValue() / 100f;
}
float x;
float y;
float previousX;
float previousY;
float partialPointRadius = 0;
if (partialPointAmount != 0) {
partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius);
x = (float) (partialPointRadius * Math.cos(currentAngle));
y = (float) (partialPointRadius * Math.sin(currentAngle));
path.moveTo(x, y);
currentAngle += anglePerPoint * partialPointAmount / 2f;
} else {
x = (float) (outerRadius * Math.cos(currentAngle));
y = (float) (outerRadius * Math.sin(currentAngle));
path.moveTo(x, y);
currentAngle += halfAnglePerPoint;
}
// True means the line will go to outer radius. False means inner radius.
boolean longSegment = false;
double numPoints = Math.ceil(points) * 2;
for (int i = 0; i < numPoints; i++) {
float radius = longSegment ? outerRadius : innerRadius;
float dTheta = halfAnglePerPoint;
if (partialPointRadius != 0 && i == numPoints - 2) {
dTheta = anglePerPoint * partialPointAmount / 2f;
}
if (partialPointRadius != 0 && i == numPoints - 1) {
radius = partialPointRadius;
}
previousX = x;
previousY = y;
x = (float) (radius * Math.cos(currentAngle));
y = (float) (radius * Math.sin(currentAngle));
if (innerRoundedness == 0 && outerRoundedness == 0) {
path.lineTo(x, y);
} else {
float cp1Theta = (float) (Math.atan2(previousY, previousX) - Math.PI / 2f);
float cp1Dx = (float) Math.cos(cp1Theta);
float cp1Dy = (float) Math.sin(cp1Theta);
float cp2Theta = (float) (Math.atan2(y, x) - Math.PI / 2f);
float cp2Dx = (float) Math.cos(cp2Theta);
float cp2Dy = (float) Math.sin(cp2Theta);
float cp1Roundedness = longSegment ? innerRoundedness : outerRoundedness;
float cp2Roundedness = longSegment ? outerRoundedness : innerRoundedness;
float cp1Radius = longSegment ? innerRadius : outerRadius;
float cp2Radius = longSegment ? outerRadius : innerRadius;
float cp1x = cp1Radius * cp1Roundedness * POLYSTAR_MAGIC_NUMBER * cp1Dx;
float cp1y = cp1Radius * cp1Roundedness * POLYSTAR_MAGIC_NUMBER * cp1Dy;
float cp2x = cp2Radius * cp2Roundedness * POLYSTAR_MAGIC_NUMBER * cp2Dx;
float cp2y = cp2Radius * cp2Roundedness * POLYSTAR_MAGIC_NUMBER * cp2Dy;
if (partialPointAmount != 0) {
if (i == 0) {
cp1x *= partialPointAmount;
cp1y *= partialPointAmount;
} else if (i == numPoints - 1) {
cp2x *= partialPointAmount;
cp2y *= partialPointAmount;
}
}
path.cubicTo(previousX - cp1x, previousY - cp1y, x + cp2x, y + cp2y, x, y);
}
currentAngle += dTheta;
longSegment = !longSegment;
}
PointF position = positionAnimation.getValue();
path.offset(position.x, position.y);
path.close();
}
private void createPolygonPath() {
int points = (int) Math.floor(pointsAnimation.getValue());
double currentAngle = rotationAnimation == null ? 0f : rotationAnimation.getValue();
// Start at +y instead of +x
currentAngle -= 90;
// convert to radians
currentAngle = Math.toRadians(currentAngle);
// adjust current angle for partial points
float anglePerPoint = (float) (2 * Math.PI / points);
float roundedness = outerRoundednessAnimation.getValue() / 100f;
float radius = outerRadiusAnimation.getValue();
float x;
float y;
float previousX;
float previousY;
x = (float) (radius * Math.cos(currentAngle));
y = (float) (radius * Math.sin(currentAngle));
path.moveTo(x, y);
currentAngle += anglePerPoint;
double numPoints = Math.ceil(points);
for (int i = 0; i < numPoints; i++) {
previousX = x;
previousY = y;
x = (float) (radius * Math.cos(currentAngle));
y = (float) (radius * Math.sin(currentAngle));
if (roundedness != 0) {
float cp1Theta = (float) (Math.atan2(previousY, previousX) - Math.PI / 2f);
float cp1Dx = (float) Math.cos(cp1Theta);
float cp1Dy = (float) Math.sin(cp1Theta);
float cp2Theta = (float) (Math.atan2(y, x) - Math.PI / 2f);
float cp2Dx = (float) Math.cos(cp2Theta);
float cp2Dy = (float) Math.sin(cp2Theta);
float cp1x = radius * roundedness * POLYGON_MAGIC_NUMBER * cp1Dx;
float cp1y = radius * roundedness * POLYGON_MAGIC_NUMBER * cp1Dy;
float cp2x = radius * roundedness * POLYGON_MAGIC_NUMBER * cp2Dx;
float cp2y = radius * roundedness * POLYGON_MAGIC_NUMBER * cp2Dy;
path.cubicTo(previousX - cp1x, previousY - cp1y, x + cp2x, y + cp2y, x, y);
} else {
path.lineTo(x, y);
}
currentAngle += anglePerPoint;
}
PointF position = positionAnimation.getValue();
path.offset(position.x, position.y);
path.close();
}
@Override public void resolveKeyPath(
KeyPath keyPath, int depth, List<KeyPath> accumulator, KeyPath currentPartialKeyPath) {
MiscUtils.resolveKeyPath(keyPath, depth, accumulator, currentPartialKeyPath, this);
}
@SuppressWarnings("unchecked")
@Override
public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> callback) {
if (property == LottieProperty.POLYSTAR_POINTS) {
pointsAnimation.setValueCallback((LottieValueCallback<Float>) callback);
} else if (property == LottieProperty.POLYSTAR_ROTATION) {
rotationAnimation.setValueCallback((LottieValueCallback<Float>) callback);
} else if (property == LottieProperty.POSITION) {
positionAnimation.setValueCallback((LottieValueCallback<PointF>) callback);
} else if (property == LottieProperty.POLYSTAR_INNER_RADIUS && innerRadiusAnimation != null) {
innerRadiusAnimation.setValueCallback((LottieValueCallback<Float>) callback);
} else if (property == LottieProperty.POLYSTAR_OUTER_RADIUS) {
outerRadiusAnimation.setValueCallback((LottieValueCallback<Float>) callback);
} else if (property == LottieProperty.POLYSTAR_INNER_ROUNDEDNESS && innerRoundednessAnimation != null) {
innerRoundednessAnimation.setValueCallback((LottieValueCallback<Float>) callback);
} else if (property == LottieProperty.POLYSTAR_OUTER_ROUNDEDNESS) {
outerRoundednessAnimation.setValueCallback((LottieValueCallback<Float>) callback);
}
}
}