content: /*
   * Copyright (C) 2014 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package android.transition;
  
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.Path;
  import android.util.AttributeSet;
  
  import com.android.internal.R;
  
  /**
   * A PathMotion that generates a curved path along an arc on an imaginary circle containing
   * the two points. If the horizontal distance between the points is less than the vertical
   * distance, then the circle's center point will be horizontally aligned with the end point. If the
   * vertical distance is less than the horizontal distance then the circle's center point
   * will be vertically aligned with the end point.
   * <p>
   * When the two points are near horizontal or vertical, the curve of the motion will be
   * small as the center of the circle will be far from both points. To force curvature of
   * the path, {@link #setMinimumHorizontalAngle(float)} and
   * {@link #setMinimumVerticalAngle(float)} may be used to set the minimum angle of the
   * arc between two points.
   * </p>
   * <p>This may be used in XML as an element inside a transition.</p>
   * <pre>{@code
   * <changeBounds>
   *   <arcMotion android:minimumHorizontalAngle="15"
   *              android:minimumVerticalAngle="0"
   *              android:maximumAngle="90"/>
   * </changeBounds>}
   * </pre>
   */
  public class ArcMotion extends PathMotion {
  
      private static final float DEFAULT_MIN_ANGLE_DEGREES = 0;
      private static final float DEFAULT_MAX_ANGLE_DEGREES = 70;
      private static final float DEFAULT_MAX_TANGENT = (float)
              Math.tan(Math.toRadians(DEFAULT_MAX_ANGLE_DEGREES/2));
  
      private float mMinimumHorizontalAngle = 0;
      private float mMinimumVerticalAngle = 0;
      private float mMaximumAngle = DEFAULT_MAX_ANGLE_DEGREES;
      private float mMinimumHorizontalTangent = 0;
      private float mMinimumVerticalTangent = 0;
      private float mMaximumTangent = DEFAULT_MAX_TANGENT;
  
      public ArcMotion() {}
  
      public ArcMotion(Context context, AttributeSet attrs) {
          super(context, attrs);
          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ArcMotion);
          float minimumVerticalAngle = a.getFloat(R.styleable.ArcMotion_minimumVerticalAngle,
                  DEFAULT_MIN_ANGLE_DEGREES);
          setMinimumVerticalAngle(minimumVerticalAngle);
          float minimumHorizontalAngle = a.getFloat(R.styleable.ArcMotion_minimumHorizontalAngle,
                  DEFAULT_MIN_ANGLE_DEGREES);
          setMinimumHorizontalAngle(minimumHorizontalAngle);
          float maximumAngle = a.getFloat(R.styleable.ArcMotion_maximumAngle,
                  DEFAULT_MAX_ANGLE_DEGREES);
          setMaximumAngle(maximumAngle);
          a.recycle();
      }
  
      /**
       * Sets the minimum arc along the circle between two points aligned near horizontally.
       * When start and end points are close to horizontal, the calculated center point of the
       * circle will be far from both points, giving a near straight path between the points.
       * By setting a minimum angle, this forces the center point to be closer and give an
       * exaggerated curve to the path.
       * <p>The default value is 0.</p>
       *
       * @param angleInDegrees The minimum angle of the arc on a circle describing the Path
       *                       between two nearly horizontally-separated points.
       * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle
       */
      public void setMinimumHorizontalAngle(float angleInDegrees) {
          mMinimumHorizontalAngle = angleInDegrees;
          mMinimumHorizontalTangent = toTangent(angleInDegrees);
      }
  
      /**
       * Returns the minimum arc along the circle between two points aligned near horizontally.
       * When start and end points are close to horizontal, the calculated center point of the
       * circle will be far from both points, giving a near straight path between the points.
       * By setting a minimum angle, this forces the center point to be closer and give an
       * exaggerated curve to the path.
       * <p>The default value is 0.</p>
       *
       * @return  The minimum arc along the circle between two points aligned near horizontally.
       * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle
       */
      public float getMinimumHorizontalAngle() {
          return mMinimumHorizontalAngle;
      }
  
      /**
       * Sets the minimum arc along the circle between two points aligned near vertically.
       * When start and end points are close to vertical, the calculated center point of the
       * circle will be far from both points, giving a near straight path between the points.
       * By setting a minimum angle, this forces the center point to be closer and give an
       * exaggerated curve to the path.
       * <p>The default value is 0.</p>
       *
       * @param angleInDegrees The minimum angle of the arc on a circle describing the Path
       *                       between two nearly vertically-separated points.
       * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle
       */
      public void setMinimumVerticalAngle(float angleInDegrees) {
          mMinimumVerticalAngle = angleInDegrees;
          mMinimumVerticalTangent = toTangent(angleInDegrees);
      }
  
      /**
       * Returns the minimum arc along the circle between two points aligned near vertically.
       * When start and end points are close to vertical, the calculated center point of the
       * circle will be far from both points, giving a near straight path between the points.
       * By setting a minimum angle, this forces the center point to be closer and give an
       * exaggerated curve to the path.
       * <p>The default value is 0.</p>
       *
       * @return The minimum angle of the arc on a circle describing the Path
       *         between two nearly vertically-separated points.
       * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle
       */
      public float getMinimumVerticalAngle() {
          return mMinimumVerticalAngle;
      }
  
      /**
       * Sets the maximum arc along the circle between two points. When start and end points
       * have close to equal x and y differences, the curve between them is large. This forces
       * the curved path to have an arc of at most the given angle.
       * <p>The default value is 70 degrees.</p>
       *
       * @param angleInDegrees The maximum angle of the arc on a circle describing the Path
       *                       between the start and end points.
       * @attr ref android.R.styleable#ArcMotion_maximumAngle
       */
      public void setMaximumAngle(float angleInDegrees) {
          mMaximumAngle = angleInDegrees;
          mMaximumTangent = toTangent(angleInDegrees);
      }
  
      /**
       * Returns the maximum arc along the circle between two points. When start and end points
       * have close to equal x and y differences, the curve between them is large. This forces
       * the curved path to have an arc of at most the given angle.
       * <p>The default value is 70 degrees.</p>
       *
       * @return The maximum angle of the arc on a circle describing the Path
       *         between the start and end points.
       * @attr ref android.R.styleable#ArcMotion_maximumAngle
       */
      public float getMaximumAngle() {
          return mMaximumAngle;
      }
  
      private static float toTangent(float arcInDegrees) {
          if (arcInDegrees < 0 || arcInDegrees > 90) {
              throw new IllegalArgumentException("Arc must be between 0 and 90 degrees");
          }
          return (float) Math.tan(Math.toRadians(arcInDegrees / 2));
      }
  
      @Override
      public Path getPath(float startX, float startY, float endX, float endY) {
          // Here's a little ascii art to show how this is calculated:
          // c---------- b
          //  \        / |
          //    \     d  |
          //      \  /   e
          //        a----f
          // This diagram assumes that the horizontal distance is less than the vertical
          // distance between The start point (a) and end point (b).
          // d is the midpoint between a and b. c is the center point of the circle with
          // This path is formed by assuming that start and end points are in
          // an arc on a circle. The end point is centered in the circle vertically
          // and start is a point on the circle.
  
          // Triangles bfa and bde form similar right triangles. The control points
          // for the cubic Bezier arc path are the midpoints between a and e and e and b.
  
          Path path = new Path();
          path.moveTo(startX, startY);
  
          float ex;
          float ey;
          float deltaX = endX - startX;
          float deltaY = endY - startY;
  
          // hypotenuse squared.
          float h2 = deltaX * deltaX + deltaY * deltaY;
  
          // Midpoint between start and end
          float dx = (startX + endX) / 2;
          float dy = (startY + endY) / 2;
  
          // Distance squared between end point and mid point is (1/2 hypotenuse)^2
          float midDist2 = h2 * 0.25f;
  
          float minimumArcDist2 = 0;
  
          boolean isMovingUpwards = startY > endY;
  
          if (deltaY == 0) {
              ex = dx;
              ey = dy + (Math.abs(deltaX) * 0.5f * mMinimumHorizontalTangent);
          } else if (deltaX == 0) {
              ex = dx + (Math.abs(deltaY) * 0.5f * mMinimumVerticalTangent);
              ey = dy;
          } else if ((Math.abs(deltaX) < Math.abs(deltaY))) {
              // Similar triangles bfa and bde mean that (ab/fb = eb/bd)
              // Therefore, eb = ab * bd / fb
              // ab = hypotenuse
              // bd = hypotenuse/2
              // fb = deltaY
              float eDistY = Math.abs(h2 / (2 * deltaY));
              if (isMovingUpwards) {
                  ey = endY + eDistY;
                  ex = endX;
              } else {
                  ey = startY + eDistY;
                  ex = startX;
              }
  
              minimumArcDist2 = midDist2 * mMinimumVerticalTangent
                      * mMinimumVerticalTangent;
          } else {
              // Same as above, but flip X & Y and account for negative eDist
              float eDistX = h2 / (2 * deltaX);
              if (isMovingUpwards) {
                  ex = startX + eDistX;
                  ey = startY;
              } else {
                  ex = endX - eDistX;
                  ey = endY;
              }
  
              minimumArcDist2 = midDist2 * mMinimumHorizontalTangent
                      * mMinimumHorizontalTangent;
          }
          float arcDistX = dx - ex;
          float arcDistY = dy - ey;
          float arcDist2 = arcDistX * arcDistX + arcDistY * arcDistY;
  
          float maximumArcDist2 = midDist2 * mMaximumTangent * mMaximumTangent;
  
          float newArcDistance2 = 0;
          if (arcDist2 != 0 && arcDist2 < minimumArcDist2) {
              newArcDistance2 = minimumArcDist2;
          } else if (arcDist2 > maximumArcDist2) {
              newArcDistance2 = maximumArcDist2;
          }
          if (newArcDistance2 != 0) {
              float ratio2 = newArcDistance2 / arcDist2;
              float ratio = (float) Math.sqrt(ratio2);
              ex = dx + (ratio * (ex - dx));
              ey = dy + (ratio * (ey - dy));
          }
          float control1X = (startX + ex) / 2;
          float control1Y = (startY + ey) / 2;
          float control2X = (ex + endX) / 2;
          float control2Y = (ey + endY) / 2;
          path.cubicTo(control1X, control1Y, control2X, control2Y, endX, endY);
          return path;
      }
  }
  