|  | /* | 
|  | * Copyright (C) 2016 The Android Open Source Project | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | * you may not use this file except in compliance with the License. | 
|  | * You may obtain a copy of the License at | 
|  | * | 
|  | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | * See the License for the specific language governing permissions and | 
|  | * limitations under the License. | 
|  | */ | 
|  |  | 
|  | package android.graphics; | 
|  |  | 
|  | import java.awt.geom.AffineTransform; | 
|  | import java.awt.geom.PathIterator; | 
|  | import java.awt.geom.Rectangle2D; | 
|  | import java.awt.geom.RectangularShape; | 
|  | import java.awt.geom.RoundRectangle2D; | 
|  | import java.util.EnumSet; | 
|  | import java.util.NoSuchElementException; | 
|  |  | 
|  | /** | 
|  | * Defines a rectangle with rounded corners, where the sizes of the corners | 
|  | * are potentially different. | 
|  | */ | 
|  | public class RoundRectangle extends RectangularShape { | 
|  | public double x; | 
|  | public double y; | 
|  | public double width; | 
|  | public double height; | 
|  | public double ulWidth; | 
|  | public double ulHeight; | 
|  | public double urWidth; | 
|  | public double urHeight; | 
|  | public double lrWidth; | 
|  | public double lrHeight; | 
|  | public double llWidth; | 
|  | public double llHeight; | 
|  |  | 
|  | private enum Zone { | 
|  | CLOSE_OUTSIDE, | 
|  | CLOSE_INSIDE, | 
|  | MIDDLE, | 
|  | FAR_INSIDE, | 
|  | FAR_OUTSIDE | 
|  | } | 
|  |  | 
|  | private final EnumSet<Zone> close = EnumSet.of(Zone.CLOSE_OUTSIDE, Zone.CLOSE_INSIDE); | 
|  | private final EnumSet<Zone> far = EnumSet.of(Zone.FAR_OUTSIDE, Zone.FAR_INSIDE); | 
|  |  | 
|  | /** | 
|  | * @param cornerDimensions array of 8 floating-point number corresponding to the width and | 
|  | * the height of each corner in the following order: upper-left, upper-right, lower-right, | 
|  | * lower-left. It assumes for the size the same convention as {@link RoundRectangle2D}, that | 
|  | * is that the width and height of a corner correspond to the total width and height of the | 
|  | * ellipse that corner is a quarter of. | 
|  | */ | 
|  | public RoundRectangle(float x, float y, float width, float height, float[] cornerDimensions) { | 
|  | assert cornerDimensions.length == 8 : "The array of corner dimensions must have eight " + | 
|  | "elements"; | 
|  |  | 
|  | this.x = x; | 
|  | this.y = y; | 
|  | this.width = width; | 
|  | this.height = height; | 
|  |  | 
|  | float[] dimensions = cornerDimensions.clone(); | 
|  | // If a value is negative, the corresponding corner is squared | 
|  | for (int i = 0; i < dimensions.length; i += 2) { | 
|  | if (dimensions[i] < 0 || dimensions[i + 1] < 0) { | 
|  | dimensions[i] = 0; | 
|  | dimensions[i + 1] = 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | double topCornerWidth = (dimensions[0] + dimensions[2]) / 2d; | 
|  | double bottomCornerWidth = (dimensions[4] + dimensions[6]) / 2d; | 
|  | double leftCornerHeight = (dimensions[1] + dimensions[7]) / 2d; | 
|  | double rightCornerHeight = (dimensions[3] + dimensions[5]) / 2d; | 
|  |  | 
|  | // Rescale the corner dimensions if they are bigger than the rectangle | 
|  | double scale = Math.min(1.0, width / topCornerWidth); | 
|  | scale = Math.min(scale, width / bottomCornerWidth); | 
|  | scale = Math.min(scale, height / leftCornerHeight); | 
|  | scale = Math.min(scale, height / rightCornerHeight); | 
|  |  | 
|  | this.ulWidth = dimensions[0] * scale; | 
|  | this.ulHeight = dimensions[1] * scale; | 
|  | this.urWidth = dimensions[2] * scale; | 
|  | this.urHeight = dimensions[3] * scale; | 
|  | this.lrWidth = dimensions[4] * scale; | 
|  | this.lrHeight = dimensions[5] * scale; | 
|  | this.llWidth = dimensions[6] * scale; | 
|  | this.llHeight = dimensions[7] * scale; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public double getX() { | 
|  | return x; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public double getY() { | 
|  | return y; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public double getWidth() { | 
|  | return width; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public double getHeight() { | 
|  | return height; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean isEmpty() { | 
|  | return (width <= 0d) || (height <= 0d); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setFrame(double x, double y, double w, double h) { | 
|  | this.x = x; | 
|  | this.y = y; | 
|  | this.width = w; | 
|  | this.height = h; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public Rectangle2D getBounds2D() { | 
|  | return new Rectangle2D.Double(x, y, width, height); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean contains(double x, double y) { | 
|  | if (isEmpty()) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | double x0 = getX(); | 
|  | double y0 = getY(); | 
|  | double x1 = x0 + getWidth(); | 
|  | double y1 = y0 + getHeight(); | 
|  | // Check for trivial rejection - point is outside bounding rectangle | 
|  | if (x < x0 || y < y0 || x >= x1 || y >= y1) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | double insideTopX0 = x0 + ulWidth / 2d; | 
|  | double insideLeftY0 = y0 + ulHeight / 2d; | 
|  | if (x < insideTopX0 && y < insideLeftY0) { | 
|  | // In the upper-left corner | 
|  | return isInsideCorner(x - insideTopX0, y - insideLeftY0, ulWidth / 2d, ulHeight / 2d); | 
|  | } | 
|  |  | 
|  | double insideTopX1 = x1 - urWidth / 2d; | 
|  | double insideRightY0 = y0 + urHeight / 2d; | 
|  | if (x > insideTopX1 && y < insideRightY0) { | 
|  | // In the upper-right corner | 
|  | return isInsideCorner(x - insideTopX1, y - insideRightY0, urWidth / 2d, urHeight / 2d); | 
|  | } | 
|  |  | 
|  | double insideBottomX1 = x1 - lrWidth / 2d; | 
|  | double insideRightY1 = y1 - lrHeight / 2d; | 
|  | if (x > insideBottomX1 && y > insideRightY1) { | 
|  | // In the lower-right corner | 
|  | return isInsideCorner(x - insideBottomX1, y - insideRightY1, lrWidth / 2d, | 
|  | lrHeight / 2d); | 
|  | } | 
|  |  | 
|  | double insideBottomX0 = x0 + llWidth / 2d; | 
|  | double insideLeftY1 = y1 - llHeight / 2d; | 
|  | if (x < insideBottomX0 && y > insideLeftY1) { | 
|  | // In the lower-left corner | 
|  | return isInsideCorner(x - insideBottomX0, y - insideLeftY1, llWidth / 2d, | 
|  | llHeight / 2d); | 
|  | } | 
|  |  | 
|  | // In the central part of the rectangle | 
|  | return true; | 
|  | } | 
|  |  | 
|  | private boolean isInsideCorner(double x, double y, double width, double height) { | 
|  | double squareDist = height * height * x * x + width * width * y * y; | 
|  | return squareDist <= width * width * height * height; | 
|  | } | 
|  |  | 
|  | private Zone classify(double coord, double side1, double arcSize1, double side2, | 
|  | double arcSize2) { | 
|  | if (coord < side1) { | 
|  | return Zone.CLOSE_OUTSIDE; | 
|  | } else if (coord < side1 + arcSize1) { | 
|  | return Zone.CLOSE_INSIDE; | 
|  | } else if (coord < side2 - arcSize2) { | 
|  | return Zone.MIDDLE; | 
|  | } else if (coord < side2) { | 
|  | return Zone.FAR_INSIDE; | 
|  | } else { | 
|  | return Zone.FAR_OUTSIDE; | 
|  | } | 
|  | } | 
|  |  | 
|  | public boolean intersects(double x, double y, double w, double h) { | 
|  | if (isEmpty() || w <= 0 || h <= 0) { | 
|  | return false; | 
|  | } | 
|  | double x0 = getX(); | 
|  | double y0 = getY(); | 
|  | double x1 = x0 + getWidth(); | 
|  | double y1 = y0 + getHeight(); | 
|  | // Check for trivial rejection - bounding rectangles do not intersect | 
|  | if (x + w <= x0 || x >= x1 || y + h <= y0 || y >= y1) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | double maxLeftCornerWidth = Math.max(ulWidth, llWidth) / 2d; | 
|  | double maxRightCornerWidth = Math.max(urWidth, lrWidth) / 2d; | 
|  | double maxUpperCornerHeight = Math.max(ulHeight, urHeight) / 2d; | 
|  | double maxLowerCornerHeight = Math.max(llHeight, lrHeight) / 2d; | 
|  | Zone x0class = classify(x, x0, maxLeftCornerWidth, x1, maxRightCornerWidth); | 
|  | Zone x1class = classify(x + w, x0, maxLeftCornerWidth, x1, maxRightCornerWidth); | 
|  | Zone y0class = classify(y, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight); | 
|  | Zone y1class = classify(y + h, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight); | 
|  |  | 
|  | // Trivially accept if any point is inside inner rectangle | 
|  | if (x0class == Zone.MIDDLE || x1class == Zone.MIDDLE || y0class == Zone.MIDDLE || y1class == Zone.MIDDLE) { | 
|  | return true; | 
|  | } | 
|  | // Trivially accept if either edge spans inner rectangle | 
|  | if ((close.contains(x0class) && far.contains(x1class)) || (close.contains(y0class) && | 
|  | far.contains(y1class))) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Since neither edge spans the center, then one of the corners | 
|  | // must be in one of the rounded edges.  We detect this case if | 
|  | // a [xy]0class is 3 or a [xy]1class is 1.  One of those two cases | 
|  | // must be true for each direction. | 
|  | // We now find a "nearest point" to test for being inside a rounded | 
|  | // corner. | 
|  | if (x1class == Zone.CLOSE_INSIDE && y1class == Zone.CLOSE_INSIDE) { | 
|  | // Potentially in upper-left corner | 
|  | x = x + w - x0 - ulWidth / 2d; | 
|  | y = y + h - y0 - ulHeight / 2d; | 
|  | return x > 0 || y > 0 || isInsideCorner(x, y, ulWidth / 2d, ulHeight / 2d); | 
|  | } | 
|  | if (x1class == Zone.CLOSE_INSIDE) { | 
|  | // Potentially in lower-left corner | 
|  | x = x + w - x0 - llWidth / 2d; | 
|  | y = y - y1 + llHeight / 2d; | 
|  | return x > 0 || y < 0 || isInsideCorner(x, y, llWidth / 2d, llHeight / 2d); | 
|  | } | 
|  | if (y1class == Zone.CLOSE_INSIDE) { | 
|  | //Potentially in the upper-right corner | 
|  | x = x - x1 + urWidth / 2d; | 
|  | y = y + h - y0 - urHeight / 2d; | 
|  | return x < 0 || y > 0 || isInsideCorner(x, y, urWidth / 2d, urHeight / 2d); | 
|  | } | 
|  | // Potentially in the lower-right corner | 
|  | x = x - x1 + lrWidth / 2d; | 
|  | y = y - y1 + lrHeight / 2d; | 
|  | return x < 0 || y < 0 || isInsideCorner(x, y, lrWidth / 2d, lrHeight / 2d); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean contains(double x, double y, double w, double h) { | 
|  | if (isEmpty() || w <= 0 || h <= 0) { | 
|  | return false; | 
|  | } | 
|  | return (contains(x, y) && | 
|  | contains(x + w, y) && | 
|  | contains(x, y + h) && | 
|  | contains(x + w, y + h)); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public PathIterator getPathIterator(final AffineTransform at) { | 
|  | return new PathIterator() { | 
|  | int index; | 
|  |  | 
|  | // ArcIterator.btan(Math.PI/2) | 
|  | public static final double CtrlVal = 0.5522847498307933; | 
|  | private final double ncv = 1.0 - CtrlVal; | 
|  |  | 
|  | // Coordinates of control points for Bezier curves approximating the straight lines | 
|  | // and corners of the rounded rectangle. | 
|  | private final double[][] ctrlpts = { | 
|  | {0.0, 0.0, 0.0, ulHeight}, | 
|  | {0.0, 0.0, 1.0, -llHeight}, | 
|  | {0.0, 0.0, 1.0, -llHeight * ncv, 0.0, ncv * llWidth, 1.0, 0.0, 0.0, llWidth, | 
|  | 1.0, 0.0}, | 
|  | {1.0, -lrWidth, 1.0, 0.0}, | 
|  | {1.0, -lrWidth * ncv, 1.0, 0.0, 1.0, 0.0, 1.0, -lrHeight * ncv, 1.0, 0.0, 1.0, | 
|  | -lrHeight}, | 
|  | {1.0, 0.0, 0.0, urHeight}, | 
|  | {1.0, 0.0, 0.0, ncv * urHeight, 1.0, -urWidth * ncv, 0.0, 0.0, 1.0, -urWidth, | 
|  | 0.0, 0.0}, | 
|  | {0.0, ulWidth, 0.0, 0.0}, | 
|  | {0.0, ncv * ulWidth, 0.0, 0.0, 0.0, 0.0, 0.0, ncv * ulHeight, 0.0, 0.0, 0.0, | 
|  | ulHeight}, | 
|  | {} | 
|  | }; | 
|  | private final int[] types = { | 
|  | SEG_MOVETO, | 
|  | SEG_LINETO, SEG_CUBICTO, | 
|  | SEG_LINETO, SEG_CUBICTO, | 
|  | SEG_LINETO, SEG_CUBICTO, | 
|  | SEG_LINETO, SEG_CUBICTO, | 
|  | SEG_CLOSE, | 
|  | }; | 
|  |  | 
|  | @Override | 
|  | public int getWindingRule() { | 
|  | return WIND_NON_ZERO; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean isDone() { | 
|  | return index >= ctrlpts.length; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void next() { | 
|  | index++; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int currentSegment(float[] coords) { | 
|  | if (isDone()) { | 
|  | throw new NoSuchElementException("roundrect iterator out of bounds"); | 
|  | } | 
|  | int nc = 0; | 
|  | double ctrls[] = ctrlpts[index]; | 
|  | for (int i = 0; i < ctrls.length; i += 4) { | 
|  | coords[nc++] = (float) (x + ctrls[i] * width + ctrls[i + 1] / 2d); | 
|  | coords[nc++] = (float) (y + ctrls[i + 2] * height + ctrls[i + 3] / 2d); | 
|  | } | 
|  | if (at != null) { | 
|  | at.transform(coords, 0, coords, 0, nc / 2); | 
|  | } | 
|  | return types[index]; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int currentSegment(double[] coords) { | 
|  | if (isDone()) { | 
|  | throw new NoSuchElementException("roundrect iterator out of bounds"); | 
|  | } | 
|  | int nc = 0; | 
|  | double ctrls[] = ctrlpts[index]; | 
|  | for (int i = 0; i < ctrls.length; i += 4) { | 
|  | coords[nc++] = x + ctrls[i] * width + ctrls[i + 1] / 2d; | 
|  | coords[nc++] = y + ctrls[i + 2] * height + ctrls[i + 3] / 2d; | 
|  | } | 
|  | if (at != null) { | 
|  | at.transform(coords, 0, coords, 0, nc / 2); | 
|  | } | 
|  | return types[index]; | 
|  | } | 
|  | }; | 
|  | } | 
|  | } |