Home | History | Annotate | Download | only in transition
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.transition;
     17 
     18 import com.android.internal.R;
     19 
     20 import android.content.Context;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Path;
     23 import android.util.AttributeSet;
     24 import android.util.FloatMath;
     25 
     26 /**
     27  * A PathMotion that generates a curved path along an arc on an imaginary circle containing
     28  * the two points. If the horizontal distance between the points is less than the vertical
     29  * distance, then the circle's center point will be horizontally aligned with the end point. If the
     30  * vertical distance is less than the horizontal distance then the circle's center point
     31  * will be vertically aligned with the end point.
     32  * <p>
     33  * When the two points are near horizontal or vertical, the curve of the motion will be
     34  * small as the center of the circle will be far from both points. To force curvature of
     35  * the path, {@link #setMinimumHorizontalAngle(float)} and
     36  * {@link #setMinimumVerticalAngle(float)} may be used to set the minimum angle of the
     37  * arc between two points.
     38  * </p>
     39  * <p>This may be used in XML as an element inside a transition.</p>
     40  * <pre>
     41  * {@code
     42  * &lt;changeBounds>
     43  *   &lt;arcMotion android:minimumHorizontalAngle="15"
     44  *              android:minimumVerticalAngle="0"
     45  *              android:maximumAngle="90"/>
     46  * &lt;/changeBounds>}
     47  * </pre>
     48  */
     49 public class ArcMotion extends PathMotion {
     50 
     51     private static final float DEFAULT_MIN_ANGLE_DEGREES = 0;
     52     private static final float DEFAULT_MAX_ANGLE_DEGREES = 70;
     53     private static final float DEFAULT_MAX_TANGENT = (float)
     54             Math.tan(Math.toRadians(DEFAULT_MAX_ANGLE_DEGREES/2));
     55 
     56     private float mMinimumHorizontalAngle = 0;
     57     private float mMinimumVerticalAngle = 0;
     58     private float mMaximumAngle = DEFAULT_MAX_ANGLE_DEGREES;
     59     private float mMinimumHorizontalTangent = 0;
     60     private float mMinimumVerticalTangent = 0;
     61     private float mMaximumTangent = DEFAULT_MAX_TANGENT;
     62 
     63     public ArcMotion() {}
     64 
     65     public ArcMotion(Context context, AttributeSet attrs) {
     66         super(context, attrs);
     67         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ArcMotion);
     68         float minimumVerticalAngle = a.getFloat(R.styleable.ArcMotion_minimumVerticalAngle,
     69                 DEFAULT_MIN_ANGLE_DEGREES);
     70         setMinimumVerticalAngle(minimumVerticalAngle);
     71         float minimumHorizontalAngle = a.getFloat(R.styleable.ArcMotion_minimumHorizontalAngle,
     72                 DEFAULT_MIN_ANGLE_DEGREES);
     73         setMinimumHorizontalAngle(minimumHorizontalAngle);
     74         float maximumAngle = a.getFloat(R.styleable.ArcMotion_maximumAngle,
     75                 DEFAULT_MAX_ANGLE_DEGREES);
     76         setMaximumAngle(maximumAngle);
     77         a.recycle();
     78     }
     79 
     80     /**
     81      * Sets the minimum arc along the circle between two points aligned near horizontally.
     82      * When start and end points are close to horizontal, the calculated center point of the
     83      * circle will be far from both points, giving a near straight path between the points.
     84      * By setting a minimum angle, this forces the center point to be closer and give an
     85      * exaggerated curve to the path.
     86      * <p>The default value is 0.</p>
     87      *
     88      * @param angleInDegrees The minimum angle of the arc on a circle describing the Path
     89      *                       between two nearly horizontally-separated points.
     90      * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle
     91      */
     92     public void setMinimumHorizontalAngle(float angleInDegrees) {
     93         mMinimumHorizontalAngle = angleInDegrees;
     94         mMinimumHorizontalTangent = toTangent(angleInDegrees);
     95     }
     96 
     97     /**
     98      * Returns the minimum arc along the circle between two points aligned near horizontally.
     99      * When start and end points are close to horizontal, the calculated center point of the
    100      * circle will be far from both points, giving a near straight path between the points.
    101      * By setting a minimum angle, this forces the center point to be closer and give an
    102      * exaggerated curve to the path.
    103      * <p>The default value is 0.</p>
    104      *
    105      * @return  The minimum arc along the circle between two points aligned near horizontally.
    106      * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle
    107      */
    108     public float getMinimumHorizontalAngle() {
    109         return mMinimumHorizontalAngle;
    110     }
    111 
    112     /**
    113      * Sets the minimum arc along the circle between two points aligned near vertically.
    114      * When start and end points are close to vertical, the calculated center point of the
    115      * circle will be far from both points, giving a near straight path between the points.
    116      * By setting a minimum angle, this forces the center point to be closer and give an
    117      * exaggerated curve to the path.
    118      * <p>The default value is 0.</p>
    119      *
    120      * @param angleInDegrees The minimum angle of the arc on a circle describing the Path
    121      *                       between two nearly vertically-separated points.
    122      * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle
    123      */
    124     public void setMinimumVerticalAngle(float angleInDegrees) {
    125         mMinimumVerticalAngle = angleInDegrees;
    126         mMinimumVerticalTangent = toTangent(angleInDegrees);
    127     }
    128 
    129     /**
    130      * Returns the minimum arc along the circle between two points aligned near vertically.
    131      * When start and end points are close to vertical, the calculated center point of the
    132      * circle will be far from both points, giving a near straight path between the points.
    133      * By setting a minimum angle, this forces the center point to be closer and give an
    134      * exaggerated curve to the path.
    135      * <p>The default value is 0.</p>
    136      *
    137      * @return The minimum angle of the arc on a circle describing the Path
    138      *         between two nearly vertically-separated points.
    139      * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle
    140      */
    141     public float getMinimumVerticalAngle() {
    142         return mMinimumVerticalAngle;
    143     }
    144 
    145     /**
    146      * Sets the maximum arc along the circle between two points. When start and end points
    147      * have close to equal x and y differences, the curve between them is large. This forces
    148      * the curved path to have an arc of at most the given angle.
    149      * <p>The default value is 70 degrees.</p>
    150      *
    151      * @param angleInDegrees The maximum angle of the arc on a circle describing the Path
    152      *                       between the start and end points.
    153      * @attr ref android.R.styleable#ArcMotion_maximumAngle
    154      */
    155     public void setMaximumAngle(float angleInDegrees) {
    156         mMaximumAngle = angleInDegrees;
    157         mMaximumTangent = toTangent(angleInDegrees);
    158     }
    159 
    160     /**
    161      * Returns the maximum arc along the circle between two points. When start and end points
    162      * have close to equal x and y differences, the curve between them is large. This forces
    163      * the curved path to have an arc of at most the given angle.
    164      * <p>The default value is 70 degrees.</p>
    165      *
    166      * @return The maximum angle of the arc on a circle describing the Path
    167      *         between the start and end points.
    168      * @attr ref android.R.styleable#ArcMotion_maximumAngle
    169      */
    170     public float getMaximumAngle() {
    171         return mMaximumAngle;
    172     }
    173 
    174     private static float toTangent(float arcInDegrees) {
    175         if (arcInDegrees < 0 || arcInDegrees > 90) {
    176             throw new IllegalArgumentException("Arc must be between 0 and 90 degrees");
    177         }
    178         return (float) Math.tan(Math.toRadians(arcInDegrees / 2));
    179     }
    180 
    181     @Override
    182     public Path getPath(float startX, float startY, float endX, float endY) {
    183         // Here's a little ascii art to show how this is calculated:
    184         // c---------- b
    185         //  \        / |
    186         //    \     d  |
    187         //      \  /   e
    188         //        a----f
    189         // This diagram assumes that the horizontal distance is less than the vertical
    190         // distance between The start point (a) and end point (b).
    191         // d is the midpoint between a and b. c is the center point of the circle with
    192         // This path is formed by assuming that start and end points are in
    193         // an arc on a circle. The end point is centered in the circle vertically
    194         // and start is a point on the circle.
    195 
    196         // Triangles bfa and bde form similar right triangles. The control points
    197         // for the cubic Bezier arc path are the midpoints between a and e and e and b.
    198 
    199         Path path = new Path();
    200         path.moveTo(startX, startY);
    201 
    202         float ex;
    203         float ey;
    204         if (startY == endY) {
    205             ex = (startX + endX) / 2;
    206             ey = startY + mMinimumHorizontalTangent * Math.abs(endX - startX) / 2;
    207         } else if (startX == endX) {
    208             ex = startX + mMinimumVerticalTangent * Math.abs(endY - startY) / 2;
    209             ey = (startY + endY) / 2;
    210         } else {
    211             float deltaX = endX - startX;
    212             float deltaY = startY - endY; // Y is inverted compared to diagram above.
    213             // hypotenuse squared.
    214             float h2 = deltaX * deltaX + deltaY * deltaY;
    215 
    216             // Midpoint between start and end
    217             float dx = (startX + endX) / 2;
    218             float dy = (startY + endY) / 2;
    219 
    220             // Distance squared between end point and mid point is (1/2 hypotenuse)^2
    221             float midDist2 = h2 * 0.25f;
    222 
    223             float minimumArcDist2 = 0;
    224 
    225             if (Math.abs(deltaX) < Math.abs(deltaY)) {
    226                 // Similar triangles bfa and bde mean that (ab/fb = eb/bd)
    227                 // Therefore, eb = ab * bd / fb
    228                 // ab = hypotenuse
    229                 // bd = hypotenuse/2
    230                 // fb = deltaY
    231                 float eDistY = h2 / (2 * deltaY);
    232                 ey = endY + eDistY;
    233                 ex = endX;
    234 
    235                 minimumArcDist2 = midDist2 * mMinimumVerticalTangent
    236                         * mMinimumVerticalTangent;
    237             } else {
    238                 // Same as above, but flip X & Y
    239                 float eDistX = h2 / (2 * deltaX);
    240                 ex = endX + eDistX;
    241                 ey = endY;
    242 
    243                 minimumArcDist2 = midDist2 * mMinimumHorizontalTangent
    244                         * mMinimumHorizontalTangent;
    245             }
    246             float arcDistX = dx - ex;
    247             float arcDistY = dy - ey;
    248             float arcDist2 = arcDistX * arcDistX + arcDistY * arcDistY;
    249 
    250             float maximumArcDist2 = midDist2 * mMaximumTangent * mMaximumTangent;
    251 
    252             float newArcDistance2 = 0;
    253             if (arcDist2 < minimumArcDist2) {
    254                 newArcDistance2 = minimumArcDist2;
    255             } else if (arcDist2 > maximumArcDist2) {
    256                 newArcDistance2 = maximumArcDist2;
    257             }
    258             if (newArcDistance2 != 0) {
    259                 float ratio2 = newArcDistance2 / arcDist2;
    260                 float ratio = FloatMath.sqrt(ratio2);
    261                 ex = dx + (ratio * (ex - dx));
    262                 ey = dy + (ratio * (ey - dy));
    263             }
    264         }
    265         float controlX1 = (startX + ex) / 2;
    266         float controlY1 = (startY + ey) / 2;
    267         float controlX2 = (ex + endX) / 2;
    268         float controlY2 = (ey + endY) / 2;
    269         path.cubicTo(controlX1, controlY1, controlX2, controlY2, endX, endY);
    270         return path;
    271     }
    272 }
    273