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 * <changeBounds> 43 * <arcMotion android:minimumHorizontalAngle="15" 44 * android:minimumVerticalAngle="0" 45 * android:maximumAngle="90"/> 46 * </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