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 */ 16package android.transition; 17 18import android.content.Context; 19import android.content.res.TypedArray; 20import android.graphics.Path; 21import android.util.AttributeSet; 22 23import 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 */ 47public 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 if (startY == endY) { 203 ex = (startX + endX) / 2; 204 ey = startY + mMinimumHorizontalTangent * Math.abs(endX - startX) / 2; 205 } else if (startX == endX) { 206 ex = startX + mMinimumVerticalTangent * Math.abs(endY - startY) / 2; 207 ey = (startY + endY) / 2; 208 } else { 209 float deltaX = endX - startX; 210 float deltaY = endY - startY; 211 // hypotenuse squared. 212 float h2 = deltaX * deltaX + deltaY * deltaY; 213 214 // Midpoint between start and end 215 float dx = (startX + endX) / 2; 216 float dy = (startY + endY) / 2; 217 218 // Distance squared between end point and mid point is (1/2 hypotenuse)^2 219 float midDist2 = h2 * 0.25f; 220 221 float minimumArcDist2 = 0; 222 boolean isQuadrant1Or3 = (deltaX * deltaY) > 0; 223 224 if ((Math.abs(deltaX) < Math.abs(deltaY))) { 225 // Similar triangles bfa and bde mean that (ab/fb = eb/bd) 226 // Therefore, eb = ab * bd / fb 227 // ab = hypotenuse 228 // bd = hypotenuse/2 229 // fb = deltaY 230 float eDistY = h2 / (2 * deltaY); 231 if (isQuadrant1Or3) { 232 ey = startY + eDistY; 233 ex = startX; 234 } else { 235 ey = endY - eDistY; 236 ex = endX; 237 } 238 239 minimumArcDist2 = midDist2 * mMinimumVerticalTangent 240 * mMinimumVerticalTangent; 241 } else { 242 // Same as above, but flip X & Y 243 float eDistX = h2 / (2 * deltaX); 244 if (isQuadrant1Or3) { 245 ex = endX - eDistX; 246 ey = endY; 247 } else { 248 ex = startX + eDistX; 249 ey = startY; 250 } 251 252 minimumArcDist2 = midDist2 * mMinimumHorizontalTangent 253 * mMinimumHorizontalTangent; 254 } 255 float arcDistX = dx - ex; 256 float arcDistY = dy - ey; 257 float arcDist2 = arcDistX * arcDistX + arcDistY * arcDistY; 258 259 float maximumArcDist2 = midDist2 * mMaximumTangent * mMaximumTangent; 260 261 float newArcDistance2 = 0; 262 if (arcDist2 < minimumArcDist2) { 263 newArcDistance2 = minimumArcDist2; 264 } else if (arcDist2 > maximumArcDist2) { 265 newArcDistance2 = maximumArcDist2; 266 } 267 if (newArcDistance2 != 0) { 268 float ratio2 = newArcDistance2 / arcDist2; 269 float ratio = (float) Math.sqrt(ratio2); 270 ex = dx + (ratio * (ex - dx)); 271 ey = dy + (ratio * (ey - dy)); 272 } 273 } 274 float controlX1 = (startX + ex) / 2; 275 float controlY1 = (startY + ey) / 2; 276 float controlX2 = (ex + endX) / 2; 277 float controlY2 = (ey + endY) / 2; 278 path.cubicTo(controlX1, controlY1, controlX2, controlY2, endX, endY); 279 return path; 280 } 281} 282