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