VectorDrawable.java revision 452f6ece7fe2fd1a85fca53f54e90bf041083b21
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14
15package android.graphics.drawable;
16
17import android.content.res.Resources;
18import android.content.res.Resources.Theme;
19import android.content.res.TypedArray;
20import android.graphics.Canvas;
21import android.graphics.ColorFilter;
22import android.graphics.Matrix;
23import android.graphics.Paint;
24import android.graphics.Path;
25import android.graphics.PathMeasure;
26import android.graphics.PixelFormat;
27import android.graphics.Rect;
28import android.graphics.Region;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.util.Xml;
32
33import com.android.internal.R;
34
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37import org.xmlpull.v1.XmlPullParserFactory;
38
39import java.io.IOException;
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.HashMap;
43
44/**
45 * This lets you create a drawable based on an XML vector graphic It can be
46 * defined in an XML file with the <code>&lt;vector></code> element.
47 * <p/>
48 * The vector drawable has the following elements:
49 * <p/>
50 * <dl>
51 * <dt><code>&lt;vector></code></dt>
52 * <dd>Used to defined a vector drawable</dd>
53 * <dt><code>&lt;size></code></dt>
54 * <dd>Used to defined the intrinsic Width Height size of the drawable using
55 * <code>android:width</code> and <code>android:height</code></dd>
56 * <dt><code>&lt;viewport></code></dt>
57 * <dd>Used to defined the size of the virtual canvas the paths are drawn on.
58 * The size is defined using the attributes <code>android:viewportHeight</code>
59 * <code>android:viewportWidth</code></dd>
60 * <dt><code>&lt;group></code></dt>
61 * <dd>Defines a group of paths or subgroups, plus transformation information.
62 * The transformations are defined in the same coordinates as the viewport.
63 * And the transformations are applied in the order of scale, rotate then translate. </dd>
64 * <dt><code>android:rotation</code>
65 * <dd>The degrees of rotation of the group.</dd></dt>
66 * <dt><code>android:pivotX</code>
67 * <dd>The X coordinate of the pivot for the scale and rotation of the group</dd></dt>
68 * <dt><code>android:pivotY</code>
69 * <dd>The Y coordinate of the pivot for the scale and rotation of the group</dd></dt>
70 * <dt><code>android:scaleX</code>
71 * <dd>The amount of scale on the X Coordinate</dd></dt>
72 * <dt><code>android:scaleY</code>
73 * <dd>The amount of scale on the Y coordinate</dd></dt>
74 * <dt><code>android:translateX</code>
75 * <dd>The amount of translation on the X coordinate</dd></dt>
76 * <dt><code>android:translateY</code>
77 * <dd>The amount of translation on the Y coordinate</dd></dt>
78 * <dt><code>&lt;path></code></dt>
79 * <dd>Defines paths to be drawn.
80 * <dl>
81 * <dt><code>android:name</code>
82 * <dd>Defines the name of the path.</dd></dt>
83 * <dt><code>android:pathData</code>
84 * <dd>Defines path string. This is using exactly same format as "d" attribute
85 * in the SVG's path data</dd></dt>
86 * <dt><code>android:fill</code>
87 * <dd>Defines the color to fill the path (none if not present).</dd></dt>
88 * <dt><code>android:stroke</code>
89 * <dd>Defines the color to draw the path outline (none if not present).</dd>
90 * </dt>
91 * <dt><code>android:strokeWidth</code>
92 * <dd>The width a path stroke</dd></dt>
93 * <dt><code>android:strokeOpacity</code>
94 * <dd>The opacity of a path stroke</dd></dt>
95 * <dt><code>android:fillOpacity</code>
96 * <dd>The opacity to fill the path with</dd></dt>
97 * <dt><code>android:trimPathStart</code>
98 * <dd>The fraction of the path to trim from the start from 0 to 1</dd></dt>
99 * <dt><code>android:trimPathEnd</code>
100 * <dd>The fraction of the path to trim from the end from 0 to 1</dd></dt>
101 * <dt><code>android:trimPathOffset</code>
102 * <dd>Shift trim region (allows showed region to include the start and end)
103 * from 0 to 1</dd></dt>
104 * <dt><code>android:clipToPath</code>
105 * <dd>Path will set the clip path</dd></dt>
106 * <dt><code>android:strokeLineCap</code>
107 * <dd>Sets the linecap for a stroked path: butt, round, square</dd></dt>
108 * <dt><code>android:strokeLineJoin</code>
109 * <dd>Sets the lineJoin for a stroked path: miter,round,bevel</dd></dt>
110 * <dt><code>android:strokeMiterLimit</code>
111 * <dd>Sets the Miter limit for a stroked path</dd></dt>
112 * </dl>
113 * </dd>
114 */
115public class VectorDrawable extends Drawable {
116    private static final String LOGTAG = VectorDrawable.class.getSimpleName();
117
118    private static final String SHAPE_SIZE = "size";
119    private static final String SHAPE_VIEWPORT = "viewport";
120    private static final String SHAPE_GROUP = "group";
121    private static final String SHAPE_PATH = "path";
122    private static final String SHAPE_VECTOR = "vector";
123
124    private static final int LINECAP_BUTT = 0;
125    private static final int LINECAP_ROUND = 1;
126    private static final int LINECAP_SQUARE = 2;
127
128    private static final int LINEJOIN_MITER = 0;
129    private static final int LINEJOIN_ROUND = 1;
130    private static final int LINEJOIN_BEVEL = 2;
131
132    private final VectorDrawableState mVectorState;
133
134    private int mAlpha = 0xFF;
135
136    public VectorDrawable() {
137        mVectorState = new VectorDrawableState(null);
138    }
139
140    private VectorDrawable(VectorDrawableState state, Resources res, Theme theme) {
141        mVectorState = new VectorDrawableState(state);
142
143        if (theme != null && canApplyTheme()) {
144            applyTheme(theme);
145        }
146    }
147
148    @Override
149    public ConstantState getConstantState() {
150        return mVectorState;
151    }
152
153    @Override
154    public void draw(Canvas canvas) {
155        final int saveCount = canvas.save();
156        final Rect bounds = getBounds();
157        canvas.translate(bounds.left, bounds.top);
158        mVectorState.mVPathRenderer.draw(canvas, bounds.width(), bounds.height());
159        canvas.restoreToCount(saveCount);
160    }
161
162    @Override
163    public void setAlpha(int alpha) {
164        // TODO correct handling of transparent
165        if (mAlpha != alpha) {
166            mAlpha = alpha;
167            invalidateSelf();
168        }
169    }
170
171    @Override
172    public void setColorFilter(ColorFilter colorFilter) {
173        mVectorState.mVPathRenderer.setColorFilter(colorFilter);
174        invalidateSelf();
175    }
176
177    @Override
178    public int getOpacity() {
179        return PixelFormat.TRANSLUCENT;
180    }
181
182    /**
183     * Sets padding for this shape, defined by a Rect object. Define the padding
184     * in the Rect object as: left, top, right, bottom.
185     */
186    public void setPadding(Rect padding) {
187        setPadding(padding.left, padding.top, padding.right, padding.bottom);
188    }
189
190    /**
191     * Sets padding for the shape.
192     *
193     * @param left padding for the left side (in pixels)
194     * @param top padding for the top (in pixels)
195     * @param right padding for the right side (in pixels)
196     * @param bottom padding for the bottom (in pixels)
197     */
198    public void setPadding(int left, int top, int right, int bottom) {
199        if ((left | top | right | bottom) == 0) {
200            mVectorState.mPadding = null;
201        } else {
202            if (mVectorState.mPadding == null) {
203                mVectorState.mPadding = new Rect();
204            }
205            mVectorState.mPadding.set(left, top, right, bottom);
206        }
207        invalidateSelf();
208    }
209
210    @Override
211    public int getIntrinsicWidth() {
212        return (int) mVectorState.mVPathRenderer.mBaseWidth;
213    }
214
215    @Override
216    public int getIntrinsicHeight() {
217        return (int) mVectorState.mVPathRenderer.mBaseHeight;
218    }
219
220    @Override
221    public boolean getPadding(Rect padding) {
222        if (mVectorState.mPadding != null) {
223            padding.set(mVectorState.mPadding);
224            return true;
225        } else {
226            return super.getPadding(padding);
227        }
228    }
229
230    @Override
231    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
232            throws XmlPullParserException, IOException {
233        final VPathRenderer p = inflateInternal(res, parser, attrs, theme);
234        setPathRenderer(p);
235    }
236
237    @Override
238    public boolean canApplyTheme() {
239        return super.canApplyTheme() || mVectorState != null && mVectorState.canApplyTheme();
240    }
241
242    @Override
243    public void applyTheme(Theme t) {
244        super.applyTheme(t);
245
246        final VectorDrawableState state = mVectorState;
247        final VPathRenderer path = state.mVPathRenderer;
248        if (path != null && path.canApplyTheme()) {
249            path.applyTheme(t);
250        }
251    }
252
253    /** @hide */
254    public static VectorDrawable create(Resources resources, int rid) {
255        try {
256            final XmlPullParser xpp = resources.getXml(rid);
257            final AttributeSet attrs = Xml.asAttributeSet(xpp);
258            final XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
259            factory.setNamespaceAware(true);
260
261            final VectorDrawable drawable = new VectorDrawable();
262            drawable.inflate(resources, xpp, attrs);
263
264            return drawable;
265        } catch (XmlPullParserException e) {
266            Log.e(LOGTAG, "parser error", e);
267        } catch (IOException e) {
268            Log.e(LOGTAG, "parser error", e);
269        }
270        return null;
271    }
272
273    private VPathRenderer inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs,
274            Theme theme) throws XmlPullParserException, IOException {
275        final VPathRenderer pathRenderer = new VPathRenderer();
276
277        boolean noSizeTag = true;
278        boolean noViewportTag = true;
279        boolean noGroupTag = true;
280        boolean noPathTag = true;
281
282        VGroup currentGroup = new VGroup();
283
284        int eventType = parser.getEventType();
285        while (eventType != XmlPullParser.END_DOCUMENT) {
286            if (eventType == XmlPullParser.START_TAG) {
287                final String tagName = parser.getName();
288                if (SHAPE_PATH.equals(tagName)) {
289                    final VPath path = new VPath();
290                    path.inflate(res, attrs, theme);
291                    currentGroup.add(path);
292                    noPathTag = false;
293                } else if (SHAPE_SIZE.equals(tagName)) {
294                    pathRenderer.parseSize(res, attrs);
295                    noSizeTag = false;
296                } else if (SHAPE_VIEWPORT.equals(tagName)) {
297                    pathRenderer.parseViewport(res, attrs);
298                    noViewportTag = false;
299                } else if (SHAPE_GROUP.equals(tagName)) {
300                    currentGroup = new VGroup();
301                    currentGroup.inflate(res, attrs, theme);
302                    pathRenderer.mGroupList.add(currentGroup);
303                    noGroupTag = false;
304                }
305            }
306
307            eventType = parser.next();
308        }
309
310        if (noGroupTag && !noPathTag) {
311            pathRenderer.mGroupList.add(currentGroup);
312        }
313
314        if (noSizeTag || noViewportTag || noPathTag) {
315            final StringBuffer tag = new StringBuffer();
316
317            if (noSizeTag) {
318                tag.append(SHAPE_SIZE);
319            }
320
321            if (noViewportTag) {
322                if (tag.length() > 0) {
323                    tag.append(" & ");
324                }
325                tag.append(SHAPE_SIZE);
326            }
327
328            if (noPathTag) {
329                if (tag.length() > 0) {
330                    tag.append(" or ");
331                }
332                tag.append(SHAPE_PATH);
333            }
334
335            throw new XmlPullParserException("no " + tag + " defined");
336        }
337
338        return pathRenderer;
339    }
340
341    private void setPathRenderer(VPathRenderer pathRenderer) {
342        mVectorState.mVPathRenderer = pathRenderer;
343    }
344
345    private static class VectorDrawableState extends ConstantState {
346        int mChangingConfigurations;
347        VPathRenderer mVPathRenderer;
348        Rect mPadding;
349
350        public VectorDrawableState(VectorDrawableState copy) {
351            if (copy != null) {
352                mChangingConfigurations = copy.mChangingConfigurations;
353                mVPathRenderer = new VPathRenderer(copy.mVPathRenderer);
354                mPadding = new Rect(copy.mPadding);
355            }
356        }
357
358        @Override
359        public Drawable newDrawable() {
360            return new VectorDrawable(this, null, null);
361        }
362
363        @Override
364        public Drawable newDrawable(Resources res) {
365            return new VectorDrawable(this, res, null);
366        }
367
368        @Override
369        public Drawable newDrawable(Resources res, Theme theme) {
370            return new VectorDrawable(this, res, theme);
371        }
372
373        @Override
374        public int getChangingConfigurations() {
375            return mChangingConfigurations;
376        }
377    }
378
379    private static class VPathRenderer {
380        private final Path mPath = new Path();
381        private final Path mRenderPath = new Path();
382        private final Matrix mMatrix = new Matrix();
383
384        private Paint mStrokePaint;
385        private Paint mFillPaint;
386        private ColorFilter mColorFilter;
387        private PathMeasure mPathMeasure;
388
389        final ArrayList<VGroup> mGroupList = new ArrayList<VGroup>();
390
391        float mBaseWidth = 0;
392        float mBaseHeight = 0;
393        float mViewportWidth = 0;
394        float mViewportHeight = 0;
395
396        public VPathRenderer() {
397        }
398
399        public VPathRenderer(VPathRenderer copy) {
400            mGroupList.addAll(copy.mGroupList);
401
402            mBaseWidth = copy.mBaseWidth;
403            mBaseHeight = copy.mBaseHeight;
404            mViewportWidth = copy.mViewportHeight;
405            mViewportHeight = copy.mViewportHeight;
406        }
407
408        public boolean canApplyTheme() {
409            final ArrayList<VGroup> groups = mGroupList;
410            for (int i = groups.size() - 1; i >= 0; i--) {
411                final ArrayList<VPath> paths = groups.get(i).mVGList;
412                for (int j = paths.size() - 1; j >= 0; j--) {
413                    final VPath path = paths.get(j);
414                    if (path.canApplyTheme()) {
415                        return true;
416                    }
417                }
418            }
419
420            return false;
421        }
422
423        public void applyTheme(Theme t) {
424            final ArrayList<VGroup> groups = mGroupList;
425            for (int i = groups.size() - 1; i >= 0; i--) {
426                VGroup currentGroup = groups.get(i);
427                currentGroup.applyTheme(t);
428                final ArrayList<VPath> paths = currentGroup.mVGList;
429                for (int j = paths.size() - 1; j >= 0; j--) {
430                    final VPath path = paths.get(j);
431                    if (path.canApplyTheme()) {
432                        path.applyTheme(t);
433                    }
434                }
435            }
436        }
437
438        public void setColorFilter(ColorFilter colorFilter) {
439            mColorFilter = colorFilter;
440
441            if (mFillPaint != null) {
442                mFillPaint.setColorFilter(colorFilter);
443            }
444
445            if (mStrokePaint != null) {
446                mStrokePaint.setColorFilter(colorFilter);
447            }
448
449        }
450
451        public void draw(Canvas canvas, int w, int h) {
452            if (mGroupList == null || mGroupList.size() == 0) {
453                Log.e(LOGTAG,"There is no group to draw");
454                return;
455            }
456
457            for (int i = 0; i < mGroupList.size(); i++) {
458                VGroup currentGroup = mGroupList.get(i);
459                if (currentGroup != null) {
460                    drawPath(currentGroup, canvas, w, h);
461                }
462            }
463        }
464
465        private void drawPath(VGroup vGroup, Canvas canvas, int w, int h) {
466            final float scale = Math.min(h / mViewportHeight, w / mViewportWidth);
467
468            mMatrix.reset();
469
470            // The order we apply is the same as the
471            // RenderNode.cpp::applyViewPropertyTransforms().
472            mMatrix.postTranslate(-vGroup.mPivotX, -vGroup.mPivotY);
473            mMatrix.postScale(vGroup.mScaleX, vGroup.mScaleY);
474            mMatrix.postRotate(vGroup.mRotate, 0, 0);
475            mMatrix.postTranslate(vGroup.mTranslateX + vGroup.mPivotX, vGroup.mTranslateY + vGroup.mPivotY);
476
477            mMatrix.postScale(scale, scale, mViewportWidth / 2f, mViewportHeight / 2f);
478            mMatrix.postTranslate(w / 2f - mViewportWidth / 2f, h / 2f - mViewportHeight / 2f);
479
480            ArrayList<VPath> paths = vGroup.getPaths();
481            for (int i = 0; i < paths.size(); i++) {
482                VPath vPath = paths.get(i);
483                vPath.toPath(mPath);
484                final Path path = mPath;
485
486                if (vPath.mTrimPathStart != 0.0f || vPath.mTrimPathEnd != 1.0f) {
487                    float start = (vPath.mTrimPathStart + vPath.mTrimPathOffset) % 1.0f;
488                    float end = (vPath.mTrimPathEnd + vPath.mTrimPathOffset) % 1.0f;
489
490                    if (mPathMeasure == null) {
491                        mPathMeasure = new PathMeasure();
492                    }
493                    mPathMeasure.setPath(mPath, false);
494
495                    float len = mPathMeasure.getLength();
496                    start = start * len;
497                    end = end * len;
498                    path.reset();
499                    if (start > end) {
500                        mPathMeasure.getSegment(start, len, path, true);
501                        mPathMeasure.getSegment(0f, end, path, true);
502                    } else {
503                        mPathMeasure.getSegment(start, end, path, true);
504                    }
505                    path.rLineTo(0, 0); // fix bug in measure
506                }
507
508                mRenderPath.reset();
509
510                mRenderPath.addPath(path, mMatrix);
511
512                if (vPath.mClip) {
513                    canvas.clipPath(mRenderPath, Region.Op.REPLACE);
514                }
515
516                if (vPath.mFillColor != 0) {
517                    if (mFillPaint == null) {
518                        mFillPaint = new Paint();
519                        mFillPaint.setColorFilter(mColorFilter);
520                        mFillPaint.setStyle(Paint.Style.FILL);
521                        mFillPaint.setAntiAlias(true);
522                    }
523
524                    mFillPaint.setColor(vPath.mFillColor);
525                    canvas.drawPath(mRenderPath, mFillPaint);
526                }
527
528                if (vPath.mStrokeColor != 0) {
529                    if (mStrokePaint == null) {
530                        mStrokePaint = new Paint();
531                        mStrokePaint.setColorFilter(mColorFilter);
532                        mStrokePaint.setStyle(Paint.Style.STROKE);
533                        mStrokePaint.setAntiAlias(true);
534                    }
535
536                    final Paint strokePaint = mStrokePaint;
537                    if (vPath.mStrokeLineJoin != null) {
538                        strokePaint.setStrokeJoin(vPath.mStrokeLineJoin);
539                    }
540
541                    if (vPath.mStrokeLineCap != null) {
542                        strokePaint.setStrokeCap(vPath.mStrokeLineCap);
543                    }
544
545                    strokePaint.setStrokeMiter(vPath.mStrokeMiterlimit * scale);
546                    strokePaint.setColor(vPath.mStrokeColor);
547                    strokePaint.setStrokeWidth(vPath.mStrokeWidth * scale);
548                    canvas.drawPath(mRenderPath, strokePaint);
549                }
550            }
551        }
552
553        private void parseViewport(Resources r, AttributeSet attrs)
554                throws XmlPullParserException {
555            final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableViewport);
556            mViewportWidth = a.getFloat(R.styleable.VectorDrawableViewport_viewportWidth, mViewportWidth);
557            mViewportHeight = a.getFloat(R.styleable.VectorDrawableViewport_viewportHeight, mViewportHeight);
558
559            if (mViewportWidth <= 0) {
560                throw new XmlPullParserException(a.getPositionDescription() +
561                        "<viewport> tag requires viewportWidth > 0");
562            } else if (mViewportHeight <= 0) {
563                throw new XmlPullParserException(a.getPositionDescription() +
564                        "<viewport> tag requires viewportHeight > 0");
565            }
566
567            a.recycle();
568        }
569
570        private void parseSize(Resources r, AttributeSet attrs)
571                throws XmlPullParserException  {
572            final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableSize);
573            mBaseWidth = a.getDimension(R.styleable.VectorDrawableSize_width, mBaseWidth);
574            mBaseHeight = a.getDimension(R.styleable.VectorDrawableSize_height, mBaseHeight);
575
576            if (mBaseWidth <= 0) {
577                throw new XmlPullParserException(a.getPositionDescription() +
578                        "<size> tag requires width > 0");
579            } else if (mBaseHeight <= 0) {
580                throw new XmlPullParserException(a.getPositionDescription() +
581                        "<size> tag requires height > 0");
582            }
583
584            a.recycle();
585        }
586
587    }
588
589    private static class VGroup {
590        private final HashMap<String, VPath> mVGPathMap = new HashMap<String, VPath>();
591        private final ArrayList<VPath> mVGList = new ArrayList<VPath>();
592
593        private float mRotate = 0;
594        private float mPivotX = 0;
595        private float mPivotY = 0;
596        private float mScaleX = 1;
597        private float mScaleY = 1;
598        private float mTranslateX = 0;
599        private float mTranslateY = 0;
600
601        private int[] mThemeAttrs;
602
603        public void add(VPath path) {
604            String id = path.getID();
605            mVGPathMap.put(id, path);
606            mVGList.add(path);
607         }
608
609        public void applyTheme(Theme t) {
610            if (mThemeAttrs == null) {
611                return;
612            }
613
614            final TypedArray a = t.resolveAttributes(
615                    mThemeAttrs, R.styleable.VectorDrawablePath);
616
617            mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate);
618            mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX);
619            mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY);
620            mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX);
621            mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY);
622            mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX);
623            mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY);
624            a.recycle();
625        }
626
627        public void inflate(Resources res, AttributeSet attrs, Theme theme) {
628            final TypedArray a = obtainAttributes(res, theme, attrs, R.styleable.VectorDrawableGroup);
629            final int[] themeAttrs = a.extractThemeAttrs();
630
631            mThemeAttrs = themeAttrs;
632            // NOTE: The set of attributes loaded here MUST match the
633            // set of attributes loaded in applyTheme.
634
635            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_rotation] == 0) {
636                mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate);
637            }
638
639            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_pivotX] == 0) {
640                mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX);
641            }
642
643            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_pivotY] == 0) {
644                mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY);
645            }
646
647            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_scaleX] == 0) {
648                mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX);
649            }
650
651            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_scaleY] == 0) {
652                mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY);
653            }
654
655            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_translateX] == 0) {
656                mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX);
657            }
658
659            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_translateY] == 0) {
660                mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY);
661            }
662
663            a.recycle();
664        }
665
666        /**
667         * Must return in order of adding
668         * @return ordered list of paths
669         */
670        public ArrayList<VPath> getPaths() {
671            return mVGList;
672        }
673
674    }
675
676    private static class VPath {
677        private static final int MAX_STATES = 10;
678
679        private int[] mThemeAttrs;
680
681        int mStrokeColor = 0;
682        float mStrokeWidth = 0;
683        float mStrokeOpacity = Float.NaN;
684
685        int mFillColor = 0;
686        int mFillRule;
687        float mFillOpacity = Float.NaN;
688
689        float mTrimPathStart = 0;
690        float mTrimPathEnd = 1;
691        float mTrimPathOffset = 0;
692
693        boolean mClip = false;
694        Paint.Cap mStrokeLineCap = Paint.Cap.BUTT;
695        Paint.Join mStrokeLineJoin = Paint.Join.MITER;
696        float mStrokeMiterlimit = 4;
697
698        private VNode[] mNode = null;
699        private String mId;
700
701        public VPath() {
702            // Empty constructor.
703        }
704
705        public void toPath(Path path) {
706            path.reset();
707            if (mNode != null) {
708                VNode.createPath(mNode, path);
709            }
710        }
711
712        public String getID() {
713            return mId;
714        }
715
716        private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) {
717            switch (id) {
718                case LINECAP_BUTT:
719                    return Paint.Cap.BUTT;
720                case LINECAP_ROUND:
721                    return Paint.Cap.ROUND;
722                case LINECAP_SQUARE:
723                    return Paint.Cap.SQUARE;
724                default:
725                    return defValue;
726            }
727        }
728
729        private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) {
730            switch (id) {
731                case LINEJOIN_MITER:
732                    return Paint.Join.MITER;
733                case LINEJOIN_ROUND:
734                    return Paint.Join.ROUND;
735                case LINEJOIN_BEVEL:
736                    return Paint.Join.BEVEL;
737                default:
738                    return defValue;
739            }
740        }
741
742        public void inflate(Resources r, AttributeSet attrs, Theme theme) {
743            final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawablePath);
744            final int[] themeAttrs = a.extractThemeAttrs();
745            mThemeAttrs = themeAttrs;
746
747            // NOTE: The set of attributes loaded here MUST match the
748            // set of attributes loaded in applyTheme.
749            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_clipToPath] == 0) {
750                mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip);
751            }
752
753            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_name] == 0) {
754                mId = a.getString(R.styleable.VectorDrawablePath_name);
755            }
756
757            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pathData] == 0) {
758                mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData));
759            }
760
761            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fill] == 0) {
762                mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor);
763            }
764
765            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fillOpacity] == 0) {
766                mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity);
767            }
768
769            if (themeAttrs == null
770                    || themeAttrs[R.styleable.VectorDrawablePath_strokeLineCap] == 0) {
771                mStrokeLineCap = getStrokeLineCap(
772                        a.getInt(R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap);
773            }
774
775            if (themeAttrs == null
776                    || themeAttrs[R.styleable.VectorDrawablePath_strokeLineJoin] == 0) {
777                mStrokeLineJoin = getStrokeLineJoin(
778                        a.getInt(R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin);
779            }
780
781            if (themeAttrs == null
782                    || themeAttrs[R.styleable.VectorDrawablePath_strokeMiterLimit] == 0) {
783                mStrokeMiterlimit = a.getFloat(
784                        R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit);
785            }
786
787            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_stroke] == 0) {
788                mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor);
789            }
790
791            if (themeAttrs == null
792                    || themeAttrs[R.styleable.VectorDrawablePath_strokeOpacity] == 0) {
793                mStrokeOpacity = a.getFloat(
794                        R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity);
795            }
796
797            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_strokeWidth] == 0) {
798                mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth);
799            }
800
801            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_trimPathEnd] == 0) {
802                mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd);
803            }
804
805            if (themeAttrs == null
806                    || themeAttrs[R.styleable.VectorDrawablePath_trimPathOffset] == 0) {
807                mTrimPathOffset = a.getFloat(
808                        R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset);
809            }
810
811            if (themeAttrs == null
812                    || themeAttrs[R.styleable.VectorDrawablePath_trimPathStart] == 0) {
813                mTrimPathStart = a.getFloat(
814                        R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart);
815            }
816
817            updateColorAlphas();
818
819            a.recycle();
820        }
821
822        public boolean canApplyTheme() {
823            return mThemeAttrs != null;
824        }
825
826        public void applyTheme(Theme t) {
827            if (mThemeAttrs == null) {
828                return;
829            }
830
831            final TypedArray a = t.resolveAttributes(
832                    mThemeAttrs, R.styleable.VectorDrawablePath);
833
834            mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip);
835
836            if (a.hasValue(R.styleable.VectorDrawablePath_name)) {
837                mId = a.getString(R.styleable.VectorDrawablePath_name);
838            }
839
840            if (a.hasValue(R.styleable.VectorDrawablePath_pathData)) {
841                mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData));
842            }
843
844            mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor);
845            mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity);
846
847            mStrokeLineCap = getStrokeLineCap(a.getInt(
848                    R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap);
849            mStrokeLineJoin = getStrokeLineJoin(a.getInt(
850                    R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin);
851            mStrokeMiterlimit = a.getFloat(
852                    R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit);
853            mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor);
854            mStrokeOpacity = a.getFloat(
855                    R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity);
856            mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth);
857
858            mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd);
859            mTrimPathOffset = a.getFloat(
860                    R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset);
861            mTrimPathStart = a.getFloat(
862                    R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart);
863
864            updateColorAlphas();
865            a.recycle();
866        }
867
868        private void updateColorAlphas() {
869            if (!Float.isNaN(mFillOpacity)) {
870                mFillColor &= 0x00FFFFFF;
871                mFillColor |= ((int) (0xFF * mFillOpacity)) << 24;
872            }
873
874            if (!Float.isNaN(mStrokeOpacity)) {
875                mStrokeColor &= 0x00FFFFFF;
876                mStrokeColor |= ((int) (0xFF * mStrokeOpacity)) << 24;
877            }
878        }
879
880        private static int nextStart(String s, int end) {
881            char c;
882
883            while (end < s.length()) {
884                c = s.charAt(end);
885                if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) {
886                    return end;
887                }
888                end++;
889            }
890            return end;
891        }
892
893        private void addNode(ArrayList<VectorDrawable.VNode> list, char cmd, float[] val) {
894            list.add(new VectorDrawable.VNode(cmd, val));
895        }
896
897        /**
898         * parse the floats in the string
899         * this is an optimized version of
900         * parseFloat(s.split(",|\\s"));
901         *
902         * @param s the string containing a command and list of floats
903         * @return array of floats
904         */
905        private static float[] getFloats(String s) {
906            if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') {
907                return new float[0];
908            }
909            try {
910                float[] tmp = new float[s.length()];
911                int count = 0;
912                int pos = 1, end;
913                while ((end = extract(s, pos)) >= 0) {
914                    if (pos < end) {
915                        tmp[count++] = Float.parseFloat(s.substring(pos, end));
916                    }
917                    pos = end + 1;
918                }
919                // handle the final float if there is one
920                if (pos < s.length()) {
921                    tmp[count++] = Float.parseFloat(s.substring(pos, s.length()));
922                }
923                return Arrays.copyOf(tmp, count);
924            } catch (NumberFormatException e){
925                Log.e(LOGTAG,"error in parsing \""+s+"\"");
926                throw e;
927            }
928        }
929
930        /**
931         * calculate the position of the next comma or space
932         * @param s the string to search
933         * @param start the position to start searching
934         * @return the position of the next comma or space or -1 if none found
935         */
936        private static int extract(String s, int start) {
937            int space = s.indexOf(' ', start);
938            int comma = s.indexOf(',', start);
939            if (space == -1) {
940                return comma;
941            }
942            if (comma == -1) {
943                return space;
944            }
945            return (comma > space) ? space : comma;
946        }
947
948        private VectorDrawable.VNode[] parsePath(String value) {
949            int start = 0;
950            int end = 1;
951
952            ArrayList<VectorDrawable.VNode> list = new ArrayList<VectorDrawable.VNode>();
953            while (end < value.length()) {
954                end = nextStart(value, end);
955                String s = value.substring(start, end);
956                float[] val = getFloats(s);
957                addNode(list, s.charAt(0), val);
958
959                start = end;
960                end++;
961            }
962            if ((end - start) == 1 && start < value.length()) {
963
964                addNode(list, value.charAt(start), new float[0]);
965            }
966            return list.toArray(new VectorDrawable.VNode[list.size()]);
967        }
968    }
969
970    private static class VNode {
971        private char mType;
972        private float[] mParams;
973
974        public VNode(char type, float[] params) {
975            mType = type;
976            mParams = params;
977        }
978
979        public VNode(VNode n) {
980            mType = n.mType;
981            mParams = Arrays.copyOf(n.mParams, n.mParams.length);
982        }
983
984        public static void createPath(VNode[] node, Path path) {
985            float[] current = new float[4];
986            char previousCommand = 'm';
987            for (int i = 0; i < node.length; i++) {
988                addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
989                previousCommand = node[i].mType;
990            }
991        }
992
993        private static void addCommand(Path path, float[] current,
994                char previousCmd, char cmd, float[] val) {
995
996            int incr = 2;
997            float currentX = current[0];
998            float currentY = current[1];
999            float ctrlPointX = current[2];
1000            float ctrlPointY = current[3];
1001            float reflectiveCtrlPointX;
1002            float reflectiveCtrlPointY;
1003
1004            switch (cmd) {
1005                case 'z':
1006                case 'Z':
1007                    path.close();
1008                    return;
1009                case 'm':
1010                case 'M':
1011                case 'l':
1012                case 'L':
1013                case 't':
1014                case 'T':
1015                    incr = 2;
1016                    break;
1017                case 'h':
1018                case 'H':
1019                case 'v':
1020                case 'V':
1021                    incr = 1;
1022                    break;
1023                case 'c':
1024                case 'C':
1025                    incr = 6;
1026                    break;
1027                case 's':
1028                case 'S':
1029                case 'q':
1030                case 'Q':
1031                    incr = 4;
1032                    break;
1033                case 'a':
1034                case 'A':
1035                    incr = 7;
1036                    break;
1037            }
1038            for (int k = 0; k < val.length; k += incr) {
1039                switch (cmd) {
1040                    case 'm': // moveto - Start a new sub-path (relative)
1041                        path.rMoveTo(val[k + 0], val[k + 1]);
1042                        currentX += val[k + 0];
1043                        currentY += val[k + 1];
1044                        break;
1045                    case 'M': // moveto - Start a new sub-path
1046                        path.moveTo(val[k + 0], val[k + 1]);
1047                        currentX = val[k + 0];
1048                        currentY = val[k + 1];
1049                        break;
1050                    case 'l': // lineto - Draw a line from the current point (relative)
1051                        path.rLineTo(val[k + 0], val[k + 1]);
1052                        currentX += val[k + 0];
1053                        currentY += val[k + 1];
1054                        break;
1055                    case 'L': // lineto - Draw a line from the current point
1056                        path.lineTo(val[k + 0], val[k + 1]);
1057                        currentX = val[k + 0];
1058                        currentY = val[k + 1];
1059                        break;
1060                    case 'z': // closepath - Close the current subpath
1061                    case 'Z': // closepath - Close the current subpath
1062                        path.close();
1063                        break;
1064                    case 'h': // horizontal lineto - Draws a horizontal line (relative)
1065                        path.rLineTo(val[k + 0], 0);
1066                        currentX += val[k + 0];
1067                        break;
1068                    case 'H': // horizontal lineto - Draws a horizontal line
1069                        path.lineTo(val[k + 0], currentY);
1070                        currentX = val[k + 0];
1071                        break;
1072                    case 'v': // vertical lineto - Draws a vertical line from the current point (r)
1073                        path.rLineTo(0, val[k + 0]);
1074                        currentY += val[k + 0];
1075                        break;
1076                    case 'V': // vertical lineto - Draws a vertical line from the current point
1077                        path.lineTo(currentX, val[k + 0]);
1078                        currentY = val[k + 0];
1079                        break;
1080                    case 'c': // curveto - Draws a cubic Bézier curve (relative)
1081                        path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
1082                                val[k + 4], val[k + 5]);
1083
1084                        ctrlPointX = currentX + val[k + 2];
1085                        ctrlPointY = currentY + val[k + 3];
1086                        currentX += val[k + 4];
1087                        currentY += val[k + 5];
1088
1089                        break;
1090                    case 'C': // curveto - Draws a cubic Bézier curve
1091                        path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
1092                                val[k + 4], val[k + 5]);
1093                        currentX = val[k + 4];
1094                        currentY = val[k + 5];
1095                        ctrlPointX = val[k + 2];
1096                        ctrlPointY = val[k + 3];
1097                        break;
1098                    case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
1099                        reflectiveCtrlPointX = 0;
1100                        reflectiveCtrlPointY = 0;
1101                        if (previousCmd == 'c' || previousCmd == 's'
1102                                || previousCmd == 'C' || previousCmd == 'S') {
1103                            reflectiveCtrlPointX = currentX - ctrlPointX;
1104                            reflectiveCtrlPointY = currentY - ctrlPointY;
1105                        }
1106                        path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
1107                                val[k + 0], val[k + 1],
1108                                val[k + 2], val[k + 3]);
1109
1110                        ctrlPointX = currentX + val[k + 0];
1111                        ctrlPointY = currentY + val[k + 1];
1112                        currentX += val[k + 2];
1113                        currentY += val[k + 3];
1114                        break;
1115                    case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
1116                        reflectiveCtrlPointX = currentX;
1117                        reflectiveCtrlPointY = currentY;
1118                        if (previousCmd == 'c' || previousCmd == 's'
1119                                || previousCmd == 'C' || previousCmd == 'S') {
1120                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
1121                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
1122                        }
1123                        path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
1124                                val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
1125                        ctrlPointX = val[k + 0];
1126                        ctrlPointY = val[k + 1];
1127                        currentX = val[k + 2];
1128                        currentY = val[k + 3];
1129                        break;
1130                    case 'q': // Draws a quadratic Bézier (relative)
1131                        path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
1132                        ctrlPointX = currentX + val[k + 0];
1133                        ctrlPointY = currentY + val[k + 1];
1134                        currentX += val[k + 2];
1135                        currentY += val[k + 3];
1136                        break;
1137                    case 'Q': // Draws a quadratic Bézier
1138                        path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
1139                        ctrlPointX = val[k + 0];
1140                        ctrlPointY = val[k + 1];
1141                        currentX = val[k + 2];
1142                        currentY = val[k + 3];
1143                        break;
1144                    case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
1145                        reflectiveCtrlPointX = 0;
1146                        reflectiveCtrlPointY = 0;
1147                        if (previousCmd == 'q' || previousCmd == 't'
1148                                || previousCmd == 'Q' || previousCmd == 'T') {
1149                            reflectiveCtrlPointX = currentX - ctrlPointX;
1150                            reflectiveCtrlPointY = currentY - ctrlPointY;
1151                        }
1152                        path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
1153                                val[k + 0], val[k + 1]);
1154                        ctrlPointX = currentX + reflectiveCtrlPointX;
1155                        ctrlPointY = currentY + reflectiveCtrlPointY;
1156                        currentX += val[k + 0];
1157                        currentY += val[k + 1];
1158                        break;
1159                    case 'T': // Draws a quadratic Bézier curve (reflective control point)
1160                        reflectiveCtrlPointX = currentX;
1161                        reflectiveCtrlPointY = currentY;
1162                        if (previousCmd == 'q' || previousCmd == 't'
1163                                || previousCmd == 'Q' || previousCmd == 'T') {
1164                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
1165                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
1166                        }
1167                        path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
1168                                val[k + 0], val[k + 1]);
1169                        ctrlPointX = reflectiveCtrlPointX;
1170                        ctrlPointY = reflectiveCtrlPointY;
1171                        currentX = val[k + 0];
1172                        currentY = val[k + 1];
1173                        break;
1174                    case 'a': // Draws an elliptical arc
1175                        // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
1176                        drawArc(path,
1177                                currentX,
1178                                currentY,
1179                                val[k + 5] + currentX,
1180                                val[k + 6] + currentY,
1181                                val[k + 0],
1182                                val[k + 1],
1183                                val[k + 2],
1184                                val[k + 3] != 0,
1185                                val[k + 4] != 0);
1186                        currentX += val[k + 5];
1187                        currentY += val[k + 6];
1188                        ctrlPointX = currentX;
1189                        ctrlPointY = currentY;
1190                        break;
1191                    case 'A': // Draws an elliptical arc
1192                        drawArc(path,
1193                                currentX,
1194                                currentY,
1195                                val[k + 5],
1196                                val[k + 6],
1197                                val[k + 0],
1198                                val[k + 1],
1199                                val[k + 2],
1200                                val[k + 3] != 0,
1201                                val[k + 4] != 0);
1202                        currentX = val[k + 5];
1203                        currentY = val[k + 6];
1204                        ctrlPointX = currentX;
1205                        ctrlPointY = currentY;
1206                        break;
1207                }
1208                previousCmd = cmd;
1209            }
1210            current[0] = currentX;
1211            current[1] = currentY;
1212            current[2] = ctrlPointX;
1213            current[3] = ctrlPointY;
1214        }
1215
1216        private static void drawArc(Path p,
1217                float x0,
1218                float y0,
1219                float x1,
1220                float y1,
1221                float a,
1222                float b,
1223                float theta,
1224                boolean isMoreThanHalf,
1225                boolean isPositiveArc) {
1226
1227            /* Convert rotation angle from degrees to radians */
1228            double thetaD = Math.toRadians(theta);
1229            /* Pre-compute rotation matrix entries */
1230            double cosTheta = Math.cos(thetaD);
1231            double sinTheta = Math.sin(thetaD);
1232            /* Transform (x0, y0) and (x1, y1) into unit space */
1233            /* using (inverse) rotation, followed by (inverse) scale */
1234            double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
1235            double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
1236            double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
1237            double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
1238
1239            /* Compute differences and averages */
1240            double dx = x0p - x1p;
1241            double dy = y0p - y1p;
1242            double xm = (x0p + x1p) / 2;
1243            double ym = (y0p + y1p) / 2;
1244            /* Solve for intersecting unit circles */
1245            double dsq = dx * dx + dy * dy;
1246            if (dsq == 0.0) {
1247                Log.w(LOGTAG, " Points are coincident");
1248                return; /* Points are coincident */
1249            }
1250            double disc = 1.0 / dsq - 1.0 / 4.0;
1251            if (disc < 0.0) {
1252                Log.w(LOGTAG, "Points are too far apart " + dsq);
1253                float adjust = (float) (Math.sqrt(dsq) / 1.99999);
1254                drawArc(p, x0, y0, x1, y1, a * adjust,
1255                        b * adjust, theta, isMoreThanHalf, isPositiveArc);
1256                return; /* Points are too far apart */
1257            }
1258            double s = Math.sqrt(disc);
1259            double sdx = s * dx;
1260            double sdy = s * dy;
1261            double cx;
1262            double cy;
1263            if (isMoreThanHalf == isPositiveArc) {
1264                cx = xm - sdy;
1265                cy = ym + sdx;
1266            } else {
1267                cx = xm + sdy;
1268                cy = ym - sdx;
1269            }
1270
1271            double eta0 = Math.atan2((y0p - cy), (x0p - cx));
1272
1273            double eta1 = Math.atan2((y1p - cy), (x1p - cx));
1274
1275            double sweep = (eta1 - eta0);
1276            if (isPositiveArc != (sweep >= 0)) {
1277                if (sweep > 0) {
1278                    sweep -= 2 * Math.PI;
1279                } else {
1280                    sweep += 2 * Math.PI;
1281                }
1282            }
1283
1284            cx *= a;
1285            cy *= b;
1286            double tcx = cx;
1287            cx = cx * cosTheta - cy * sinTheta;
1288            cy = tcx * sinTheta + cy * cosTheta;
1289
1290            arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
1291        }
1292
1293        /**
1294         * Converts an arc to cubic Bezier segments and records them in p.
1295         *
1296         * @param p The target for the cubic Bezier segments
1297         * @param cx The x coordinate center of the ellipse
1298         * @param cy The y coordinate center of the ellipse
1299         * @param a The radius of the ellipse in the horizontal direction
1300         * @param b The radius of the ellipse in the vertical direction
1301         * @param e1x E(eta1) x coordinate of the starting point of the arc
1302         * @param e1y E(eta2) y coordinate of the starting point of the arc
1303         * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
1304         * @param start The start angle of the arc on the ellipse
1305         * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
1306         */
1307        private static void arcToBezier(Path p,
1308                double cx,
1309                double cy,
1310                double a,
1311                double b,
1312                double e1x,
1313                double e1y,
1314                double theta,
1315                double start,
1316                double sweep) {
1317            // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
1318            // and http://www.spaceroots.org/documents/ellipse/node22.html
1319
1320            // Maximum of 45 degrees per cubic Bezier segment
1321            int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI));
1322
1323            double eta1 = start;
1324            double cosTheta = Math.cos(theta);
1325            double sinTheta = Math.sin(theta);
1326            double cosEta1 = Math.cos(eta1);
1327            double sinEta1 = Math.sin(eta1);
1328            double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
1329            double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
1330
1331            double anglePerSegment = sweep / numSegments;
1332            for (int i = 0; i < numSegments; i++) {
1333                double eta2 = eta1 + anglePerSegment;
1334                double sinEta2 = Math.sin(eta2);
1335                double cosEta2 = Math.cos(eta2);
1336                double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
1337                double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
1338                double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
1339                double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
1340                double tanDiff2 = Math.tan((eta2 - eta1) / 2);
1341                double alpha =
1342                        Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
1343                double q1x = e1x + alpha * ep1x;
1344                double q1y = e1y + alpha * ep1y;
1345                double q2x = e2x - alpha * ep2x;
1346                double q2y = e2y - alpha * ep2y;
1347
1348                p.cubicTo((float) q1x,
1349                        (float) q1y,
1350                        (float) q2x,
1351                        (float) q2y,
1352                        (float) e2x,
1353                        (float) e2y);
1354                eta1 = eta2;
1355                e1x = e2x;
1356                e1y = e2y;
1357                ep1x = ep2x;
1358                ep1y = ep2y;
1359            }
1360        }
1361
1362    }
1363}
1364