1ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount/*
2ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * Copyright (C) 2014 The Android Open Source Project
3ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount *
4ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * Licensed under the Apache License, Version 2.0 (the "License");
5ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * you may not use this file except in compliance with the License.
6ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * You may obtain a copy of the License at
7ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount *
8ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount *      http://www.apache.org/licenses/LICENSE-2.0
9ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount *
10ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * Unless required by applicable law or agreed to in writing, software
11ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * distributed under the License is distributed on an "AS IS" BASIS,
12ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * See the License for the specific language governing permissions and
14ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * limitations under the License.
15ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount */
16ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mountpackage android.transition;
17ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
18ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mountimport android.content.Context;
19ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mountimport android.content.res.TypedArray;
20ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mountimport android.graphics.Path;
21ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mountimport android.util.AttributeSet;
22ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
23eddc8dbed621077ddbc3a66f475920ea5f63283dGeorge Mountimport com.android.internal.R;
24eddc8dbed621077ddbc3a66f475920ea5f63283dGeorge Mount
25ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount/**
26ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * A PathMotion that generates a curved path along an arc on an imaginary circle containing
27ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * the two points. If the horizontal distance between the points is less than the vertical
28ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * distance, then the circle's center point will be horizontally aligned with the end point. If the
29ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * vertical distance is less than the horizontal distance then the circle's center point
30ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * will be vertically aligned with the end point.
31ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * <p>
32ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * When the two points are near horizontal or vertical, the curve of the motion will be
33ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * small as the center of the circle will be far from both points. To force curvature of
34ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * the path, {@link #setMinimumHorizontalAngle(float)} and
35ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * {@link #setMinimumVerticalAngle(float)} may be used to set the minimum angle of the
36ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * arc between two points.
37ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * </p>
38ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * <p>This may be used in XML as an element inside a transition.</p>
3971fbb81b14958b80fe55738607740c6630e4e9daNeil Fuller * <pre>{@code
4071fbb81b14958b80fe55738607740c6630e4e9daNeil Fuller * <changeBounds>
4171fbb81b14958b80fe55738607740c6630e4e9daNeil Fuller *   <arcMotion android:minimumHorizontalAngle="15"
42f9557619a7643c971e64e5b35583476202e77b7bGeorge Mount *              android:minimumVerticalAngle="0"
43f9557619a7643c971e64e5b35583476202e77b7bGeorge Mount *              android:maximumAngle="90"/>
4471fbb81b14958b80fe55738607740c6630e4e9daNeil Fuller * </changeBounds>}
45ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount * </pre>
46ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount */
47ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mountpublic class ArcMotion extends PathMotion {
48ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
49ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private static final float DEFAULT_MIN_ANGLE_DEGREES = 0;
50ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private static final float DEFAULT_MAX_ANGLE_DEGREES = 70;
51ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private static final float DEFAULT_MAX_TANGENT = (float)
52ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount            Math.tan(Math.toRadians(DEFAULT_MAX_ANGLE_DEGREES/2));
53ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
54ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private float mMinimumHorizontalAngle = 0;
55ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private float mMinimumVerticalAngle = 0;
56ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private float mMaximumAngle = DEFAULT_MAX_ANGLE_DEGREES;
57ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private float mMinimumHorizontalTangent = 0;
58ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private float mMinimumVerticalTangent = 0;
59ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private float mMaximumTangent = DEFAULT_MAX_TANGENT;
60ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
61ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    public ArcMotion() {}
62ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
63ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    public ArcMotion(Context context, AttributeSet attrs) {
64ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        super(context, attrs);
65ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ArcMotion);
66ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        float minimumVerticalAngle = a.getFloat(R.styleable.ArcMotion_minimumVerticalAngle,
67ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount                DEFAULT_MIN_ANGLE_DEGREES);
68ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        setMinimumVerticalAngle(minimumVerticalAngle);
69ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        float minimumHorizontalAngle = a.getFloat(R.styleable.ArcMotion_minimumHorizontalAngle,
70ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount                DEFAULT_MIN_ANGLE_DEGREES);
71ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        setMinimumHorizontalAngle(minimumHorizontalAngle);
72ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        float maximumAngle = a.getFloat(R.styleable.ArcMotion_maximumAngle,
73ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount                DEFAULT_MAX_ANGLE_DEGREES);
74ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        setMaximumAngle(maximumAngle);
75ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        a.recycle();
76ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    }
77ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
78ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    /**
79ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * Sets the minimum arc along the circle between two points aligned near horizontally.
80ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * When start and end points are close to horizontal, the calculated center point of the
81ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * circle will be far from both points, giving a near straight path between the points.
82ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * By setting a minimum angle, this forces the center point to be closer and give an
83ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * exaggerated curve to the path.
84ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * <p>The default value is 0.</p>
85ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *
86ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @param angleInDegrees The minimum angle of the arc on a circle describing the Path
87ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *                       between two nearly horizontally-separated points.
88ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle
89ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     */
90ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    public void setMinimumHorizontalAngle(float angleInDegrees) {
91ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        mMinimumHorizontalAngle = angleInDegrees;
92ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        mMinimumHorizontalTangent = toTangent(angleInDegrees);
93ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    }
94ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
95ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    /**
96ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * Returns the minimum arc along the circle between two points aligned near horizontally.
97ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * When start and end points are close to horizontal, the calculated center point of the
98ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * circle will be far from both points, giving a near straight path between the points.
99ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * By setting a minimum angle, this forces the center point to be closer and give an
100ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * exaggerated curve to the path.
101ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * <p>The default value is 0.</p>
102ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *
103ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @return  The minimum arc along the circle between two points aligned near horizontally.
104ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle
105ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     */
106ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    public float getMinimumHorizontalAngle() {
107ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        return mMinimumHorizontalAngle;
108ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    }
109ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
110ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    /**
111ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * Sets the minimum arc along the circle between two points aligned near vertically.
112ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * When start and end points are close to vertical, the calculated center point of the
113ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * circle will be far from both points, giving a near straight path between the points.
114ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * By setting a minimum angle, this forces the center point to be closer and give an
115ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * exaggerated curve to the path.
116ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * <p>The default value is 0.</p>
117ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *
118ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @param angleInDegrees The minimum angle of the arc on a circle describing the Path
119ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *                       between two nearly vertically-separated points.
120ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle
121ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     */
122ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    public void setMinimumVerticalAngle(float angleInDegrees) {
123ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        mMinimumVerticalAngle = angleInDegrees;
124ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        mMinimumVerticalTangent = toTangent(angleInDegrees);
125ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    }
126ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
127ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    /**
128ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * Returns the minimum arc along the circle between two points aligned near vertically.
129ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * When start and end points are close to vertical, the calculated center point of the
130ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * circle will be far from both points, giving a near straight path between the points.
131ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * By setting a minimum angle, this forces the center point to be closer and give an
132ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * exaggerated curve to the path.
133ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * <p>The default value is 0.</p>
134ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *
135ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @return The minimum angle of the arc on a circle describing the Path
136ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *         between two nearly vertically-separated points.
137ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle
138ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     */
139ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    public float getMinimumVerticalAngle() {
140ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        return mMinimumVerticalAngle;
141ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    }
142ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
143ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    /**
144ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * Sets the maximum arc along the circle between two points. When start and end points
145ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * have close to equal x and y differences, the curve between them is large. This forces
146ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * the curved path to have an arc of at most the given angle.
147ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * <p>The default value is 70 degrees.</p>
148ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *
149ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @param angleInDegrees The maximum angle of the arc on a circle describing the Path
150ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *                       between the start and end points.
151ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @attr ref android.R.styleable#ArcMotion_maximumAngle
152ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     */
153ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    public void setMaximumAngle(float angleInDegrees) {
154ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        mMaximumAngle = angleInDegrees;
155ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        mMaximumTangent = toTangent(angleInDegrees);
156ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    }
157ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
158ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    /**
159ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * Returns the maximum arc along the circle between two points. When start and end points
160ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * have close to equal x and y differences, the curve between them is large. This forces
161ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * the curved path to have an arc of at most the given angle.
162ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * <p>The default value is 70 degrees.</p>
163ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *
164ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @return The maximum angle of the arc on a circle describing the Path
165ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     *         between the start and end points.
166ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     * @attr ref android.R.styleable#ArcMotion_maximumAngle
167ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount     */
168ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    public float getMaximumAngle() {
169ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        return mMaximumAngle;
170ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    }
171ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
172ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    private static float toTangent(float arcInDegrees) {
173ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        if (arcInDegrees < 0 || arcInDegrees > 90) {
174ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount            throw new IllegalArgumentException("Arc must be between 0 and 90 degrees");
175ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        }
176ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        return (float) Math.tan(Math.toRadians(arcInDegrees / 2));
177ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    }
178ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
179ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    @Override
180ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    public Path getPath(float startX, float startY, float endX, float endY) {
181ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // Here's a little ascii art to show how this is calculated:
182ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // c---------- b
183ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        //  \        / |
184ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        //    \     d  |
185ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        //      \  /   e
186ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        //        a----f
187ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // This diagram assumes that the horizontal distance is less than the vertical
188ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // distance between The start point (a) and end point (b).
189ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // d is the midpoint between a and b. c is the center point of the circle with
190ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // This path is formed by assuming that start and end points are in
191ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // an arc on a circle. The end point is centered in the circle vertically
192ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // and start is a point on the circle.
193ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
194ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // Triangles bfa and bde form similar right triangles. The control points
195ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        // for the cubic Bezier arc path are the midpoints between a and e and e and b.
196ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
197ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        Path path = new Path();
198ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        path.moveTo(startX, startY);
199ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
200ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        float ex;
201ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        float ey;
2025de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float deltaX = endX - startX;
2035de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float deltaY = endY - startY;
2045de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss
2055de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        // hypotenuse squared.
2065de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float h2 = deltaX * deltaX + deltaY * deltaY;
207ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
2085de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        // Midpoint between start and end
2095de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float dx = (startX + endX) / 2;
2105de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float dy = (startY + endY) / 2;
211ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
2125de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        // Distance squared between end point and mid point is (1/2 hypotenuse)^2
2135de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float midDist2 = h2 * 0.25f;
214ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
2155de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float minimumArcDist2 = 0;
216ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
2175de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        boolean isMovingUpwards = startY > endY;
218ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
2195de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        if ((Math.abs(deltaX) < Math.abs(deltaY))) {
2205de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            // Similar triangles bfa and bde mean that (ab/fb = eb/bd)
2215de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            // Therefore, eb = ab * bd / fb
2225de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            // ab = hypotenuse
2235de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            // bd = hypotenuse/2
2245de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            // fb = deltaY
2255de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            float eDistY = Math.abs(h2 / (2 * deltaY));
2265de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            if (isMovingUpwards) {
2275de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                ey = endY + eDistY;
2285de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                ex = endX;
229ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount            } else {
2305de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                ey = startY + eDistY;
2315de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                ex = startX;
2325de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            }
233ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
2345de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            minimumArcDist2 = midDist2 * mMinimumVerticalTangent
2355de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                    * mMinimumVerticalTangent;
2365de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        } else {
2375de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            // Same as above, but flip X & Y and account for negative eDist
2385de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            float eDistX = h2 / (2 * deltaX);
2395de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            if (isMovingUpwards) {
2405de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                ex = startX + eDistX;
2415de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                ey = startY;
2425de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            } else {
2435de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                ex = endX - eDistX;
2445de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                ey = endY;
245ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount            }
246ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
2475de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            minimumArcDist2 = midDist2 * mMinimumHorizontalTangent
2485de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss                    * mMinimumHorizontalTangent;
2495de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        }
2505de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float arcDistX = dx - ex;
2515de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float arcDistY = dy - ey;
2525de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float arcDist2 = arcDistX * arcDistX + arcDistY * arcDistY;
253ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount
2545de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float maximumArcDist2 = midDist2 * mMaximumTangent * mMaximumTangent;
2555de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss
2565de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float newArcDistance2 = 0;
2575de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        if (arcDist2 < minimumArcDist2) {
2585de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            newArcDistance2 = minimumArcDist2;
2595de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        } else if (arcDist2 > maximumArcDist2) {
2605de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            newArcDistance2 = maximumArcDist2;
2615de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        }
2625de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        if (newArcDistance2 != 0) {
2635de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            float ratio2 = newArcDistance2 / arcDist2;
2645de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            float ratio = (float) Math.sqrt(ratio2);
2655de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            ex = dx + (ratio * (ex - dx));
2665de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss            ey = dy + (ratio * (ey - dy));
267ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        }
2685de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float control1X = (startX + ex) / 2;
2695de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float control1Y = (startY + ey) / 2;
2705de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float control2X = (ex + endX) / 2;
2715de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        float control2Y = (ey + endY) / 2;
2725de23c5eb4b56fdaa377fb7c098176441a17c7b4Ben Weiss        path.cubicTo(control1X, control1Y, control2X, control2Y, endX, endY);
273ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount        return path;
274ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount    }
275ecd857be3946283ebb4306e2c03ae70f5c5bb152George Mount}
276