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