diff options
| author | 2016-02-04 19:53:25 -0600 | |
|---|---|---|
| committer | 2016-02-11 12:26:50 +0000 | |
| commit | 3381cde9f293c52f195b31b0e4049649db31181a (patch) | |
| tree | e446b038290c2b1d2b1ac83df21b90c71894df5e | |
| parent | ebdcc80ac26ae51ba27d9469a501a6242256aa50 (diff) | |
Layoutlib supports rounded corners of different sizes
Bug: http://b.android.com/29098
Change-Id: I4e7dc3810559b509baf5ea306221c1d2504be0e1
| -rw-r--r-- | tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java | 25 | ||||
| -rw-r--r-- | tools/layoutlib/bridge/src/android/graphics/RoundRectangle.java | 370 |
2 files changed, 381 insertions, 14 deletions
diff --git a/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java index d0dd22f8faad..a10ac00fc356 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java @@ -388,21 +388,18 @@ public final class Path_Delegate { @LayoutlibDelegate /*package*/ static void native_addRoundRect(long nPath, float left, float top, float right, float bottom, float[] radii, int dir) { - // Java2D doesn't support different rounded corners in each corner, so just use the - // first value. - native_addRoundRect(nPath, left, top, right, bottom, radii[0], radii[1], dir); - - // there can be a case where this API is used but with similar values for all corners, so - // in that case we don't warn. - // we only care if 2 corners are different so just compare to the next one. - for (int i = 0 ; i < 3 ; i++) { - if (radii[i * 2] != radii[(i + 1) * 2] || radii[i * 2 + 1] != radii[(i + 1) * 2 + 1]) { - Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, - "Different corner sizes are not supported in Path.addRoundRect.", - null, null /*data*/); - break; - } + + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + float[] cornerDimensions = new float[radii.length]; + for (int i = 0; i < radii.length; i++) { + cornerDimensions[i] = 2 * radii[i]; } + pathDelegate.mPath.append(new RoundRectangle(left, top, right - left, bottom - top, + cornerDimensions), false); } @LayoutlibDelegate diff --git a/tools/layoutlib/bridge/src/android/graphics/RoundRectangle.java b/tools/layoutlib/bridge/src/android/graphics/RoundRectangle.java new file mode 100644 index 000000000000..edd36e54aa77 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/RoundRectangle.java @@ -0,0 +1,370 @@ +/* + * 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) { + if (cornerDimensions.length != 8) { + throw new IllegalArgumentException("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]; + } + }; + } +} |