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