1 package org.robolectric.shadows; 2 3 import java.awt.geom.AffineTransform; 4 import java.awt.geom.PathIterator; 5 import java.awt.geom.Rectangle2D; 6 import java.awt.geom.RectangularShape; 7 import java.awt.geom.RoundRectangle2D; 8 import java.util.EnumSet; 9 import java.util.NoSuchElementException; 10 11 /** 12 * Defines a rectangle with rounded corners, where the sizes of the corners are potentially 13 * different. 14 * 15 * <p>Copied from 16 * https://github.com/aosp-mirror/platform_frameworks_base/blob/oreo-release/tools/layoutlib/bridge/src/android/graphics/RoundRectangle.java 17 */ 18 public class RoundRectangle extends RectangularShape { 19 public double x; 20 public double y; 21 public double width; 22 public double height; 23 public double ulWidth; 24 public double ulHeight; 25 public double urWidth; 26 public double urHeight; 27 public double lrWidth; 28 public double lrHeight; 29 public double llWidth; 30 public double llHeight; 31 32 private enum Zone { 33 CLOSE_OUTSIDE, 34 CLOSE_INSIDE, 35 MIDDLE, 36 FAR_INSIDE, 37 FAR_OUTSIDE 38 } 39 40 private final EnumSet<Zone> close = EnumSet.of(Zone.CLOSE_OUTSIDE, Zone.CLOSE_INSIDE); 41 private final EnumSet<Zone> far = EnumSet.of(Zone.FAR_OUTSIDE, Zone.FAR_INSIDE); 42 43 /** 44 * @param cornerDimensions array of 8 floating-point number corresponding to the width and the 45 * height of each corner in the following order: upper-left, upper-right, lower-right, 46 * lower-left. It assumes for the size the same convention as {@link RoundRectangle2D}, that 47 * is that the width and height of a corner correspond to the total width and height of the 48 * ellipse that corner is a quarter of. 49 */ RoundRectangle(float x, float y, float width, float height, float[] cornerDimensions)50 public RoundRectangle(float x, float y, float width, float height, float[] cornerDimensions) { 51 assert cornerDimensions.length == 8 52 : "The array of corner dimensions must have eight " + "elements"; 53 54 this.x = x; 55 this.y = y; 56 this.width = width; 57 this.height = height; 58 59 float[] dimensions = cornerDimensions.clone(); 60 // If a value is negative, the corresponding corner is squared 61 for (int i = 0; i < dimensions.length; i += 2) { 62 if (dimensions[i] < 0 || dimensions[i + 1] < 0) { 63 dimensions[i] = 0; 64 dimensions[i + 1] = 0; 65 } 66 } 67 68 double topCornerWidth = (dimensions[0] + dimensions[2]) / 2d; 69 double bottomCornerWidth = (dimensions[4] + dimensions[6]) / 2d; 70 double leftCornerHeight = (dimensions[1] + dimensions[7]) / 2d; 71 double rightCornerHeight = (dimensions[3] + dimensions[5]) / 2d; 72 73 // Rescale the corner dimensions if they are bigger than the rectangle 74 double scale = Math.min(1.0, width / topCornerWidth); 75 scale = Math.min(scale, width / bottomCornerWidth); 76 scale = Math.min(scale, height / leftCornerHeight); 77 scale = Math.min(scale, height / rightCornerHeight); 78 79 this.ulWidth = dimensions[0] * scale; 80 this.ulHeight = dimensions[1] * scale; 81 this.urWidth = dimensions[2] * scale; 82 this.urHeight = dimensions[3] * scale; 83 this.lrWidth = dimensions[4] * scale; 84 this.lrHeight = dimensions[5] * scale; 85 this.llWidth = dimensions[6] * scale; 86 this.llHeight = dimensions[7] * scale; 87 } 88 89 @Override getX()90 public double getX() { 91 return x; 92 } 93 94 @Override getY()95 public double getY() { 96 return y; 97 } 98 99 @Override getWidth()100 public double getWidth() { 101 return width; 102 } 103 104 @Override getHeight()105 public double getHeight() { 106 return height; 107 } 108 109 @Override isEmpty()110 public boolean isEmpty() { 111 return (width <= 0d) || (height <= 0d); 112 } 113 114 @Override setFrame(double x, double y, double w, double h)115 public void setFrame(double x, double y, double w, double h) { 116 this.x = x; 117 this.y = y; 118 this.width = w; 119 this.height = h; 120 } 121 122 @Override getBounds2D()123 public Rectangle2D getBounds2D() { 124 return new Rectangle2D.Double(x, y, width, height); 125 } 126 127 @Override contains(double x, double y)128 public boolean contains(double x, double y) { 129 if (isEmpty()) { 130 return false; 131 } 132 133 double x0 = getX(); 134 double y0 = getY(); 135 double x1 = x0 + getWidth(); 136 double y1 = y0 + getHeight(); 137 // Check for trivial rejection - point is outside bounding rectangle 138 if (x < x0 || y < y0 || x >= x1 || y >= y1) { 139 return false; 140 } 141 142 double insideTopX0 = x0 + ulWidth / 2d; 143 double insideLeftY0 = y0 + ulHeight / 2d; 144 if (x < insideTopX0 && y < insideLeftY0) { 145 // In the upper-left corner 146 return isInsideCorner(x - insideTopX0, y - insideLeftY0, ulWidth / 2d, ulHeight / 2d); 147 } 148 149 double insideTopX1 = x1 - urWidth / 2d; 150 double insideRightY0 = y0 + urHeight / 2d; 151 if (x > insideTopX1 && y < insideRightY0) { 152 // In the upper-right corner 153 return isInsideCorner(x - insideTopX1, y - insideRightY0, urWidth / 2d, urHeight / 2d); 154 } 155 156 double insideBottomX1 = x1 - lrWidth / 2d; 157 double insideRightY1 = y1 - lrHeight / 2d; 158 if (x > insideBottomX1 && y > insideRightY1) { 159 // In the lower-right corner 160 return isInsideCorner(x - insideBottomX1, y - insideRightY1, lrWidth / 2d, lrHeight / 2d); 161 } 162 163 double insideBottomX0 = x0 + llWidth / 2d; 164 double insideLeftY1 = y1 - llHeight / 2d; 165 if (x < insideBottomX0 && y > insideLeftY1) { 166 // In the lower-left corner 167 return isInsideCorner(x - insideBottomX0, y - insideLeftY1, llWidth / 2d, llHeight / 2d); 168 } 169 170 // In the central part of the rectangle 171 return true; 172 } 173 isInsideCorner(double x, double y, double width, double height)174 private boolean isInsideCorner(double x, double y, double width, double height) { 175 double squareDist = height * height * x * x + width * width * y * y; 176 return squareDist <= width * width * height * height; 177 } 178 classify( double coord, double side1, double arcSize1, double side2, double arcSize2)179 private Zone classify( 180 double coord, double side1, double arcSize1, double side2, double arcSize2) { 181 if (coord < side1) { 182 return Zone.CLOSE_OUTSIDE; 183 } else if (coord < side1 + arcSize1) { 184 return Zone.CLOSE_INSIDE; 185 } else if (coord < side2 - arcSize2) { 186 return Zone.MIDDLE; 187 } else if (coord < side2) { 188 return Zone.FAR_INSIDE; 189 } else { 190 return Zone.FAR_OUTSIDE; 191 } 192 } 193 194 @Override intersects(double x, double y, double w, double h)195 public boolean intersects(double x, double y, double w, double h) { 196 if (isEmpty() || w <= 0 || h <= 0) { 197 return false; 198 } 199 double x0 = getX(); 200 double y0 = getY(); 201 double x1 = x0 + getWidth(); 202 double y1 = y0 + getHeight(); 203 // Check for trivial rejection - bounding rectangles do not intersect 204 if (x + w <= x0 || x >= x1 || y + h <= y0 || y >= y1) { 205 return false; 206 } 207 208 double maxLeftCornerWidth = Math.max(ulWidth, llWidth) / 2d; 209 double maxRightCornerWidth = Math.max(urWidth, lrWidth) / 2d; 210 double maxUpperCornerHeight = Math.max(ulHeight, urHeight) / 2d; 211 double maxLowerCornerHeight = Math.max(llHeight, lrHeight) / 2d; 212 Zone x0class = classify(x, x0, maxLeftCornerWidth, x1, maxRightCornerWidth); 213 Zone x1class = classify(x + w, x0, maxLeftCornerWidth, x1, maxRightCornerWidth); 214 Zone y0class = classify(y, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight); 215 Zone y1class = classify(y + h, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight); 216 217 // Trivially accept if any point is inside inner rectangle 218 if (x0class == Zone.MIDDLE 219 || x1class == Zone.MIDDLE 220 || y0class == Zone.MIDDLE 221 || y1class == Zone.MIDDLE) { 222 return true; 223 } 224 // Trivially accept if either edge spans inner rectangle 225 if ((close.contains(x0class) && far.contains(x1class)) 226 || (close.contains(y0class) && far.contains(y1class))) { 227 return true; 228 } 229 230 // Since neither edge spans the center, then one of the corners 231 // must be in one of the rounded edges. We detect this case if 232 // a [xy]0class is 3 or a [xy]1class is 1. One of those two cases 233 // must be true for each direction. 234 // We now find a "nearest point" to test for being inside a rounded 235 // corner. 236 if (x1class == Zone.CLOSE_INSIDE && y1class == Zone.CLOSE_INSIDE) { 237 // Potentially in upper-left corner 238 x = x + w - x0 - ulWidth / 2d; 239 y = y + h - y0 - ulHeight / 2d; 240 return x > 0 || y > 0 || isInsideCorner(x, y, ulWidth / 2d, ulHeight / 2d); 241 } 242 if (x1class == Zone.CLOSE_INSIDE) { 243 // Potentially in lower-left corner 244 x = x + w - x0 - llWidth / 2d; 245 y = y - y1 + llHeight / 2d; 246 return x > 0 || y < 0 || isInsideCorner(x, y, llWidth / 2d, llHeight / 2d); 247 } 248 if (y1class == Zone.CLOSE_INSIDE) { 249 // Potentially in the upper-right corner 250 x = x - x1 + urWidth / 2d; 251 y = y + h - y0 - urHeight / 2d; 252 return x < 0 || y > 0 || isInsideCorner(x, y, urWidth / 2d, urHeight / 2d); 253 } 254 // Potentially in the lower-right corner 255 x = x - x1 + lrWidth / 2d; 256 y = y - y1 + lrHeight / 2d; 257 return x < 0 || y < 0 || isInsideCorner(x, y, lrWidth / 2d, lrHeight / 2d); 258 } 259 260 @Override contains(double x, double y, double w, double h)261 public boolean contains(double x, double y, double w, double h) { 262 if (isEmpty() || w <= 0 || h <= 0) { 263 return false; 264 } 265 return (contains(x, y) && contains(x + w, y) && contains(x, y + h) && contains(x + w, y + h)); 266 } 267 268 @Override getPathIterator(final AffineTransform at)269 public PathIterator getPathIterator(final AffineTransform at) { 270 return new PathIterator() { 271 int index; 272 273 // ArcIterator.btan(Math.PI/2) 274 public static final double CtrlVal = 0.5522847498307933; 275 private final double ncv = 1.0 - CtrlVal; 276 277 // Coordinates of control points for Bezier curves approximating the straight lines 278 // and corners of the rounded rectangle. 279 private final double[][] ctrlpts = { 280 {0.0, 0.0, 0.0, ulHeight}, 281 {0.0, 0.0, 1.0, -llHeight}, 282 {0.0, 0.0, 1.0, -llHeight * ncv, 0.0, ncv * llWidth, 1.0, 0.0, 0.0, llWidth, 1.0, 0.0}, 283 {1.0, -lrWidth, 1.0, 0.0}, 284 {1.0, -lrWidth * ncv, 1.0, 0.0, 1.0, 0.0, 1.0, -lrHeight * ncv, 1.0, 0.0, 1.0, -lrHeight}, 285 {1.0, 0.0, 0.0, urHeight}, 286 {1.0, 0.0, 0.0, ncv * urHeight, 1.0, -urWidth * ncv, 0.0, 0.0, 1.0, -urWidth, 0.0, 0.0}, 287 {0.0, ulWidth, 0.0, 0.0}, 288 {0.0, ncv * ulWidth, 0.0, 0.0, 0.0, 0.0, 0.0, ncv * ulHeight, 0.0, 0.0, 0.0, ulHeight}, 289 {} 290 }; 291 private final int[] types = { 292 SEG_MOVETO, 293 SEG_LINETO, 294 SEG_CUBICTO, 295 SEG_LINETO, 296 SEG_CUBICTO, 297 SEG_LINETO, 298 SEG_CUBICTO, 299 SEG_LINETO, 300 SEG_CUBICTO, 301 SEG_CLOSE, 302 }; 303 304 @Override 305 public int getWindingRule() { 306 return WIND_NON_ZERO; 307 } 308 309 @Override 310 public boolean isDone() { 311 return index >= ctrlpts.length; 312 } 313 314 @Override 315 public void next() { 316 index++; 317 } 318 319 @Override 320 public int currentSegment(float[] coords) { 321 if (isDone()) { 322 throw new NoSuchElementException("roundrect iterator out of bounds"); 323 } 324 int nc = 0; 325 double ctrls[] = ctrlpts[index]; 326 for (int i = 0; i < ctrls.length; i += 4) { 327 coords[nc++] = (float) (x + ctrls[i] * width + ctrls[i + 1] / 2d); 328 coords[nc++] = (float) (y + ctrls[i + 2] * height + ctrls[i + 3] / 2d); 329 } 330 if (at != null) { 331 at.transform(coords, 0, coords, 0, nc / 2); 332 } 333 return types[index]; 334 } 335 336 @Override 337 public int currentSegment(double[] coords) { 338 if (isDone()) { 339 throw new NoSuchElementException("roundrect iterator out of bounds"); 340 } 341 int nc = 0; 342 double ctrls[] = ctrlpts[index]; 343 for (int i = 0; i < ctrls.length; i += 4) { 344 coords[nc++] = x + ctrls[i] * width + ctrls[i + 1] / 2d; 345 coords[nc++] = y + ctrls[i + 2] * height + ctrls[i + 3] / 2d; 346 } 347 if (at != null) { 348 at.transform(coords, 0, coords, 0, nc / 2); 349 } 350 return types[index]; 351 } 352 }; 353 } 354 } 355