/* * Copyright (C) 2010 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 com.android.ide.common.rendering.api.LayoutLog; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import android.annotation.NonNull; import android.graphics.Path.Direction; import android.graphics.Path.FillType; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Area; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.util.ArrayList; import libcore.util.NativeAllocationRegistry_Delegate; /** * Delegate implementing the native methods of android.graphics.Path * * Through the layoutlib_create tool, the original native methods of Path have been replaced * by calls to methods of the same name in this delegate class. * * This class behaves like the original native implementation, but in Java, keeping previously * native data into its own objects and mapping them to int that are sent back and forth between * it and the original Path class. * * @see DelegateManager * */ public final class Path_Delegate { // ---- delegate manager ---- private static final DelegateManager sManager = new DelegateManager(Path_Delegate.class); private static final float EPSILON = 1e-4f; private static long sFinalizer = -1; // ---- delegate data ---- private FillType mFillType = FillType.WINDING; private Path2D mPath = new Path2D.Double(); private float mLastX = 0; private float mLastY = 0; // true if the path contains does not contain a curve or line. private boolean mCachedIsEmpty = true; // ---- Public Helper methods ---- public static Path_Delegate getDelegate(long nPath) { return sManager.getDelegate(nPath); } public Path2D getJavaShape() { return mPath; } public void setJavaShape(Shape shape) { reset(); mPath.append(shape, false /*connect*/); } public void reset() { mPath.reset(); mLastX = 0; mLastY = 0; } public void setPathIterator(PathIterator iterator) { reset(); mPath.append(iterator, false /*connect*/); } // ---- native methods ---- @LayoutlibDelegate /*package*/ static long nInit() { // create the delegate Path_Delegate newDelegate = new Path_Delegate(); return sManager.addNewDelegate(newDelegate); } @LayoutlibDelegate /*package*/ static long nInit(long nPath) { // create the delegate Path_Delegate newDelegate = new Path_Delegate(); // get the delegate to copy, which could be null if nPath is 0 Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate != null) { newDelegate.set(pathDelegate); } return sManager.addNewDelegate(newDelegate); } @LayoutlibDelegate /*package*/ static void nReset(long nPath) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.reset(); } @LayoutlibDelegate /*package*/ static void nRewind(long nPath) { // call out to reset since there's nothing to optimize in // terms of data structs. nReset(nPath); } @LayoutlibDelegate /*package*/ static void nSet(long native_dst, long nSrc) { Path_Delegate pathDstDelegate = sManager.getDelegate(native_dst); if (pathDstDelegate == null) { return; } Path_Delegate pathSrcDelegate = sManager.getDelegate(nSrc); if (pathSrcDelegate == null) { return; } pathDstDelegate.set(pathSrcDelegate); } @LayoutlibDelegate /*package*/ static boolean nIsConvex(long nPath) { Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, "Path.isConvex is not supported.", null, null); return true; } @LayoutlibDelegate /*package*/ static int nGetFillType(long nPath) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return 0; } return pathDelegate.mFillType.nativeInt; } @LayoutlibDelegate public static void nSetFillType(long nPath, int ft) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.setFillType(Path.sFillTypeArray[ft]); } @LayoutlibDelegate /*package*/ static boolean nIsEmpty(long nPath) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); return pathDelegate == null || pathDelegate.isEmpty(); } @LayoutlibDelegate /*package*/ static boolean nIsRect(long nPath, RectF rect) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return false; } // create an Area that can test if the path is a rect Area area = new Area(pathDelegate.mPath); if (area.isRectangular()) { if (rect != null) { pathDelegate.fillBounds(rect); } return true; } return false; } @LayoutlibDelegate /*package*/ static void nComputeBounds(long nPath, RectF bounds) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.fillBounds(bounds); } @LayoutlibDelegate /*package*/ static void nIncReserve(long nPath, int extraPtCount) { // since we use a java2D path, there's no way to pre-allocate new points, // so we do nothing. } @LayoutlibDelegate /*package*/ static void nMoveTo(long nPath, float x, float y) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.moveTo(x, y); } @LayoutlibDelegate /*package*/ static void nRMoveTo(long nPath, float dx, float dy) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.rMoveTo(dx, dy); } @LayoutlibDelegate /*package*/ static void nLineTo(long nPath, float x, float y) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.lineTo(x, y); } @LayoutlibDelegate /*package*/ static void nRLineTo(long nPath, float dx, float dy) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.rLineTo(dx, dy); } @LayoutlibDelegate /*package*/ static void nQuadTo(long nPath, float x1, float y1, float x2, float y2) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.quadTo(x1, y1, x2, y2); } @LayoutlibDelegate /*package*/ static void nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.rQuadTo(dx1, dy1, dx2, dy2); } @LayoutlibDelegate /*package*/ static void nCubicTo(long nPath, float x1, float y1, float x2, float y2, float x3, float y3) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.cubicTo(x1, y1, x2, y2, x3, y3); } @LayoutlibDelegate /*package*/ static void nRCubicTo(long nPath, float x1, float y1, float x2, float y2, float x3, float y3) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.rCubicTo(x1, y1, x2, y2, x3, y3); } @LayoutlibDelegate /*package*/ static void nArcTo(long nPath, float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.arcTo(left, top, right, bottom, startAngle, sweepAngle, forceMoveTo); } @LayoutlibDelegate /*package*/ static void nClose(long nPath) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.close(); } @LayoutlibDelegate /*package*/ static void nAddRect(long nPath, float left, float top, float right, float bottom, int dir) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.addRect(left, top, right, bottom, dir); } @LayoutlibDelegate /*package*/ static void nAddOval(long nPath, float left, float top, float right, float bottom, int dir) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.mPath.append(new Ellipse2D.Float( left, top, right - left, bottom - top), false); } @LayoutlibDelegate /*package*/ static void nAddCircle(long nPath, float x, float y, float radius, int dir) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } // because x/y is the center of the circle, need to offset this by the radius pathDelegate.mPath.append(new Ellipse2D.Float( x - radius, y - radius, radius * 2, radius * 2), false); } @LayoutlibDelegate /*package*/ static void nAddArc(long nPath, float left, float top, float right, float bottom, float startAngle, float sweepAngle) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } // because x/y is the center of the circle, need to offset this by the radius pathDelegate.mPath.append(new Arc2D.Float( left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN), false); } @LayoutlibDelegate /*package*/ static void nAddRoundRect(long nPath, float left, float top, float right, float bottom, float rx, float ry, int dir) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.mPath.append(new RoundRectangle2D.Float( left, top, right - left, bottom - top, rx * 2, ry * 2), false); } @LayoutlibDelegate /*package*/ static void nAddRoundRect(long nPath, float left, float top, float right, float bottom, float[] radii, int dir) { 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 /*package*/ static void nAddPath(long nPath, long src, float dx, float dy) { addPath(nPath, src, AffineTransform.getTranslateInstance(dx, dy)); } @LayoutlibDelegate /*package*/ static void nAddPath(long nPath, long src) { addPath(nPath, src, null /*transform*/); } @LayoutlibDelegate /*package*/ static void nAddPath(long nPath, long src, long matrix) { Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix); if (matrixDelegate == null) { return; } addPath(nPath, src, matrixDelegate.getAffineTransform()); } @LayoutlibDelegate /*package*/ static void nOffset(long nPath, float dx, float dy) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.offset(dx, dy); } @LayoutlibDelegate /*package*/ static void nSetLastPoint(long nPath, float dx, float dy) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } pathDelegate.mLastX = dx; pathDelegate.mLastY = dy; } @LayoutlibDelegate /*package*/ static void nTransform(long nPath, long matrix, long dst_path) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return; } Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix); if (matrixDelegate == null) { return; } // this can be null if dst_path is 0 Path_Delegate dstDelegate = sManager.getDelegate(dst_path); pathDelegate.transform(matrixDelegate, dstDelegate); } @LayoutlibDelegate /*package*/ static void nTransform(long nPath, long matrix) { nTransform(nPath, matrix, 0); } @LayoutlibDelegate /*package*/ static boolean nOp(long nPath1, long nPath2, int op, long result) { Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, "Path.op() not supported", null); return false; } @LayoutlibDelegate /*package*/ static long nGetFinalizer() { synchronized (Path_Delegate.class) { if (sFinalizer == -1) { sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer( sManager::removeJavaReferenceFor); } } return sFinalizer; } @LayoutlibDelegate /*package*/ static float[] nApproximate(long nPath, float error) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return null; } // Get a FlatteningIterator PathIterator iterator = pathDelegate.getJavaShape().getPathIterator(null, error); float segment[] = new float[6]; float totalLength = 0; ArrayList points = new ArrayList(); Point2D.Float previousPoint = null; while (!iterator.isDone()) { int type = iterator.currentSegment(segment); Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]); // MoveTo shouldn't affect the length if (previousPoint != null && type != PathIterator.SEG_MOVETO) { totalLength += currentPoint.distance(previousPoint); } previousPoint = currentPoint; points.add(currentPoint); iterator.next(); } int nPoints = points.size(); float[] result = new float[nPoints * 3]; previousPoint = null; // Distance that we've covered so far. Used to calculate the fraction of the path that // we've covered up to this point. float walkedDistance = .0f; for (int i = 0; i < nPoints; i++) { Point2D.Float point = points.get(i); float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f; walkedDistance += distance; result[i * 3] = walkedDistance / totalLength; result[i * 3 + 1] = point.x; result[i * 3 + 2] = point.y; previousPoint = point; } return result; } // ---- Private helper methods ---- private void set(Path_Delegate delegate) { mPath.reset(); setFillType(delegate.mFillType); mPath.append(delegate.mPath, false /*connect*/); } private void setFillType(FillType fillType) { mFillType = fillType; mPath.setWindingRule(getWindingRule(fillType)); } /** * Returns the Java2D winding rules matching a given Android {@link FillType}. * @param type the android fill type * @return the matching java2d winding rule. */ private static int getWindingRule(FillType type) { switch (type) { case WINDING: case INVERSE_WINDING: return GeneralPath.WIND_NON_ZERO; case EVEN_ODD: case INVERSE_EVEN_ODD: return GeneralPath.WIND_EVEN_ODD; default: assert false; return GeneralPath.WIND_NON_ZERO; } } @NonNull private static Direction getDirection(int direction) { for (Direction d : Direction.values()) { if (direction == d.nativeInt) { return d; } } assert false; return null; } public static void addPath(long destPath, long srcPath, AffineTransform transform) { Path_Delegate destPathDelegate = sManager.getDelegate(destPath); if (destPathDelegate == null) { return; } Path_Delegate srcPathDelegate = sManager.getDelegate(srcPath); if (srcPathDelegate == null) { return; } if (transform != null) { destPathDelegate.mPath.append( srcPathDelegate.mPath.getPathIterator(transform), false); } else { destPathDelegate.mPath.append(srcPathDelegate.mPath, false); } } /** * Returns whether the path already contains any points. * Note that this is different to * {@link #isEmpty} because if all elements are {@link PathIterator#SEG_MOVETO}, * {@link #isEmpty} will return true while hasPoints will return false. */ public boolean hasPoints() { return !mPath.getPathIterator(null).isDone(); } /** * Returns whether the path is empty (contains no lines or curves). * @see Path#isEmpty */ public boolean isEmpty() { if (!mCachedIsEmpty) { return false; } float[] coords = new float[6]; mCachedIsEmpty = Boolean.TRUE; for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) { int type = it.currentSegment(coords); if (type != PathIterator.SEG_MOVETO) { // Once we know that the path is not empty, we do not need to check again unless // Path#reset is called. mCachedIsEmpty = false; return false; } } return true; } /** * Fills the given {@link RectF} with the path bounds. * @param bounds the RectF to be filled. */ public void fillBounds(RectF bounds) { Rectangle2D rect = mPath.getBounds2D(); bounds.left = (float)rect.getMinX(); bounds.right = (float)rect.getMaxX(); bounds.top = (float)rect.getMinY(); bounds.bottom = (float)rect.getMaxY(); } /** * Set the beginning of the next contour to the point (x,y). * * @param x The x-coordinate of the start of a new contour * @param y The y-coordinate of the start of a new contour */ public void moveTo(float x, float y) { mPath.moveTo(mLastX = x, mLastY = y); } /** * Set the beginning of the next contour relative to the last point on the * previous contour. If there is no previous contour, this is treated the * same as moveTo(). * * @param dx The amount to add to the x-coordinate of the end of the * previous contour, to specify the start of a new contour * @param dy The amount to add to the y-coordinate of the end of the * previous contour, to specify the start of a new contour */ public void rMoveTo(float dx, float dy) { dx += mLastX; dy += mLastY; mPath.moveTo(mLastX = dx, mLastY = dy); } /** * Add a line from the last point to the specified point (x,y). * If no moveTo() call has been made for this contour, the first point is * automatically set to (0,0). * * @param x The x-coordinate of the end of a line * @param y The y-coordinate of the end of a line */ public void lineTo(float x, float y) { if (!hasPoints()) { mPath.moveTo(mLastX = 0, mLastY = 0); } mPath.lineTo(mLastX = x, mLastY = y); } /** * Same as lineTo, but the coordinates are considered relative to the last * point on this contour. If there is no previous point, then a moveTo(0,0) * is inserted automatically. * * @param dx The amount to add to the x-coordinate of the previous point on * this contour, to specify a line * @param dy The amount to add to the y-coordinate of the previous point on * this contour, to specify a line */ public void rLineTo(float dx, float dy) { if (!hasPoints()) { mPath.moveTo(mLastX = 0, mLastY = 0); } if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) { // The delta is so small that this shouldn't generate a line return; } dx += mLastX; dy += mLastY; mPath.lineTo(mLastX = dx, mLastY = dy); } /** * Add a quadratic bezier from the last point, approaching control point * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for * this contour, the first point is automatically set to (0,0). * * @param x1 The x-coordinate of the control point on a quadratic curve * @param y1 The y-coordinate of the control point on a quadratic curve * @param x2 The x-coordinate of the end point on a quadratic curve * @param y2 The y-coordinate of the end point on a quadratic curve */ public void quadTo(float x1, float y1, float x2, float y2) { mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2); } /** * Same as quadTo, but the coordinates are considered relative to the last * point on this contour. If there is no previous point, then a moveTo(0,0) * is inserted automatically. * * @param dx1 The amount to add to the x-coordinate of the last point on * this contour, for the control point of a quadratic curve * @param dy1 The amount to add to the y-coordinate of the last point on * this contour, for the control point of a quadratic curve * @param dx2 The amount to add to the x-coordinate of the last point on * this contour, for the end point of a quadratic curve * @param dy2 The amount to add to the y-coordinate of the last point on * this contour, for the end point of a quadratic curve */ public void rQuadTo(float dx1, float dy1, float dx2, float dy2) { if (!hasPoints()) { mPath.moveTo(mLastX = 0, mLastY = 0); } dx1 += mLastX; dy1 += mLastY; dx2 += mLastX; dy2 += mLastY; mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2); } /** * Add a cubic bezier from the last point, approaching control points * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been * made for this contour, the first point is automatically set to (0,0). * * @param x1 The x-coordinate of the 1st control point on a cubic curve * @param y1 The y-coordinate of the 1st control point on a cubic curve * @param x2 The x-coordinate of the 2nd control point on a cubic curve * @param y2 The y-coordinate of the 2nd control point on a cubic curve * @param x3 The x-coordinate of the end point on a cubic curve * @param y3 The y-coordinate of the end point on a cubic curve */ public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { if (!hasPoints()) { mPath.moveTo(0, 0); } mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3); } /** * Same as cubicTo, but the coordinates are considered relative to the * current point on this contour. If there is no previous point, then a * moveTo(0,0) is inserted automatically. */ public void rCubicTo(float dx1, float dy1, float dx2, float dy2, float dx3, float dy3) { if (!hasPoints()) { mPath.moveTo(mLastX = 0, mLastY = 0); } dx1 += mLastX; dy1 += mLastY; dx2 += mLastX; dy2 += mLastY; dx3 += mLastX; dy3 += mLastY; mPath.curveTo(dx1, dy1, dx2, dy2, mLastX = dx3, mLastY = dy3); } /** * Append the specified arc to the path as a new contour. If the start of * the path is different from the path's current last point, then an * automatic lineTo() is added to connect the current contour to the * start of the arc. However, if the path is empty, then we call moveTo() * with the first point of the arc. The sweep angle is tread mod 360. * * @param left The left of oval defining shape and size of the arc * @param top The top of oval defining shape and size of the arc * @param right The right of oval defining shape and size of the arc * @param bottom The bottom of oval defining shape and size of the arc * @param startAngle Starting angle (in degrees) where the arc begins * @param sweepAngle Sweep angle (in degrees) measured clockwise, treated * mod 360. * @param forceMoveTo If true, always begin a new contour with the arc */ public void arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) { Arc2D arc = new Arc2D.Float(left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN); mPath.append(arc, true /*connect*/); resetLastPointFromPath(); } /** * Close the current contour. If the current point is not equal to the * first point of the contour, a line segment is automatically added. */ public void close() { mPath.closePath(); } private void resetLastPointFromPath() { Point2D last = mPath.getCurrentPoint(); mLastX = (float) last.getX(); mLastY = (float) last.getY(); } /** * Add a closed rectangle contour to the path * * @param left The left side of a rectangle to add to the path * @param top The top of a rectangle to add to the path * @param right The right side of a rectangle to add to the path * @param bottom The bottom of a rectangle to add to the path * @param dir The direction to wind the rectangle's contour */ public void addRect(float left, float top, float right, float bottom, int dir) { moveTo(left, top); Direction direction = getDirection(dir); switch (direction) { case CW: lineTo(right, top); lineTo(right, bottom); lineTo(left, bottom); break; case CCW: lineTo(left, bottom); lineTo(right, bottom); lineTo(right, top); break; } close(); resetLastPointFromPath(); } /** * Offset the path by (dx,dy), returning true on success * * @param dx The amount in the X direction to offset the entire path * @param dy The amount in the Y direction to offset the entire path */ public void offset(float dx, float dy) { GeneralPath newPath = new GeneralPath(); PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy)); newPath.append(iterator, false /*connect*/); mPath = newPath; } /** * Transform the points in this path by matrix, and write the answer * into dst. If dst is null, then the the original path is modified. * * @param matrix The matrix to apply to the path * @param dst The transformed path is written here. If dst is null, * then the the original path is modified */ public void transform(Matrix_Delegate matrix, Path_Delegate dst) { if (matrix.hasPerspective()) { assert false; Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_AFFINE, "android.graphics.Path#transform() only " + "supports affine transformations.", null, null /*data*/); } GeneralPath newPath = new GeneralPath(); PathIterator iterator = mPath.getPathIterator(matrix.getAffineTransform()); newPath.append(iterator, false /*connect*/); if (dst != null) { dst.mPath = newPath; } else { mPath = newPath; } } }