1 /*
2  * Copyright (C) 2010 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 com.android.ide.common.rendering.api.LayoutLog;
20 import com.android.layoutlib.bridge.Bridge;
21 import com.android.layoutlib.bridge.impl.DelegateManager;
22 import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
23 
24 import android.annotation.NonNull;
25 import android.graphics.Path.Direction;
26 import android.graphics.Path.FillType;
27 
28 import java.awt.Shape;
29 import java.awt.geom.AffineTransform;
30 import java.awt.geom.Arc2D;
31 import java.awt.geom.Area;
32 import java.awt.geom.Ellipse2D;
33 import java.awt.geom.GeneralPath;
34 import java.awt.geom.Path2D;
35 import java.awt.geom.PathIterator;
36 import java.awt.geom.Point2D;
37 import java.awt.geom.Rectangle2D;
38 import java.awt.geom.RoundRectangle2D;
39 import java.util.ArrayList;
40 
41 /**
42  * Delegate implementing the native methods of android.graphics.Path
43  *
44  * Through the layoutlib_create tool, the original native methods of Path have been replaced
45  * by calls to methods of the same name in this delegate class.
46  *
47  * This class behaves like the original native implementation, but in Java, keeping previously
48  * native data into its own objects and mapping them to int that are sent back and forth between
49  * it and the original Path class.
50  *
51  * @see DelegateManager
52  *
53  */
54 public final class Path_Delegate {
55 
56     // ---- delegate manager ----
57     private static final DelegateManager<Path_Delegate> sManager =
58             new DelegateManager<Path_Delegate>(Path_Delegate.class);
59 
60     private static final float EPSILON = 1e-4f;
61 
62     // ---- delegate data ----
63     private FillType mFillType = FillType.WINDING;
64     private Path2D mPath = new Path2D.Double();
65 
66     private float mLastX = 0;
67     private float mLastY = 0;
68 
69     // true if the path contains does not contain a curve or line.
70     private boolean mCachedIsEmpty = true;
71 
72     // ---- Public Helper methods ----
73 
getDelegate(long nPath)74     public static Path_Delegate getDelegate(long nPath) {
75         return sManager.getDelegate(nPath);
76     }
77 
getJavaShape()78     public Path2D getJavaShape() {
79         return mPath;
80     }
81 
setJavaShape(Shape shape)82     public void setJavaShape(Shape shape) {
83         reset();
84         mPath.append(shape, false /*connect*/);
85     }
86 
reset()87     public void reset() {
88         mPath.reset();
89         mLastX = 0;
90         mLastY = 0;
91     }
92 
setPathIterator(PathIterator iterator)93     public void setPathIterator(PathIterator iterator) {
94         reset();
95         mPath.append(iterator, false /*connect*/);
96     }
97 
98     // ---- native methods ----
99 
100     @LayoutlibDelegate
nInit()101     /*package*/ static long nInit() {
102         // create the delegate
103         Path_Delegate newDelegate = new Path_Delegate();
104 
105         return sManager.addNewDelegate(newDelegate);
106     }
107 
108     @LayoutlibDelegate
nInit(long nPath)109     /*package*/ static long nInit(long nPath) {
110         // create the delegate
111         Path_Delegate newDelegate = new Path_Delegate();
112 
113         // get the delegate to copy, which could be null if nPath is 0
114         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
115         if (pathDelegate != null) {
116             newDelegate.set(pathDelegate);
117         }
118 
119         return sManager.addNewDelegate(newDelegate);
120     }
121 
122     @LayoutlibDelegate
nReset(long nPath)123     /*package*/ static void nReset(long nPath) {
124         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
125         if (pathDelegate == null) {
126             return;
127         }
128 
129         pathDelegate.reset();
130     }
131 
132     @LayoutlibDelegate
nRewind(long nPath)133     /*package*/ static void nRewind(long nPath) {
134         // call out to reset since there's nothing to optimize in
135         // terms of data structs.
136         nReset(nPath);
137     }
138 
139     @LayoutlibDelegate
nSet(long native_dst, long nSrc)140     /*package*/ static void nSet(long native_dst, long nSrc) {
141         Path_Delegate pathDstDelegate = sManager.getDelegate(native_dst);
142         if (pathDstDelegate == null) {
143             return;
144         }
145 
146         Path_Delegate pathSrcDelegate = sManager.getDelegate(nSrc);
147         if (pathSrcDelegate == null) {
148             return;
149         }
150 
151         pathDstDelegate.set(pathSrcDelegate);
152     }
153 
154     @LayoutlibDelegate
nIsConvex(long nPath)155     /*package*/ static boolean nIsConvex(long nPath) {
156         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
157                 "Path.isConvex is not supported.", null, null);
158         return true;
159     }
160 
161     @LayoutlibDelegate
nGetFillType(long nPath)162     /*package*/ static int nGetFillType(long nPath) {
163         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
164         if (pathDelegate == null) {
165             return 0;
166         }
167 
168         return pathDelegate.mFillType.nativeInt;
169     }
170 
171     @LayoutlibDelegate
nSetFillType(long nPath, int ft)172     public static void nSetFillType(long nPath, int ft) {
173         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
174         if (pathDelegate == null) {
175             return;
176         }
177 
178         pathDelegate.setFillType(Path.sFillTypeArray[ft]);
179     }
180 
181     @LayoutlibDelegate
nIsEmpty(long nPath)182     /*package*/ static boolean nIsEmpty(long nPath) {
183         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
184         return pathDelegate == null || pathDelegate.isEmpty();
185 
186     }
187 
188     @LayoutlibDelegate
nIsRect(long nPath, RectF rect)189     /*package*/ static boolean nIsRect(long nPath, RectF rect) {
190         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
191         if (pathDelegate == null) {
192             return false;
193         }
194 
195         // create an Area that can test if the path is a rect
196         Area area = new Area(pathDelegate.mPath);
197         if (area.isRectangular()) {
198             if (rect != null) {
199                 pathDelegate.fillBounds(rect);
200             }
201 
202             return true;
203         }
204 
205         return false;
206     }
207 
208     @LayoutlibDelegate
nComputeBounds(long nPath, RectF bounds)209     /*package*/ static void nComputeBounds(long nPath, RectF bounds) {
210         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
211         if (pathDelegate == null) {
212             return;
213         }
214 
215         pathDelegate.fillBounds(bounds);
216     }
217 
218     @LayoutlibDelegate
nIncReserve(long nPath, int extraPtCount)219     /*package*/ static void nIncReserve(long nPath, int extraPtCount) {
220         // since we use a java2D path, there's no way to pre-allocate new points,
221         // so we do nothing.
222     }
223 
224     @LayoutlibDelegate
nMoveTo(long nPath, float x, float y)225     /*package*/ static void nMoveTo(long nPath, float x, float y) {
226         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
227         if (pathDelegate == null) {
228             return;
229         }
230 
231         pathDelegate.moveTo(x, y);
232     }
233 
234     @LayoutlibDelegate
nRMoveTo(long nPath, float dx, float dy)235     /*package*/ static void nRMoveTo(long nPath, float dx, float dy) {
236         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
237         if (pathDelegate == null) {
238             return;
239         }
240 
241         pathDelegate.rMoveTo(dx, dy);
242     }
243 
244     @LayoutlibDelegate
nLineTo(long nPath, float x, float y)245     /*package*/ static void nLineTo(long nPath, float x, float y) {
246         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
247         if (pathDelegate == null) {
248             return;
249         }
250 
251         pathDelegate.lineTo(x, y);
252     }
253 
254     @LayoutlibDelegate
nRLineTo(long nPath, float dx, float dy)255     /*package*/ static void nRLineTo(long nPath, float dx, float dy) {
256         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
257         if (pathDelegate == null) {
258             return;
259         }
260 
261         pathDelegate.rLineTo(dx, dy);
262     }
263 
264     @LayoutlibDelegate
nQuadTo(long nPath, float x1, float y1, float x2, float y2)265     /*package*/ static void nQuadTo(long nPath, float x1, float y1, float x2, float y2) {
266         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
267         if (pathDelegate == null) {
268             return;
269         }
270 
271         pathDelegate.quadTo(x1, y1, x2, y2);
272     }
273 
274     @LayoutlibDelegate
nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2)275     /*package*/ static void nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2) {
276         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
277         if (pathDelegate == null) {
278             return;
279         }
280 
281         pathDelegate.rQuadTo(dx1, dy1, dx2, dy2);
282     }
283 
284     @LayoutlibDelegate
nCubicTo(long nPath, float x1, float y1, float x2, float y2, float x3, float y3)285     /*package*/ static void nCubicTo(long nPath, float x1, float y1,
286             float x2, float y2, float x3, float y3) {
287         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
288         if (pathDelegate == null) {
289             return;
290         }
291 
292         pathDelegate.cubicTo(x1, y1, x2, y2, x3, y3);
293     }
294 
295     @LayoutlibDelegate
nRCubicTo(long nPath, float x1, float y1, float x2, float y2, float x3, float y3)296     /*package*/ static void nRCubicTo(long nPath, float x1, float y1,
297             float x2, float y2, float x3, float y3) {
298         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
299         if (pathDelegate == null) {
300             return;
301         }
302 
303         pathDelegate.rCubicTo(x1, y1, x2, y2, x3, y3);
304     }
305 
306     @LayoutlibDelegate
nArcTo(long nPath, float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)307     /*package*/ static void nArcTo(long nPath, float left, float top, float right,
308             float bottom,
309                     float startAngle, float sweepAngle, boolean forceMoveTo) {
310         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
311         if (pathDelegate == null) {
312             return;
313         }
314 
315         pathDelegate.arcTo(left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
316     }
317 
318     @LayoutlibDelegate
nClose(long nPath)319     /*package*/ static void nClose(long nPath) {
320         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
321         if (pathDelegate == null) {
322             return;
323         }
324 
325         pathDelegate.close();
326     }
327 
328     @LayoutlibDelegate
nAddRect(long nPath, float left, float top, float right, float bottom, int dir)329     /*package*/ static void nAddRect(long nPath,
330             float left, float top, float right, float bottom, int dir) {
331         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
332         if (pathDelegate == null) {
333             return;
334         }
335 
336         pathDelegate.addRect(left, top, right, bottom, dir);
337     }
338 
339     @LayoutlibDelegate
nAddOval(long nPath, float left, float top, float right, float bottom, int dir)340     /*package*/ static void nAddOval(long nPath, float left, float top, float right,
341             float bottom, int dir) {
342         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
343         if (pathDelegate == null) {
344             return;
345         }
346 
347         pathDelegate.mPath.append(new Ellipse2D.Float(
348                 left, top, right - left, bottom - top), false);
349     }
350 
351     @LayoutlibDelegate
nAddCircle(long nPath, float x, float y, float radius, int dir)352     /*package*/ static void nAddCircle(long nPath, float x, float y, float radius, int dir) {
353         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
354         if (pathDelegate == null) {
355             return;
356         }
357 
358         // because x/y is the center of the circle, need to offset this by the radius
359         pathDelegate.mPath.append(new Ellipse2D.Float(
360                 x - radius, y - radius, radius * 2, radius * 2), false);
361     }
362 
363     @LayoutlibDelegate
nAddArc(long nPath, float left, float top, float right, float bottom, float startAngle, float sweepAngle)364     /*package*/ static void nAddArc(long nPath, float left, float top, float right,
365             float bottom, float startAngle, float sweepAngle) {
366         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
367         if (pathDelegate == null) {
368             return;
369         }
370 
371         // because x/y is the center of the circle, need to offset this by the radius
372         pathDelegate.mPath.append(new Arc2D.Float(
373                 left, top, right - left, bottom - top,
374                 -startAngle, -sweepAngle, Arc2D.OPEN), false);
375     }
376 
377     @LayoutlibDelegate
nAddRoundRect(long nPath, float left, float top, float right, float bottom, float rx, float ry, int dir)378     /*package*/ static void nAddRoundRect(long nPath, float left, float top, float right,
379             float bottom, float rx, float ry, int dir) {
380 
381         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
382         if (pathDelegate == null) {
383             return;
384         }
385 
386         pathDelegate.mPath.append(new RoundRectangle2D.Float(
387                 left, top, right - left, bottom - top, rx * 2, ry * 2), false);
388     }
389 
390     @LayoutlibDelegate
nAddRoundRect(long nPath, float left, float top, float right, float bottom, float[] radii, int dir)391     /*package*/ static void nAddRoundRect(long nPath, float left, float top, float right,
392             float bottom, float[] radii, int dir) {
393 
394         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
395         if (pathDelegate == null) {
396             return;
397         }
398 
399         float[] cornerDimensions = new float[radii.length];
400         for (int i = 0; i < radii.length; i++) {
401             cornerDimensions[i] = 2 * radii[i];
402         }
403         pathDelegate.mPath.append(new RoundRectangle(left, top, right - left, bottom - top,
404                 cornerDimensions), false);
405     }
406 
407     @LayoutlibDelegate
nAddPath(long nPath, long src, float dx, float dy)408     /*package*/ static void nAddPath(long nPath, long src, float dx, float dy) {
409         addPath(nPath, src, AffineTransform.getTranslateInstance(dx, dy));
410     }
411 
412     @LayoutlibDelegate
nAddPath(long nPath, long src)413     /*package*/ static void nAddPath(long nPath, long src) {
414         addPath(nPath, src, null /*transform*/);
415     }
416 
417     @LayoutlibDelegate
nAddPath(long nPath, long src, long matrix)418     /*package*/ static void nAddPath(long nPath, long src, long matrix) {
419         Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix);
420         if (matrixDelegate == null) {
421             return;
422         }
423 
424         addPath(nPath, src, matrixDelegate.getAffineTransform());
425     }
426 
427     @LayoutlibDelegate
nOffset(long nPath, float dx, float dy)428     /*package*/ static void nOffset(long nPath, float dx, float dy) {
429         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
430         if (pathDelegate == null) {
431             return;
432         }
433 
434         pathDelegate.offset(dx, dy);
435     }
436 
437     @LayoutlibDelegate
nSetLastPoint(long nPath, float dx, float dy)438     /*package*/ static void nSetLastPoint(long nPath, float dx, float dy) {
439         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
440         if (pathDelegate == null) {
441             return;
442         }
443 
444         pathDelegate.mLastX = dx;
445         pathDelegate.mLastY = dy;
446     }
447 
448     @LayoutlibDelegate
nTransform(long nPath, long matrix, long dst_path)449     /*package*/ static void nTransform(long nPath, long matrix,
450                                                 long dst_path) {
451         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
452         if (pathDelegate == null) {
453             return;
454         }
455 
456         Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix);
457         if (matrixDelegate == null) {
458             return;
459         }
460 
461         // this can be null if dst_path is 0
462         Path_Delegate dstDelegate = sManager.getDelegate(dst_path);
463 
464         pathDelegate.transform(matrixDelegate, dstDelegate);
465     }
466 
467     @LayoutlibDelegate
nTransform(long nPath, long matrix)468     /*package*/ static void nTransform(long nPath, long matrix) {
469         nTransform(nPath, matrix, 0);
470     }
471 
472     @LayoutlibDelegate
nOp(long nPath1, long nPath2, int op, long result)473     /*package*/ static boolean nOp(long nPath1, long nPath2, int op, long result) {
474         Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, "Path.op() not supported", null);
475         return false;
476     }
477 
478     @LayoutlibDelegate
nFinalize(long nPath)479     /*package*/ static void nFinalize(long nPath) {
480         sManager.removeJavaReferenceFor(nPath);
481     }
482 
483     @LayoutlibDelegate
nApproximate(long nPath, float error)484     /*package*/ static float[] nApproximate(long nPath, float error) {
485         Path_Delegate pathDelegate = sManager.getDelegate(nPath);
486         if (pathDelegate == null) {
487             return null;
488         }
489         // Get a FlatteningIterator
490         PathIterator iterator = pathDelegate.getJavaShape().getPathIterator(null, error);
491 
492         float segment[] = new float[6];
493         float totalLength = 0;
494         ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>();
495         Point2D.Float previousPoint = null;
496         while (!iterator.isDone()) {
497             int type = iterator.currentSegment(segment);
498             Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]);
499             // MoveTo shouldn't affect the length
500             if (previousPoint != null && type != PathIterator.SEG_MOVETO) {
501                 totalLength += currentPoint.distance(previousPoint);
502             }
503             previousPoint = currentPoint;
504             points.add(currentPoint);
505             iterator.next();
506         }
507 
508         int nPoints = points.size();
509         float[] result = new float[nPoints * 3];
510         previousPoint = null;
511         // Distance that we've covered so far. Used to calculate the fraction of the path that
512         // we've covered up to this point.
513         float walkedDistance = .0f;
514         for (int i = 0; i < nPoints; i++) {
515             Point2D.Float point = points.get(i);
516             float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f;
517             walkedDistance += distance;
518             result[i * 3] = walkedDistance / totalLength;
519             result[i * 3 + 1] = point.x;
520             result[i * 3 + 2] = point.y;
521 
522             previousPoint = point;
523         }
524 
525         return result;
526     }
527 
528     // ---- Private helper methods ----
529 
set(Path_Delegate delegate)530     private void set(Path_Delegate delegate) {
531         mPath.reset();
532         setFillType(delegate.mFillType);
533         mPath.append(delegate.mPath, false /*connect*/);
534     }
535 
setFillType(FillType fillType)536     private void setFillType(FillType fillType) {
537         mFillType = fillType;
538         mPath.setWindingRule(getWindingRule(fillType));
539     }
540 
541     /**
542      * Returns the Java2D winding rules matching a given Android {@link FillType}.
543      * @param type the android fill type
544      * @return the matching java2d winding rule.
545      */
getWindingRule(FillType type)546     private static int getWindingRule(FillType type) {
547         switch (type) {
548             case WINDING:
549             case INVERSE_WINDING:
550                 return GeneralPath.WIND_NON_ZERO;
551             case EVEN_ODD:
552             case INVERSE_EVEN_ODD:
553                 return GeneralPath.WIND_EVEN_ODD;
554 
555             default:
556                 assert false;
557                 return GeneralPath.WIND_NON_ZERO;
558         }
559     }
560 
561     @NonNull
getDirection(int direction)562     private static Direction getDirection(int direction) {
563         for (Direction d : Direction.values()) {
564             if (direction == d.nativeInt) {
565                 return d;
566             }
567         }
568 
569         assert false;
570         return null;
571     }
572 
addPath(long destPath, long srcPath, AffineTransform transform)573     public static void addPath(long destPath, long srcPath, AffineTransform transform) {
574         Path_Delegate destPathDelegate = sManager.getDelegate(destPath);
575         if (destPathDelegate == null) {
576             return;
577         }
578 
579         Path_Delegate srcPathDelegate = sManager.getDelegate(srcPath);
580         if (srcPathDelegate == null) {
581             return;
582         }
583 
584         if (transform != null) {
585             destPathDelegate.mPath.append(
586                     srcPathDelegate.mPath.getPathIterator(transform), false);
587         } else {
588             destPathDelegate.mPath.append(srcPathDelegate.mPath, false);
589         }
590     }
591 
592 
593     /**
594      * Returns whether the path already contains any points.
595      * Note that this is different to
596      * {@link #isEmpty} because if all elements are {@link PathIterator#SEG_MOVETO},
597      * {@link #isEmpty} will return true while hasPoints will return false.
598      */
hasPoints()599     public boolean hasPoints() {
600         return !mPath.getPathIterator(null).isDone();
601     }
602 
603     /**
604      * Returns whether the path is empty (contains no lines or curves).
605      * @see Path#isEmpty
606      */
isEmpty()607     public boolean isEmpty() {
608         if (!mCachedIsEmpty) {
609             return false;
610         }
611 
612         float[] coords = new float[6];
613         mCachedIsEmpty = Boolean.TRUE;
614         for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) {
615             int type = it.currentSegment(coords);
616             if (type != PathIterator.SEG_MOVETO) {
617                 // Once we know that the path is not empty, we do not need to check again unless
618                 // Path#reset is called.
619                 mCachedIsEmpty = false;
620                 return false;
621             }
622         }
623 
624         return true;
625     }
626 
627     /**
628      * Fills the given {@link RectF} with the path bounds.
629      * @param bounds the RectF to be filled.
630      */
fillBounds(RectF bounds)631     public void fillBounds(RectF bounds) {
632         Rectangle2D rect = mPath.getBounds2D();
633         bounds.left = (float)rect.getMinX();
634         bounds.right = (float)rect.getMaxX();
635         bounds.top = (float)rect.getMinY();
636         bounds.bottom = (float)rect.getMaxY();
637     }
638 
639     /**
640      * Set the beginning of the next contour to the point (x,y).
641      *
642      * @param x The x-coordinate of the start of a new contour
643      * @param y The y-coordinate of the start of a new contour
644      */
moveTo(float x, float y)645     public void moveTo(float x, float y) {
646         mPath.moveTo(mLastX = x, mLastY = y);
647     }
648 
649     /**
650      * Set the beginning of the next contour relative to the last point on the
651      * previous contour. If there is no previous contour, this is treated the
652      * same as moveTo().
653      *
654      * @param dx The amount to add to the x-coordinate of the end of the
655      *           previous contour, to specify the start of a new contour
656      * @param dy The amount to add to the y-coordinate of the end of the
657      *           previous contour, to specify the start of a new contour
658      */
rMoveTo(float dx, float dy)659     public void rMoveTo(float dx, float dy) {
660         dx += mLastX;
661         dy += mLastY;
662         mPath.moveTo(mLastX = dx, mLastY = dy);
663     }
664 
665     /**
666      * Add a line from the last point to the specified point (x,y).
667      * If no moveTo() call has been made for this contour, the first point is
668      * automatically set to (0,0).
669      *
670      * @param x The x-coordinate of the end of a line
671      * @param y The y-coordinate of the end of a line
672      */
lineTo(float x, float y)673     public void lineTo(float x, float y) {
674         if (!hasPoints()) {
675             mPath.moveTo(mLastX = 0, mLastY = 0);
676         }
677         mPath.lineTo(mLastX = x, mLastY = y);
678     }
679 
680     /**
681      * Same as lineTo, but the coordinates are considered relative to the last
682      * point on this contour. If there is no previous point, then a moveTo(0,0)
683      * is inserted automatically.
684      *
685      * @param dx The amount to add to the x-coordinate of the previous point on
686      *           this contour, to specify a line
687      * @param dy The amount to add to the y-coordinate of the previous point on
688      *           this contour, to specify a line
689      */
rLineTo(float dx, float dy)690     public void rLineTo(float dx, float dy) {
691         if (!hasPoints()) {
692             mPath.moveTo(mLastX = 0, mLastY = 0);
693         }
694 
695         if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
696             // The delta is so small that this shouldn't generate a line
697             return;
698         }
699 
700         dx += mLastX;
701         dy += mLastY;
702         mPath.lineTo(mLastX = dx, mLastY = dy);
703     }
704 
705     /**
706      * Add a quadratic bezier from the last point, approaching control point
707      * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
708      * this contour, the first point is automatically set to (0,0).
709      *
710      * @param x1 The x-coordinate of the control point on a quadratic curve
711      * @param y1 The y-coordinate of the control point on a quadratic curve
712      * @param x2 The x-coordinate of the end point on a quadratic curve
713      * @param y2 The y-coordinate of the end point on a quadratic curve
714      */
quadTo(float x1, float y1, float x2, float y2)715     public void quadTo(float x1, float y1, float x2, float y2) {
716         mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2);
717     }
718 
719     /**
720      * Same as quadTo, but the coordinates are considered relative to the last
721      * point on this contour. If there is no previous point, then a moveTo(0,0)
722      * is inserted automatically.
723      *
724      * @param dx1 The amount to add to the x-coordinate of the last point on
725      *            this contour, for the control point of a quadratic curve
726      * @param dy1 The amount to add to the y-coordinate of the last point on
727      *            this contour, for the control point of a quadratic curve
728      * @param dx2 The amount to add to the x-coordinate of the last point on
729      *            this contour, for the end point of a quadratic curve
730      * @param dy2 The amount to add to the y-coordinate of the last point on
731      *            this contour, for the end point of a quadratic curve
732      */
rQuadTo(float dx1, float dy1, float dx2, float dy2)733     public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
734         if (!hasPoints()) {
735             mPath.moveTo(mLastX = 0, mLastY = 0);
736         }
737         dx1 += mLastX;
738         dy1 += mLastY;
739         dx2 += mLastX;
740         dy2 += mLastY;
741         mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2);
742     }
743 
744     /**
745      * Add a cubic bezier from the last point, approaching control points
746      * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
747      * made for this contour, the first point is automatically set to (0,0).
748      *
749      * @param x1 The x-coordinate of the 1st control point on a cubic curve
750      * @param y1 The y-coordinate of the 1st control point on a cubic curve
751      * @param x2 The x-coordinate of the 2nd control point on a cubic curve
752      * @param y2 The y-coordinate of the 2nd control point on a cubic curve
753      * @param x3 The x-coordinate of the end point on a cubic curve
754      * @param y3 The y-coordinate of the end point on a cubic curve
755      */
cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)756     public void cubicTo(float x1, float y1, float x2, float y2,
757                         float x3, float y3) {
758         if (!hasPoints()) {
759             mPath.moveTo(0, 0);
760         }
761         mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
762     }
763 
764     /**
765      * Same as cubicTo, but the coordinates are considered relative to the
766      * current point on this contour. If there is no previous point, then a
767      * moveTo(0,0) is inserted automatically.
768      */
rCubicTo(float dx1, float dy1, float dx2, float dy2, float dx3, float dy3)769     public void rCubicTo(float dx1, float dy1, float dx2, float dy2,
770                          float dx3, float dy3) {
771         if (!hasPoints()) {
772             mPath.moveTo(mLastX = 0, mLastY = 0);
773         }
774         dx1 += mLastX;
775         dy1 += mLastY;
776         dx2 += mLastX;
777         dy2 += mLastY;
778         dx3 += mLastX;
779         dy3 += mLastY;
780         mPath.curveTo(dx1, dy1, dx2, dy2, mLastX = dx3, mLastY = dy3);
781     }
782 
783     /**
784      * Append the specified arc to the path as a new contour. If the start of
785      * the path is different from the path's current last point, then an
786      * automatic lineTo() is added to connect the current contour to the
787      * start of the arc. However, if the path is empty, then we call moveTo()
788      * with the first point of the arc. The sweep angle is tread mod 360.
789      *
790      * @param left        The left of oval defining shape and size of the arc
791      * @param top         The top of oval defining shape and size of the arc
792      * @param right       The right of oval defining shape and size of the arc
793      * @param bottom      The bottom of oval defining shape and size of the arc
794      * @param startAngle  Starting angle (in degrees) where the arc begins
795      * @param sweepAngle  Sweep angle (in degrees) measured clockwise, treated
796      *                    mod 360.
797      * @param forceMoveTo If true, always begin a new contour with the arc
798      */
arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)799     public void arcTo(float left, float top, float right, float bottom, float startAngle,
800             float sweepAngle,
801             boolean forceMoveTo) {
802         Arc2D arc = new Arc2D.Float(left, top, right - left, bottom - top, -startAngle,
803                 -sweepAngle, Arc2D.OPEN);
804         mPath.append(arc, true /*connect*/);
805 
806         resetLastPointFromPath();
807     }
808 
809     /**
810      * Close the current contour. If the current point is not equal to the
811      * first point of the contour, a line segment is automatically added.
812      */
close()813     public void close() {
814         mPath.closePath();
815     }
816 
resetLastPointFromPath()817     private void resetLastPointFromPath() {
818         Point2D last = mPath.getCurrentPoint();
819         mLastX = (float) last.getX();
820         mLastY = (float) last.getY();
821     }
822 
823     /**
824      * Add a closed rectangle contour to the path
825      *
826      * @param left   The left side of a rectangle to add to the path
827      * @param top    The top of a rectangle to add to the path
828      * @param right  The right side of a rectangle to add to the path
829      * @param bottom The bottom of a rectangle to add to the path
830      * @param dir    The direction to wind the rectangle's contour
831      */
addRect(float left, float top, float right, float bottom, int dir)832     public void addRect(float left, float top, float right, float bottom,
833                         int dir) {
834         moveTo(left, top);
835 
836         Direction direction = getDirection(dir);
837 
838         switch (direction) {
839             case CW:
840                 lineTo(right, top);
841                 lineTo(right, bottom);
842                 lineTo(left, bottom);
843                 break;
844             case CCW:
845                 lineTo(left, bottom);
846                 lineTo(right, bottom);
847                 lineTo(right, top);
848                 break;
849         }
850 
851         close();
852 
853         resetLastPointFromPath();
854     }
855 
856     /**
857      * Offset the path by (dx,dy), returning true on success
858      *
859      * @param dx  The amount in the X direction to offset the entire path
860      * @param dy  The amount in the Y direction to offset the entire path
861      */
offset(float dx, float dy)862     public void offset(float dx, float dy) {
863         GeneralPath newPath = new GeneralPath();
864 
865         PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy));
866 
867         newPath.append(iterator, false /*connect*/);
868         mPath = newPath;
869     }
870 
871     /**
872      * Transform the points in this path by matrix, and write the answer
873      * into dst. If dst is null, then the the original path is modified.
874      *
875      * @param matrix The matrix to apply to the path
876      * @param dst    The transformed path is written here. If dst is null,
877      *               then the the original path is modified
878      */
transform(Matrix_Delegate matrix, Path_Delegate dst)879     public void transform(Matrix_Delegate matrix, Path_Delegate dst) {
880         if (matrix.hasPerspective()) {
881             assert false;
882             Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_AFFINE,
883                     "android.graphics.Path#transform() only " +
884                     "supports affine transformations.", null, null /*data*/);
885         }
886 
887         GeneralPath newPath = new GeneralPath();
888 
889         PathIterator iterator = mPath.getPathIterator(matrix.getAffineTransform());
890 
891         newPath.append(iterator, false /*connect*/);
892 
893         if (dst != null) {
894             dst.mPath = newPath;
895         } else {
896             mPath = newPath;
897         }
898     }
899 }
900