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