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
17package android.graphics;
18
19import com.android.ide.common.rendering.api.LayoutLog;
20import com.android.layoutlib.bridge.Bridge;
21import com.android.layoutlib.bridge.impl.DelegateManager;
22import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
23
24import android.annotation.NonNull;
25import android.graphics.Path.Direction;
26import android.graphics.Path.FillType;
27
28import java.awt.Shape;
29import java.awt.geom.AffineTransform;
30import java.awt.geom.Arc2D;
31import java.awt.geom.Area;
32import java.awt.geom.Ellipse2D;
33import java.awt.geom.GeneralPath;
34import java.awt.geom.Path2D;
35import java.awt.geom.PathIterator;
36import java.awt.geom.Point2D;
37import java.awt.geom.Rectangle2D;
38import java.awt.geom.RoundRectangle2D;
39import 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 */
54public 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
74    public static Path_Delegate getDelegate(long nPath) {
75        return sManager.getDelegate(nPath);
76    }
77
78    public Path2D getJavaShape() {
79        return mPath;
80    }
81
82    public void setJavaShape(Shape shape) {
83        reset();
84        mPath.append(shape, false /*connect*/);
85    }
86
87    public void reset() {
88        mPath.reset();
89        mLastX = 0;
90        mLastY = 0;
91    }
92
93    public void setPathIterator(PathIterator iterator) {
94        reset();
95        mPath.append(iterator, false /*connect*/);
96    }
97
98    // ---- native methods ----
99
100    @LayoutlibDelegate
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
413    /*package*/ static void nAddPath(long nPath, long src) {
414        addPath(nPath, src, null /*transform*/);
415    }
416
417    @LayoutlibDelegate
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
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
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
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
468    /*package*/ static void nTransform(long nPath, long matrix) {
469        nTransform(nPath, matrix, 0);
470    }
471
472    @LayoutlibDelegate
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
479    /*package*/ static void nFinalize(long nPath) {
480        sManager.removeJavaReferenceFor(nPath);
481    }
482
483    @LayoutlibDelegate
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        for (int i = 0; i < nPoints; i++) {
512            Point2D.Float point = points.get(i);
513            float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f;
514            result[i * 3] = distance / totalLength;
515            result[i * 3 + 1] = point.x;
516            result[i * 3 + 2] = point.y;
517
518            totalLength += distance;
519            previousPoint = point;
520        }
521
522        return result;
523    }
524
525    // ---- Private helper methods ----
526
527    private void set(Path_Delegate delegate) {
528        mPath.reset();
529        setFillType(delegate.mFillType);
530        mPath.append(delegate.mPath, false /*connect*/);
531    }
532
533    private void setFillType(FillType fillType) {
534        mFillType = fillType;
535        mPath.setWindingRule(getWindingRule(fillType));
536    }
537
538    /**
539     * Returns the Java2D winding rules matching a given Android {@link FillType}.
540     * @param type the android fill type
541     * @return the matching java2d winding rule.
542     */
543    private static int getWindingRule(FillType type) {
544        switch (type) {
545            case WINDING:
546            case INVERSE_WINDING:
547                return GeneralPath.WIND_NON_ZERO;
548            case EVEN_ODD:
549            case INVERSE_EVEN_ODD:
550                return GeneralPath.WIND_EVEN_ODD;
551
552            default:
553                assert false;
554                return GeneralPath.WIND_NON_ZERO;
555        }
556    }
557
558    @NonNull
559    private static Direction getDirection(int direction) {
560        for (Direction d : Direction.values()) {
561            if (direction == d.nativeInt) {
562                return d;
563            }
564        }
565
566        assert false;
567        return null;
568    }
569
570    public static void addPath(long destPath, long srcPath, AffineTransform transform) {
571        Path_Delegate destPathDelegate = sManager.getDelegate(destPath);
572        if (destPathDelegate == null) {
573            return;
574        }
575
576        Path_Delegate srcPathDelegate = sManager.getDelegate(srcPath);
577        if (srcPathDelegate == null) {
578            return;
579        }
580
581        if (transform != null) {
582            destPathDelegate.mPath.append(
583                    srcPathDelegate.mPath.getPathIterator(transform), false);
584        } else {
585            destPathDelegate.mPath.append(srcPathDelegate.mPath, false);
586        }
587    }
588
589
590    /**
591     * Returns whether the path already contains any points.
592     * Note that this is different to
593     * {@link #isEmpty} because if all elements are {@link PathIterator#SEG_MOVETO},
594     * {@link #isEmpty} will return true while hasPoints will return false.
595     */
596    public boolean hasPoints() {
597        return !mPath.getPathIterator(null).isDone();
598    }
599
600    /**
601     * Returns whether the path is empty (contains no lines or curves).
602     * @see Path#isEmpty
603     */
604    public boolean isEmpty() {
605        if (!mCachedIsEmpty) {
606            return false;
607        }
608
609        float[] coords = new float[6];
610        mCachedIsEmpty = Boolean.TRUE;
611        for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) {
612            int type = it.currentSegment(coords);
613            if (type != PathIterator.SEG_MOVETO) {
614                // Once we know that the path is not empty, we do not need to check again unless
615                // Path#reset is called.
616                mCachedIsEmpty = false;
617                return false;
618            }
619        }
620
621        return true;
622    }
623
624    /**
625     * Fills the given {@link RectF} with the path bounds.
626     * @param bounds the RectF to be filled.
627     */
628    public void fillBounds(RectF bounds) {
629        Rectangle2D rect = mPath.getBounds2D();
630        bounds.left = (float)rect.getMinX();
631        bounds.right = (float)rect.getMaxX();
632        bounds.top = (float)rect.getMinY();
633        bounds.bottom = (float)rect.getMaxY();
634    }
635
636    /**
637     * Set the beginning of the next contour to the point (x,y).
638     *
639     * @param x The x-coordinate of the start of a new contour
640     * @param y The y-coordinate of the start of a new contour
641     */
642    public void moveTo(float x, float y) {
643        mPath.moveTo(mLastX = x, mLastY = y);
644    }
645
646    /**
647     * Set the beginning of the next contour relative to the last point on the
648     * previous contour. If there is no previous contour, this is treated the
649     * same as moveTo().
650     *
651     * @param dx The amount to add to the x-coordinate of the end of the
652     *           previous contour, to specify the start of a new contour
653     * @param dy The amount to add to the y-coordinate of the end of the
654     *           previous contour, to specify the start of a new contour
655     */
656    public void rMoveTo(float dx, float dy) {
657        dx += mLastX;
658        dy += mLastY;
659        mPath.moveTo(mLastX = dx, mLastY = dy);
660    }
661
662    /**
663     * Add a line from the last point to the specified point (x,y).
664     * If no moveTo() call has been made for this contour, the first point is
665     * automatically set to (0,0).
666     *
667     * @param x The x-coordinate of the end of a line
668     * @param y The y-coordinate of the end of a line
669     */
670    public void lineTo(float x, float y) {
671        if (!hasPoints()) {
672            mPath.moveTo(mLastX = 0, mLastY = 0);
673        }
674        mPath.lineTo(mLastX = x, mLastY = y);
675    }
676
677    /**
678     * Same as lineTo, but the coordinates are considered relative to the last
679     * point on this contour. If there is no previous point, then a moveTo(0,0)
680     * is inserted automatically.
681     *
682     * @param dx The amount to add to the x-coordinate of the previous point on
683     *           this contour, to specify a line
684     * @param dy The amount to add to the y-coordinate of the previous point on
685     *           this contour, to specify a line
686     */
687    public void rLineTo(float dx, float dy) {
688        if (!hasPoints()) {
689            mPath.moveTo(mLastX = 0, mLastY = 0);
690        }
691
692        if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
693            // The delta is so small that this shouldn't generate a line
694            return;
695        }
696
697        dx += mLastX;
698        dy += mLastY;
699        mPath.lineTo(mLastX = dx, mLastY = dy);
700    }
701
702    /**
703     * Add a quadratic bezier from the last point, approaching control point
704     * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
705     * this contour, the first point is automatically set to (0,0).
706     *
707     * @param x1 The x-coordinate of the control point on a quadratic curve
708     * @param y1 The y-coordinate of the control point on a quadratic curve
709     * @param x2 The x-coordinate of the end point on a quadratic curve
710     * @param y2 The y-coordinate of the end point on a quadratic curve
711     */
712    public void quadTo(float x1, float y1, float x2, float y2) {
713        mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2);
714    }
715
716    /**
717     * Same as quadTo, but the coordinates are considered relative to the last
718     * point on this contour. If there is no previous point, then a moveTo(0,0)
719     * is inserted automatically.
720     *
721     * @param dx1 The amount to add to the x-coordinate of the last point on
722     *            this contour, for the control point of a quadratic curve
723     * @param dy1 The amount to add to the y-coordinate of the last point on
724     *            this contour, for the control point of a quadratic curve
725     * @param dx2 The amount to add to the x-coordinate of the last point on
726     *            this contour, for the end point of a quadratic curve
727     * @param dy2 The amount to add to the y-coordinate of the last point on
728     *            this contour, for the end point of a quadratic curve
729     */
730    public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
731        if (!hasPoints()) {
732            mPath.moveTo(mLastX = 0, mLastY = 0);
733        }
734        dx1 += mLastX;
735        dy1 += mLastY;
736        dx2 += mLastX;
737        dy2 += mLastY;
738        mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2);
739    }
740
741    /**
742     * Add a cubic bezier from the last point, approaching control points
743     * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
744     * made for this contour, the first point is automatically set to (0,0).
745     *
746     * @param x1 The x-coordinate of the 1st control point on a cubic curve
747     * @param y1 The y-coordinate of the 1st control point on a cubic curve
748     * @param x2 The x-coordinate of the 2nd control point on a cubic curve
749     * @param y2 The y-coordinate of the 2nd control point on a cubic curve
750     * @param x3 The x-coordinate of the end point on a cubic curve
751     * @param y3 The y-coordinate of the end point on a cubic curve
752     */
753    public void cubicTo(float x1, float y1, float x2, float y2,
754                        float x3, float y3) {
755        if (!hasPoints()) {
756            mPath.moveTo(0, 0);
757        }
758        mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
759    }
760
761    /**
762     * Same as cubicTo, but the coordinates are considered relative to the
763     * current point on this contour. If there is no previous point, then a
764     * moveTo(0,0) is inserted automatically.
765     */
766    public void rCubicTo(float dx1, float dy1, float dx2, float dy2,
767                         float dx3, float dy3) {
768        if (!hasPoints()) {
769            mPath.moveTo(mLastX = 0, mLastY = 0);
770        }
771        dx1 += mLastX;
772        dy1 += mLastY;
773        dx2 += mLastX;
774        dy2 += mLastY;
775        dx3 += mLastX;
776        dy3 += mLastY;
777        mPath.curveTo(dx1, dy1, dx2, dy2, mLastX = dx3, mLastY = dy3);
778    }
779
780    /**
781     * Append the specified arc to the path as a new contour. If the start of
782     * the path is different from the path's current last point, then an
783     * automatic lineTo() is added to connect the current contour to the
784     * start of the arc. However, if the path is empty, then we call moveTo()
785     * with the first point of the arc. The sweep angle is tread mod 360.
786     *
787     * @param left        The left of oval defining shape and size of the arc
788     * @param top         The top of oval defining shape and size of the arc
789     * @param right       The right of oval defining shape and size of the arc
790     * @param bottom      The bottom of oval defining shape and size of the arc
791     * @param startAngle  Starting angle (in degrees) where the arc begins
792     * @param sweepAngle  Sweep angle (in degrees) measured clockwise, treated
793     *                    mod 360.
794     * @param forceMoveTo If true, always begin a new contour with the arc
795     */
796    public void arcTo(float left, float top, float right, float bottom, float startAngle,
797            float sweepAngle,
798            boolean forceMoveTo) {
799        Arc2D arc = new Arc2D.Float(left, top, right - left, bottom - top, -startAngle,
800                -sweepAngle, Arc2D.OPEN);
801        mPath.append(arc, true /*connect*/);
802
803        resetLastPointFromPath();
804    }
805
806    /**
807     * Close the current contour. If the current point is not equal to the
808     * first point of the contour, a line segment is automatically added.
809     */
810    public void close() {
811        mPath.closePath();
812    }
813
814    private void resetLastPointFromPath() {
815        Point2D last = mPath.getCurrentPoint();
816        mLastX = (float) last.getX();
817        mLastY = (float) last.getY();
818    }
819
820    /**
821     * Add a closed rectangle contour to the path
822     *
823     * @param left   The left side of a rectangle to add to the path
824     * @param top    The top of a rectangle to add to the path
825     * @param right  The right side of a rectangle to add to the path
826     * @param bottom The bottom of a rectangle to add to the path
827     * @param dir    The direction to wind the rectangle's contour
828     */
829    public void addRect(float left, float top, float right, float bottom,
830                        int dir) {
831        moveTo(left, top);
832
833        Direction direction = getDirection(dir);
834
835        switch (direction) {
836            case CW:
837                lineTo(right, top);
838                lineTo(right, bottom);
839                lineTo(left, bottom);
840                break;
841            case CCW:
842                lineTo(left, bottom);
843                lineTo(right, bottom);
844                lineTo(right, top);
845                break;
846        }
847
848        close();
849
850        resetLastPointFromPath();
851    }
852
853    /**
854     * Offset the path by (dx,dy), returning true on success
855     *
856     * @param dx  The amount in the X direction to offset the entire path
857     * @param dy  The amount in the Y direction to offset the entire path
858     */
859    public void offset(float dx, float dy) {
860        GeneralPath newPath = new GeneralPath();
861
862        PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy));
863
864        newPath.append(iterator, false /*connect*/);
865        mPath = newPath;
866    }
867
868    /**
869     * Transform the points in this path by matrix, and write the answer
870     * into dst. If dst is null, then the the original path is modified.
871     *
872     * @param matrix The matrix to apply to the path
873     * @param dst    The transformed path is written here. If dst is null,
874     *               then the the original path is modified
875     */
876    public void transform(Matrix_Delegate matrix, Path_Delegate dst) {
877        if (matrix.hasPerspective()) {
878            assert false;
879            Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_AFFINE,
880                    "android.graphics.Path#transform() only " +
881                    "supports affine transformations.", null, null /*data*/);
882        }
883
884        GeneralPath newPath = new GeneralPath();
885
886        PathIterator iterator = mPath.getPathIterator(matrix.getAffineTransform());
887
888        newPath.append(iterator, false /*connect*/);
889
890        if (dst != null) {
891            dst.mPath = newPath;
892        } else {
893            mPath = newPath;
894        }
895    }
896}
897