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