content: package com.airbnb.lottie.animation.content;
  
  import android.graphics.Path;
  import android.graphics.PathMeasure;
  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 Path lastSegmentPath = new Path();
    private final PathMeasure lastSegmentPathMeasure = new PathMeasure();
    private final float[] lastSegmentPosition = new float[2];
  
    private final String name;
    private final LottieDrawable lottieDrawable;
    private final PolystarShape.Type type;
    private final boolean hidden;
    private final boolean isReversed;
    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 final 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();
      isReversed = polystarShape.isReversed();
      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);
      if (isReversed) {
        anglePerPoint *= -1;
      }
      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;
  
          if (i == numPoints - 1) {
            // When there is a huge stroke, it will flash if the path ends where it starts.
            // We want the final bezier curve to end *slightly* before the start.
            // The close() call at the end will complete the polystar.
            // https://github.com/airbnb/lottie-android/issues/2329
            lastSegmentPath.reset();
            lastSegmentPath.moveTo(previousX, previousY);
            lastSegmentPath.cubicTo(previousX - cp1x, previousY - cp1y, x + cp2x, y + cp2y, x, y);
            lastSegmentPathMeasure.setPath(lastSegmentPath, false);
            lastSegmentPathMeasure.getPosTan(lastSegmentPathMeasure.getLength() * 0.9999f, lastSegmentPosition, null);
            path.cubicTo(previousX - cp1x, previousY - cp1y, x + cp2x, y + cp2y,lastSegmentPosition[0], lastSegmentPosition[1]);
          } else {
            path.cubicTo(previousX - cp1x, previousY - cp1y, x + cp2x, y + cp2y, x, y);
          }
        } else {
          if (i == numPoints - 1) {
            // When there is a huge stroke, it will flash if the path ends where it starts.
            // The close() call should make the path effectively equivalent.
            // https://github.com/airbnb/lottie-android/issues/2329
            continue;
          }
          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);
      }
    }
  }
  