VectorDrawable.java revision 70802d2501dd5607ca0f38d7aa36771d9e0c7b20
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.animation.ObjectAnimator;
18import android.animation.ValueAnimator;
19import android.content.res.Resources;
20import android.content.res.Resources.Theme;
21import android.content.res.TypedArray;
22import android.graphics.Canvas;
23import android.graphics.ColorFilter;
24import android.graphics.Matrix;
25import android.graphics.Paint;
26import android.graphics.Path;
27import android.graphics.PathMeasure;
28import android.graphics.PixelFormat;
29import android.graphics.Rect;
30import android.graphics.Region;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.util.Xml;
34import android.view.animation.AccelerateDecelerateInterpolator;
35import android.view.animation.Interpolator;
36import android.view.animation.LinearInterpolator;
37
38import com.android.internal.R;
39
40import org.xmlpull.v1.XmlPullParser;
41import org.xmlpull.v1.XmlPullParserException;
42import org.xmlpull.v1.XmlPullParserFactory;
43
44import java.io.IOException;
45import java.util.ArrayList;
46import java.util.Arrays;
47import java.util.Collection;
48import java.util.HashMap;
49import java.util.HashSet;
50
51/**
52 * This lets you create a drawable based on an XML vector graphic It can be
53 * defined in an XML file with the <code>&lt;vector></code> element.
54 * <p/>
55 * The vector drawable has 6 elements:
56 * <p/>
57 * <dl>
58 * <dt><code>&lt;vector></code></dt>
59 * <dd>The attribute <code>android:trigger</code> defines a state change that
60 * will drive the animation</dd>
61 * <dd>The attribute <code>android:versionCode</code> defines the version of
62 * VectorDrawable</dd>
63 * <dt><code>&lt;size></code></dt>
64 * <dd>Used to defined the intrinsic Width Height size of the drawable using
65 * <code>android:width</code> and <code>android:height</code></dd>
66 * <dt><code>&lt;viewport></code></dt>
67 * <dd>Used to defined the size of the virtual canvas the paths are drawn on.
68 * The size is defined using the attributes <code>android:viewportHeight</code>
69 * <code>android:viewportWidth</code></dd>
70 * <dt><code>&lt;group></code></dt>
71 * <dd>Defines a "key frame" in the animation if there is only one group the
72 * drawable is static 2D image that has no animation.</dd>
73 * <dt><code>&lt;path></code></dt>
74 * <dd>Defines paths to be drawn. The path elements must be within a group
75 * <dl>
76 * <dt><code>android:name</code>
77 * <dd>Defines the name of the path.</dd></dt>
78 * <dt><code>android:pathData</code>
79 * <dd>Defines path string.</dd></dt>
80 * <dt><code>android:fill</code>
81 * <dd>Defines the color to fill the path (none if not present).</dd></dt>
82 * <dt><code>android:stroke</code>
83 * <dd>Defines the color to draw the path outline (none if not present).</dd>
84 * </dt>
85 * <dt><code>android:strokeWidth</code>
86 * <dd>The width a path stroke</dd></dt>
87 * <dt><code>android:strokeOpacity</code>
88 * <dd>The opacity of a path stroke</dd></dt>
89 * <dt><code>android:rotation</code>
90 * <dd>The amount to rotation the path stroke.</dd></dt>
91 * <dt><code>android:pivotX</code>
92 * <dd>The X coordinate of the center of rotation of a path</dd></dt>
93 * <dt><code>android:pivotY</code>
94 * <dd>The Y coordinate of the center of rotation of a path</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 * <dt><code>android:state_pressed</code>
113 * <dd>Sets a condition to be met to draw path</dd></dt>
114 * <dt><code>android:state_focused</code>
115 * <dd>Sets a condition to be met to draw path</dd></dt>
116 * <dt><code>android:state_selected</code>
117 * <dd>Sets a condition to be met to draw path</dd></dt>
118 * <dt><code>android:state_window_focused</code>
119 * <dd>Sets a condition to be met to draw path</dd></dt>
120 * <dt><code>android:state_enabled</code>
121 * <dd>Sets a condition to be met to draw path</dd></dt>
122 * <dt><code>android:state_activated</code>
123 * <dd>Sets a condition to be met to draw path</dd></dt>
124 * <dt><code>android:state_accelerated</code>
125 * <dd>Sets a condition to be met to draw path</dd></dt>
126 * <dt><code>android:state_hovered</code>
127 * <dd>Sets a condition to be met to draw path</dd></dt>
128 * <dt><code>android:state_checked</code>
129 * <dd>Sets a condition to be met to draw path</dd></dt>
130 * <dt><code>android:state_checkable</code>
131 * <dd>Sets a condition to be met to draw path</dd></dt>
132 * </dl>
133 * </dd>
134 * <dt><code>&lt;animation></code></dt>
135 * <dd>Used to customize the transition between two paths
136 * <dl>
137 * <dt><code>android:sequence</code>
138 * <dd>Configures this animation sequence between the named paths.</dd></dt>
139 * <dt><code>android:limitTo</code>
140 * <dd>Limits an animation to only interpolate the selected variable unlimited,
141 * path, rotation, trimPathStart, trimPathEnd, trimPathOffset</dd></dt>
142 * <dt><code>android:repeatCount</code>
143 * <dd>Number of times to loop this aspect of the animation</dd></dt>
144 * <dt><code>android:durations</code>
145 * <dd>The duration of each step in the animation in milliseconds Must contain
146 * the number of named paths - 1</dd></dt>
147 * <dt><code>android:startDelay</code>
148 * <dd></dd></dt>
149 * <dt><code>android:repeatStyle</code>
150 * <dd>when repeating how does it repeat back and forth or a to b: forward,
151 * inAndOut</dd></dt>
152 * <dt><code>android:animate</code>
153 * <dd>linear, accelerate, decelerate, easing</dd></dt>
154 * </dl>
155 * </dd>
156 */
157public class VectorDrawable extends Drawable {
158    private static final String LOGTAG = VectorDrawable.class.getSimpleName();
159
160    private static final String SHAPE_SIZE = "size";
161    private static final String SHAPE_VIEWPORT = "viewport";
162    private static final String SHAPE_GROUP = "group";
163    private static final String SHAPE_PATH = "path";
164    private static final String SHAPE_TRANSITION = "transition";
165    private static final String SHAPE_ANIMATION = "animation";
166    private static final String SHAPE_VECTOR = "vector";
167
168    private static final int LINECAP_BUTT = 0;
169    private static final int LINECAP_ROUND = 1;
170    private static final int LINECAP_SQUARE = 2;
171
172    private static final int LINEJOIN_MITER = 0;
173    private static final int LINEJOIN_ROUND = 1;
174    private static final int LINEJOIN_BEVEL = 2;
175
176    private static final int DEFAULT_DURATION = 1000;
177    private static final long DEFAULT_INFINITE_DURATION = 60 * 60 * 1000;
178
179    private final VectorDrawableState mVectorState;
180
181    private int mAlpha = 0xFF;
182
183    public VectorDrawable() {
184        mVectorState = new VectorDrawableState(null);
185        mVectorState.mBasicAnimator = ObjectAnimator.ofFloat(this, "AnimationFraction", 0, 0);
186
187        setDuration(DEFAULT_DURATION);
188    }
189
190    private VectorDrawable(VectorDrawableState state, Resources res, Theme theme) {
191        mVectorState = new VectorDrawableState(state);
192        mVectorState.mBasicAnimator = ObjectAnimator.ofFloat(this, "AnimationFraction", 0, 0);
193
194        if (theme != null && canApplyTheme()) {
195            applyTheme(theme);
196        }
197
198        long duration = mVectorState.mVAnimatedPath.getTotalAnimationDuration();
199        if (duration == -1) {
200            // If duration is infinite, set to 1 hour.
201            // TODO: Define correct approach for infinite.
202            duration = DEFAULT_INFINITE_DURATION;
203            mVectorState.mBasicAnimator.setFloatValues(0, duration / 1000);
204            mVectorState.mBasicAnimator.setInterpolator(new LinearInterpolator());
205        }
206        setDuration(duration);
207    }
208
209    @Override
210    public ConstantState getConstantState() {
211        return mVectorState;
212    }
213
214    @Override
215    public void jumpToCurrentState() {
216        stop();
217    }
218
219    /**
220     * Starts the animation.
221     */
222    public void start() {
223        mVectorState.mBasicAnimator.start();
224    }
225
226    /**
227     * Stops the animation and moves to the end state.
228     */
229    public void stop() {
230        mVectorState.mBasicAnimator.end();
231    }
232
233    /**
234     * Returns the current completion value for the animation.
235     *
236     * @return the current point on the animation, typically between 0 and 1
237     */
238    public float geAnimationFraction() {
239        return mVectorState.mVAnimatedPath.getValue();
240    }
241
242    /**
243     * Set the current completion value for the animation.
244     *
245     * @param value the point along the animation, typically between 0 and 1
246     */
247    public void setAnimationFraction(float value) {
248        mVectorState.mVAnimatedPath.setAnimationFraction(value);
249        invalidateSelf();
250    }
251
252    /**
253     * set the amount of time the animation will take
254     *
255     * @param duration amount of time in milliseconds
256     */
257    public void setDuration(long duration) {
258        mVectorState.mBasicAnimator.setDuration(duration);
259    }
260
261    /**
262     * Defines what this animation should do when it reaches the end. This
263     * setting is applied only when the repeat count is either greater than 0 or
264     * {@link ValueAnimator#INFINITE}.
265     *
266     * @param mode the animation mode, either {@link ValueAnimator#RESTART} or
267     *            {@link ValueAnimator#REVERSE}
268     */
269    public void setRepeatMode(int mode) {
270        mVectorState.mBasicAnimator.setRepeatMode(mode);
271    }
272
273    /**
274     * Sets animation to repeat
275     *
276     * @param repeat True if this drawable repeats its animation
277     */
278    public void setRepeatCount(int repeat) {
279        mVectorState.mBasicAnimator.setRepeatCount(repeat);
280    }
281
282    /**
283     * @return the animation repeat count, either a value greater than 0 or
284     *         {@link ValueAnimator#INFINITE}
285     */
286    public int getRepeatCount() {
287        return mVectorState.mBasicAnimator.getRepeatCount();
288    }
289
290    @Override
291    public boolean isStateful() {
292        return true;
293    }
294
295    @Override
296    protected boolean onStateChange(int[] state) {
297        super.onStateChange(state);
298
299        mVectorState.mVAnimatedPath.setState(state);
300
301        final int direction = mVectorState.mVAnimatedPath.getTrigger(state);
302        if (direction > 0) {
303            animateForward();
304        } else if (direction < 0) {
305            animateBackward();
306        }
307
308        invalidateSelf();
309        return true;
310    }
311
312    private void animateForward() {
313        if (!mVectorState.mBasicAnimator.isStarted()) {
314            mVectorState.mBasicAnimator.setFloatValues(0, 1);
315            start();
316        }
317    }
318
319    private void animateBackward() {
320        if (!mVectorState.mBasicAnimator.isStarted()) {
321            mVectorState.mBasicAnimator.setFloatValues(1, 0);
322            start();
323        }
324    }
325
326    @Override
327    public void draw(Canvas canvas) {
328        final int saveCount = canvas.save();
329        final Rect bounds = getBounds();
330        canvas.translate(bounds.left, bounds.top);
331        mVectorState.mVAnimatedPath.draw(canvas, bounds.width(), bounds.height());
332        canvas.restoreToCount(saveCount);
333    }
334
335    @Override
336    public void setAlpha(int alpha) {
337        // TODO correct handling of transparent
338        if (mAlpha != alpha) {
339            mAlpha = alpha;
340            invalidateSelf();
341        }
342    }
343
344    @Override
345    public void setColorFilter(ColorFilter colorFilter) {
346        // TODO: support color filter
347    }
348
349    @Override
350    public int getOpacity() {
351        return PixelFormat.TRANSLUCENT;
352    }
353
354    /**
355     * Sets padding for this shape, defined by a Rect object. Define the padding
356     * in the Rect object as: left, top, right, bottom.
357     */
358    public void setPadding(Rect padding) {
359        setPadding(padding.left, padding.top, padding.right, padding.bottom);
360    }
361
362    /**
363     * Sets padding for the shape.
364     *
365     * @param left padding for the left side (in pixels)
366     * @param top padding for the top (in pixels)
367     * @param right padding for the right side (in pixels)
368     * @param bottom padding for the bottom (in pixels)
369     */
370    public void setPadding(int left, int top, int right, int bottom) {
371        if ((left | top | right | bottom) == 0) {
372            mVectorState.mPadding = null;
373        } else {
374            if (mVectorState.mPadding == null) {
375                mVectorState.mPadding = new Rect();
376            }
377            mVectorState.mPadding.set(left, top, right, bottom);
378        }
379        invalidateSelf();
380    }
381
382    @Override
383    public int getIntrinsicWidth() {
384        return (int) mVectorState.mVAnimatedPath.mBaseWidth;
385    }
386
387    @Override
388    public int getIntrinsicHeight() {
389        return (int) mVectorState.mVAnimatedPath.mBaseHeight;
390    }
391
392    @Override
393    public boolean getPadding(Rect padding) {
394        if (mVectorState.mPadding != null) {
395            padding.set(mVectorState.mPadding);
396            return true;
397        } else {
398            return super.getPadding(padding);
399        }
400    }
401
402    @Override
403    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
404            throws XmlPullParserException, IOException {
405        final VAnimatedPath p = inflateInternal(res, parser, attrs, theme);
406        setAnimatedPath(p);
407    }
408
409    @Override
410    public boolean canApplyTheme() {
411        return super.canApplyTheme() || mVectorState != null && mVectorState.canApplyTheme();
412    }
413
414    @Override
415    public void applyTheme(Theme t) {
416        super.applyTheme(t);
417
418        final VectorDrawableState state = mVectorState;
419        final VAnimatedPath path = state.mVAnimatedPath;
420        if (path != null && path.canApplyTheme()) {
421            path.applyTheme(t);
422        }
423    }
424
425    /** @hide */
426    public static VectorDrawable create(Resources resources, int rid) {
427        try {
428            final XmlPullParser xpp = resources.getXml(rid);
429            final AttributeSet attrs = Xml.asAttributeSet(xpp);
430            final XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
431            factory.setNamespaceAware(true);
432
433            final VectorDrawable drawable = new VectorDrawable();
434            drawable.inflate(resources, xpp, attrs);
435            drawable.setAnimationFraction(0);
436
437            return drawable;
438        } catch (XmlPullParserException e) {
439            Log.e(LOGTAG, "parser error", e);
440        } catch (IOException e) {
441            Log.e(LOGTAG, "parser error", e);
442        }
443        return null;
444    }
445
446    private VAnimatedPath inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs,
447            Theme theme) throws XmlPullParserException, IOException {
448        final VAnimatedPath animatedPath = new VAnimatedPath();
449
450        boolean noSizeTag = true;
451        boolean noViewportTag = true;
452        boolean noGroupTag = true;
453        boolean noPathTag = true;
454
455        VGroup currentGroup = null;
456
457        int eventType = parser.getEventType();
458        while (eventType != XmlPullParser.END_DOCUMENT) {
459            if (eventType == XmlPullParser.START_TAG) {
460                final String tagName = parser.getName();
461                if (SHAPE_PATH.equals(tagName)) {
462                    final VPath path = new VPath();
463                    path.inflate(res, attrs, theme);
464                    currentGroup.add(path);
465                    noPathTag = false;
466                } else if (SHAPE_ANIMATION.equals(tagName)) {
467                    final VAnimation anim = new VAnimation();
468                    anim.inflate(animatedPath.mGroupList, res, attrs, theme);
469                    animatedPath.addAnimation(anim);
470                } else if (SHAPE_SIZE.equals(tagName)) {
471                    animatedPath.parseSize(res, attrs);
472                    noSizeTag = false;
473                } else if (SHAPE_VIEWPORT.equals(tagName)) {
474                    animatedPath.parseViewport(res, attrs);
475                    noViewportTag = false;
476                } else if (SHAPE_GROUP.equals(tagName)) {
477                    currentGroup = new VGroup();
478                    animatedPath.mGroupList.add(currentGroup);
479                    noGroupTag = false;
480                } else if (SHAPE_VECTOR.equals(tagName)) {
481                    final TypedArray a = res.obtainAttributes(attrs, R.styleable.VectorDrawable);
482                    animatedPath.setTrigger(a.getInteger(R.styleable.VectorDrawable_trigger, 0));
483
484                    // Parsing the version information.
485                    // Right now, we only support version "1".
486                    // If the xml didn't specify the version number, the default
487                    // version is "1".
488                    final int versionCode = a.getInt(R.styleable.VectorDrawable_versionCode, 1);
489                    if (versionCode != 1) {
490                        throw new IllegalArgumentException(
491                                "So far, VectorDrawable only support version 1");
492                    }
493
494                    a.recycle();
495                }
496            }
497
498            eventType = parser.next();
499        }
500
501        if (noSizeTag || noViewportTag || noGroupTag || noPathTag) {
502            final StringBuffer tag = new StringBuffer();
503
504            if (noSizeTag) {
505                tag.append(SHAPE_SIZE);
506            }
507
508            if (noViewportTag) {
509                if (tag.length() > 0) {
510                    tag.append(" & ");
511                }
512                tag.append(SHAPE_SIZE);
513            }
514
515            if (noGroupTag) {
516                if (tag.length() > 0) {
517                    tag.append(" & ");
518                }
519                tag.append(SHAPE_GROUP);
520            }
521
522            if (noPathTag) {
523                if (tag.length() > 0) {
524                    tag.append(" or ");
525                }
526                tag.append(SHAPE_PATH);
527            }
528
529            throw new XmlPullParserException("no " + tag + " defined");
530        }
531
532        // post parse cleanup
533        animatedPath.parseFinish();
534        return animatedPath;
535    }
536
537    private void setAnimatedPath(VAnimatedPath animatedPath) {
538        mVectorState.mVAnimatedPath = animatedPath;
539
540        long duration = mVectorState.mVAnimatedPath.getTotalAnimationDuration();
541        if (duration == -1) { // if it set to infinite set to 1 hour
542            duration = DEFAULT_INFINITE_DURATION; // TODO define correct
543                                                  // approach for infinite
544            mVectorState.mBasicAnimator.setFloatValues(0, duration / 1000);
545            mVectorState.mBasicAnimator.setInterpolator(new LinearInterpolator());
546        }
547
548        setDuration(duration);
549        setAnimationFraction(0);
550    }
551
552    @Override
553    public boolean setVisible(boolean visible, boolean restart) {
554        boolean changed = super.setVisible(visible, restart);
555        if (visible) {
556            if (changed || restart) {
557                setAnimationFraction(0);
558            }
559        } else {
560            stop();
561        }
562        return changed;
563    }
564
565    private static class VectorDrawableState extends ConstantState {
566        int mChangingConfigurations;
567        ValueAnimator mBasicAnimator;
568        VAnimatedPath mVAnimatedPath;
569        Rect mPadding;
570
571        public VectorDrawableState(VectorDrawableState copy) {
572            if (copy != null) {
573                mChangingConfigurations = copy.mChangingConfigurations;
574                mVAnimatedPath = new VAnimatedPath(copy.mVAnimatedPath);
575                mPadding = new Rect(copy.mPadding);
576            }
577        }
578
579        @Override
580        public Drawable newDrawable() {
581            return new VectorDrawable(this, null, null);
582        }
583
584        @Override
585        public Drawable newDrawable(Resources res) {
586            return new VectorDrawable(this, res, null);
587        }
588
589        @Override
590        public Drawable newDrawable(Resources res, Theme theme) {
591            return new VectorDrawable(this, res, theme);
592        }
593
594        @Override
595        public int getChangingConfigurations() {
596            return mChangingConfigurations;
597        }
598    }
599
600    private static class VAnimatedPath {
601        private static final int [] TRIGGER_MAP = {
602                0,
603                R.attr.state_pressed,
604                R.attr.state_focused,
605                R.attr.state_hovered,
606                R.attr.state_selected,
607                R.attr.state_checkable,
608                R.attr.state_checked,
609                R.attr.state_activated,
610                R.attr.state_focused
611        };
612
613        private final Path mPath = new Path();
614        private final Path mRenderPath = new Path();
615        private final Matrix mMatrix = new Matrix();
616
617        private ArrayList<VAnimation> mCurrentAnimList;
618        private VPath[] mCurrentPaths;
619        private Paint mStrokePaint;
620        private Paint mFillPaint;
621        private PathMeasure mPathMeasure;
622
623        private int[] mCurrentState = new int[0];
624        private float mAnimationValue;
625        private long mTotalDuration;
626        private int mTrigger;
627        private boolean mTriggerState;
628
629        final ArrayList<VGroup> mGroupList = new ArrayList<VGroup>();
630
631        float mBaseWidth = 1;
632        float mBaseHeight = 1;
633        float mViewportWidth;
634        float mViewportHeight;
635
636        public VAnimatedPath() {
637        }
638
639        public VAnimatedPath(VAnimatedPath copy) {
640            mCurrentAnimList = new ArrayList<VAnimation>(copy.mCurrentAnimList);
641            mGroupList.addAll(copy.mGroupList);
642            if (copy.mCurrentPaths != null) {
643                mCurrentPaths = new VPath[copy.mCurrentPaths.length];
644                for (int i = 0; i < mCurrentPaths.length; i++) {
645                    mCurrentPaths[i] = new VPath(copy.mCurrentPaths[i]);
646                }
647            }
648            mAnimationValue = copy.mAnimationValue; // time goes from 0 to 1
649
650            mBaseWidth = copy.mBaseWidth;
651            mBaseHeight = copy.mBaseHeight;
652            mViewportWidth = copy.mViewportHeight;
653            mViewportHeight = copy.mViewportHeight;
654            mTotalDuration = copy.mTotalDuration;
655            mTrigger = copy.mTrigger;
656            mCurrentState = new int[0];
657        }
658
659        public boolean canApplyTheme() {
660            final ArrayList<VGroup> groups = mGroupList;
661            for (int i = groups.size() - 1; i >= 0; i--) {
662                final ArrayList<VPath> paths = groups.get(i).mVGList;
663                for (int j = paths.size() - 1; j >= 0; j--) {
664                    final VPath path = paths.get(j);
665                    if (path.canApplyTheme()) {
666                        return true;
667                    }
668                }
669            }
670
671            final ArrayList<VAnimation> anims = mCurrentAnimList;
672            for (int i = anims.size() - 1; i >= 0; i--) {
673                final VAnimation anim = anims.get(i);
674                if (anim.canApplyTheme()) {
675                    return true;
676                }
677            }
678
679            return false;
680        }
681
682        public void applyTheme(Theme t) {
683            final ArrayList<VGroup> groups = mGroupList;
684            for (int i = groups.size() - 1; i >= 0; i--) {
685                final ArrayList<VPath> paths = groups.get(i).mVGList;
686                for (int j = paths.size() - 1; j >= 0; j--) {
687                    final VPath path = paths.get(j);
688                    if (path.canApplyTheme()) {
689                        path.applyTheme(t);
690                    }
691                }
692            }
693
694            final ArrayList<VAnimation> anims = mCurrentAnimList;
695            for (int i = anims.size() - 1; i >= 0; i--) {
696                final VAnimation anim = anims.get(i);
697                if (anim.canApplyTheme()) {
698                    anim.applyTheme(t);
699                }
700            }
701        }
702
703        public void setTrigger(int trigger){
704            mTrigger = VAnimatedPath.getStateForTrigger(trigger);
705        }
706
707        public long getTotalAnimationDuration() {
708            mTotalDuration = 0;
709            int size = mCurrentAnimList.size();
710            for (int i = 0; i < size; i++) {
711                VAnimation vAnimation = mCurrentAnimList.get(i);
712                long t = vAnimation.getTotalDuration();
713                if (t == -1) {
714                    mTotalDuration = -1;
715                    return -1;
716                }
717                mTotalDuration = Math.max(mTotalDuration, t);
718            }
719
720            return mTotalDuration;
721        }
722
723        public float getValue() {
724            return mAnimationValue;
725        }
726
727        /**
728         * @param value the point along the animations to show typically between 0.0f and 1.0f
729         * @return true if you need to keep repeating
730         */
731        public boolean setAnimationFraction(float value) {
732            getTotalAnimationDuration();
733
734            long animationTime = (long) (value * mTotalDuration);
735
736            final int len = mCurrentPaths.length;
737            for (int i = 0; i < len; i++) {
738                animationTime =
739                        (long) ((mTotalDuration == -1) ? value * 1000 : mTotalDuration * value);
740
741                final VPath path = mCurrentPaths[i];
742                final int size = mCurrentAnimList.size();
743                for (int j = 0; j < size; j++) {
744                    final VAnimation vAnimation = mCurrentAnimList.get(j);
745                    if (vAnimation.doesAdjustPath(path)) {
746                        mCurrentPaths[i] =  vAnimation.getPathAtTime(animationTime, path);
747                    }
748                }
749            }
750
751            mAnimationValue = value;
752
753            if (mTotalDuration == -1) {
754                return true;
755            } else {
756                return animationTime < mTotalDuration;
757            }
758        }
759
760        public void draw(Canvas canvas, int w, int h) {
761            if (mCurrentPaths == null) {
762                Log.e(LOGTAG,"mCurrentPaths == null");
763                return;
764            }
765
766            for (int i = 0; i < mCurrentPaths.length; i++) {
767                if (mCurrentPaths[i] != null && mCurrentPaths[i].isVisible(mCurrentState)) {
768                    drawPath(mCurrentPaths[i], canvas, w, h);
769                }
770            }
771        }
772
773        private void drawPath(VPath vPath, Canvas canvas, int w, int h) {
774            final float scale = Math.min(h / mViewportHeight, w / mViewportWidth);
775
776            vPath.toPath(mPath);
777            final Path path = mPath;
778
779            if (vPath.mTrimPathStart != 0.0f || vPath.mTrimPathEnd != 1.0f) {
780                float start = (vPath.mTrimPathStart + vPath.mTrimPathOffset) % 1.0f;
781                float end = (vPath.mTrimPathEnd + vPath.mTrimPathOffset) % 1.0f;
782
783                if (mPathMeasure == null) {
784                    mPathMeasure = new PathMeasure();
785                }
786                mPathMeasure.setPath(mPath, false);
787
788                float len = mPathMeasure.getLength();
789                start = start * len;
790                end = end * len;
791                path.reset();
792                if (start > end) {
793                    mPathMeasure.getSegment(start, len, path, true);
794                    mPathMeasure.getSegment(0f, end, path, true);
795                } else {
796                    mPathMeasure.getSegment(start, end, path, true);
797                }
798                path.rLineTo(0, 0); // fix bug in measure
799            }
800
801            mRenderPath.reset();
802            mMatrix.reset();
803
804            mMatrix.postRotate(vPath.mRotate, vPath.mPivotX, vPath.mPivotY);
805            mMatrix.postScale(scale, scale, mViewportWidth / 2f, mViewportHeight / 2f);
806            mMatrix.postTranslate(w / 2f - mViewportWidth / 2f, h / 2f - mViewportHeight / 2f);
807
808            mRenderPath.addPath(path, mMatrix);
809
810            if (vPath.mClip) {
811                canvas.clipPath(mRenderPath, Region.Op.REPLACE);
812            }
813
814            if (vPath.mFillColor != 0) {
815                if (mFillPaint == null) {
816                    mFillPaint = new Paint();
817                    mFillPaint.setStyle(Paint.Style.FILL);
818                    mFillPaint.setAntiAlias(true);
819                }
820
821                mFillPaint.setColor(vPath.mFillColor);
822                canvas.drawPath(mRenderPath, mFillPaint);
823            }
824
825            if (vPath.mStrokeColor != 0) {
826                if (mStrokePaint == null) {
827                    mStrokePaint = new Paint();
828                    mStrokePaint.setStyle(Paint.Style.STROKE);
829                    mStrokePaint.setAntiAlias(true);
830                }
831
832                final Paint strokePaint = mStrokePaint;
833                if (vPath.mStrokeLineJoin != null) {
834                    strokePaint.setStrokeJoin(vPath.mStrokeLineJoin);
835                }
836
837                if (vPath.mStrokeLineCap != null) {
838                    strokePaint.setStrokeCap(vPath.mStrokeLineCap);
839                }
840
841                strokePaint.setStrokeMiter(vPath.mStrokeMiterlimit * scale);
842                strokePaint.setColor(vPath.mStrokeColor);
843                strokePaint.setStrokeWidth(vPath.mStrokeWidth * scale);
844                canvas.drawPath(mRenderPath, strokePaint);
845            }
846        }
847
848        /**
849         * Ensure there is at least one animation for every path in group (linking them by names)
850         * Build the "current" path based on the first group
851         * TODO: improve memory use & performance or move to C++
852         */
853        public void parseFinish() {
854            final HashMap<String, VAnimation> newAnimations = new HashMap<String, VAnimation>();
855            for (VGroup group : mGroupList) {
856                for (VPath vPath : group.getPaths()) {
857                    if (!vPath.mAnimated) {
858                        VAnimation ap = null;
859
860                        if (!newAnimations.containsKey(vPath.getID())) {
861                            newAnimations.put(vPath.getID(), ap = new VAnimation());
862                        } else {
863                            ap = newAnimations.get(vPath.getID());
864                        }
865
866                        ap.addPath(vPath);
867                        vPath.mAnimated = true;
868                    }
869                }
870            }
871
872            if (mCurrentAnimList == null) {
873                mCurrentAnimList = new ArrayList<VectorDrawable.VAnimation>();
874            }
875            mCurrentAnimList.addAll(newAnimations.values());
876
877            final Collection<VPath> paths = mGroupList.get(0).getPaths();
878            mCurrentPaths = paths.toArray(new VPath[paths.size()]);
879            for (int i = 0; i < mCurrentPaths.length; i++) {
880                mCurrentPaths[i] = new VPath(mCurrentPaths[i]);
881            }
882        }
883
884        public void setState(int[] state) {
885            mCurrentState = Arrays.copyOf(state, state.length);
886        }
887
888        int getTrigger(int []state){
889            if (mTrigger == 0) return 0;
890            for (int i = 0; i < state.length; i++) {
891                if (state[i] == mTrigger){
892                    if (mTriggerState)
893                        return 0;
894                    mTriggerState = true;
895                    return 1;
896                }
897            }
898            if (mTriggerState) {
899                mTriggerState = false;
900                return -1;
901            }
902            return 0;
903        }
904
905        public void addAnimation(VAnimation anim) {
906            if (mCurrentAnimList == null) {
907                mCurrentAnimList = new ArrayList<VectorDrawable.VAnimation>();
908            }
909            mCurrentAnimList.add(anim);
910        }
911
912        private void parseViewport(Resources r, AttributeSet attrs)
913                throws XmlPullParserException {
914            final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableViewport);
915            mViewportWidth = a.getFloat(R.styleable.VectorDrawableViewport_viewportWidth, 0);
916            mViewportHeight = a.getFloat(R.styleable.VectorDrawableViewport_viewportHeight, 0);
917            if (mViewportWidth == 0 || mViewportHeight == 0) {
918                throw new XmlPullParserException(a.getPositionDescription()+
919                        "<viewport> tag requires viewportWidth & viewportHeight to be set");
920            }
921            a.recycle();
922        }
923
924        private void parseSize(Resources r, AttributeSet attrs)
925                throws XmlPullParserException  {
926            final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableSize);
927            mBaseWidth = a.getDimension(R.styleable.VectorDrawableSize_width, 0);
928            mBaseHeight = a.getDimension(R.styleable.VectorDrawableSize_height, 0);
929            if (mBaseWidth == 0 || mBaseHeight == 0) {
930                throw new XmlPullParserException(a.getPositionDescription()+
931                        "<size> tag requires width & height to be set");
932            }
933            a.recycle();
934        }
935
936        private static final int getStateForTrigger(int trigger) {
937            return TRIGGER_MAP[trigger];
938        }
939    }
940
941    private static class VAnimation {
942        private static final String SEPARATOR = ",";
943
944        private static final int DIRECTION_FORWARD = 0;
945        private static final int DIRECTION_IN_AND_OUT = 1;
946
947        public enum Style {
948            INTERPOLATE, CROSSFADE, WIPE
949        }
950
951        private final HashSet<String> mSeqMap = new HashSet<String>();
952
953        private Interpolator mAnimInterpolator = new AccelerateDecelerateInterpolator();
954        private VPath[] mPaths = new VPath[0];
955        private long[] mDuration = { DEFAULT_DURATION };
956
957        private int[] mThemeAttrs;
958        private Style mStyle;
959        private int mLimitProperty = 0;
960        private long mStartOffset;
961        private long mRepeat = 1;
962        private long mWipeDirection;
963        private int mMode = DIRECTION_FORWARD;
964        private int mInterpolatorType;
965        private String mId;
966
967        public VAnimation() {
968            // Empty constructor.
969        }
970
971        public void inflate(ArrayList<VGroup> groups, Resources r, AttributeSet attrs, Theme theme)
972                throws XmlPullParserException {
973            String value;
974            String[] sp;
975            int name;
976
977            final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableAnimation);
978            final int[] themeAttrs = a.extractThemeAttrs();
979            mThemeAttrs = themeAttrs;
980
981            value = a.getString(R.styleable.VectorDrawableAnimation_sequence);
982            if (value != null) {
983                sp = value.split(SEPARATOR);
984                final VectorDrawable.VPath[] paths = new VectorDrawable.VPath[sp.length];
985
986                for (int j = 0; j < sp.length; j++) {
987                    mSeqMap.add(sp[j].trim());
988
989                    final VectorDrawable.VPath path = groups.get(j).get(sp[j]);
990                    if (path == null) {
991                        throw new XmlPullParserException(a.getPositionDescription()
992                                + " missing path with name: " + sp[j]);
993                    }
994
995                    path.mAnimated = true;
996                    paths[j] = path;
997                }
998
999                setPaths(paths);
1000            }
1001
1002            name = R.styleable.VectorDrawableAnimation_durations;
1003            value = a.getString(name);
1004            if (value != null) {
1005                long totalDuration = 0;
1006                sp = value.split(SEPARATOR);
1007
1008                final long[] dur = new long[sp.length];
1009                for (int j = 0; j < dur.length; j++) {
1010                    dur[j] = Long.parseLong(sp[j]);
1011                    totalDuration +=  dur[j];
1012                }
1013
1014                if (totalDuration == 0){
1015                    throw new XmlPullParserException(a.getPositionDescription()
1016                            + " total duration must not be zero");
1017                }
1018
1019                setDuration(dur);
1020            }
1021
1022            setLimitProperty(a.getInt(R.styleable.VectorDrawableAnimation_limitTo, 0));
1023            setRepeat(a.getInt(R.styleable.VectorDrawableAnimation_repeatCount, 1));
1024            setStartOffset(a.getInt(R.styleable.VectorDrawableAnimation_startDelay, 0));
1025            setMode(a.getInt(R.styleable.VectorDrawableAnimation_repeatStyle, 0));
1026
1027            fixMissingParameters();
1028
1029            a.recycle();
1030        }
1031
1032        public boolean canApplyTheme() {
1033            return mThemeAttrs != null;
1034        }
1035
1036        public void applyTheme(Theme t) {
1037            // TODO: Apply theme.
1038        }
1039
1040        public boolean doesAdjustPath(VPath path) {
1041            return mSeqMap.contains(path.getID());
1042        }
1043
1044        public String getId() {
1045            if (mId == null) {
1046                mId = mPaths[0].getID();
1047                for (int i = 1; i < mPaths.length; i++) {
1048                    mId += mPaths[i].getID();
1049                }
1050            }
1051            return mId;
1052        }
1053
1054        public String getPathName() {
1055            return mPaths[0].getID();
1056        }
1057
1058        public Style getStyle() {
1059            return mStyle;
1060        }
1061
1062        public void setStyle(Style style) {
1063            mStyle = style;
1064        }
1065
1066        public int getLimitProperty() {
1067            return mLimitProperty;
1068        }
1069
1070        public void setLimitProperty(int limitProperty) {
1071            mLimitProperty = limitProperty;
1072        }
1073
1074        public long[] getDuration() {
1075            return mDuration;
1076        }
1077
1078        public void setDuration(long[] duration) {
1079            mDuration = duration;
1080        }
1081
1082        public long getRepeat() {
1083            return mRepeat;
1084        }
1085
1086        public void setRepeat(long repeat) {
1087            mRepeat = repeat;
1088        }
1089
1090        public long getStartOffset() {
1091            return mStartOffset;
1092        }
1093
1094        public void setStartOffset(long startOffset) {
1095            mStartOffset = startOffset;
1096        }
1097
1098        public long getWipeDirection() {
1099            return mWipeDirection;
1100        }
1101
1102        public void setWipeDirection(long wipeDirection) {
1103            mWipeDirection = wipeDirection;
1104        }
1105
1106        public int getMode() {
1107            return mMode;
1108        }
1109
1110        public void setMode(int mode) {
1111            mMode = mode;
1112        }
1113
1114        public int getInterpolator() {
1115            return mInterpolatorType;
1116        }
1117
1118        public void setInterpolator(int interpolator) {
1119            mInterpolatorType = interpolator;
1120        }
1121
1122        /**
1123         * compute the total time in milliseconds
1124         *
1125         * @return the total time in milliseconds the animation will take
1126         */
1127        public long getTotalDuration() {
1128            long total = mStartOffset;
1129            if (getRepeat() == -1) {
1130                return -1;
1131            }
1132            for (int i = 0; i < mDuration.length; i++) {
1133                if (mRepeat > 1) {
1134                    total += mDuration[i] * mRepeat;
1135                } else {
1136                    total += mDuration[i];
1137                }
1138            }
1139            return total;
1140        }
1141
1142        public void setPaths(VPath[] paths) {
1143            mPaths = paths;
1144        }
1145
1146        public void addPath(VPath path) {
1147            mPaths = Arrays.copyOf(mPaths, mPaths.length + 1);
1148            mPaths[mPaths.length - 1] = path;
1149        }
1150
1151        public boolean containsPath(String pathid) {
1152            for (int i = 0; i < mPaths.length; i++) {
1153                if (mPaths[i].getID().equals(pathid)) {
1154                    return true;
1155                }
1156            }
1157            return false;
1158        }
1159
1160        public void interpolate(VPath p1, VPath p2, float time, VPath dest) {
1161            VPath.interpolate(time, p1, p2, dest, mLimitProperty);
1162        }
1163
1164        public VPath getPathAtTime(long milliseconds, VPath dest) {
1165            if (mPaths.length == 1) {
1166                dest.copyFrom(mPaths[0]);
1167                return dest;
1168            }
1169            long point = milliseconds - mStartOffset;
1170            if (point < 0) {
1171                point = 0;
1172            }
1173            float time = 0;
1174            long sum = mDuration[0];
1175            for (int i = 1; i < mDuration.length; i++) {
1176                sum += mDuration[i];
1177            }
1178
1179            if (mRepeat > 1) {
1180                time = point / (float) (sum * mRepeat);
1181                time = mAnimInterpolator.getInterpolation(time);
1182
1183                if (mMode == DIRECTION_IN_AND_OUT) {
1184                    point = ((long) (time * sum * 2 * mRepeat)) % (sum * 2);
1185                    if (point > sum) {
1186                        point = sum * 2 - point;
1187                    }
1188                } else {
1189                    point = ((long) (time * sum * mRepeat)) % sum;
1190                }
1191            } else if (mRepeat == 1) {
1192                time = point / (float) (sum * mRepeat);
1193                time = mAnimInterpolator.getInterpolation(time);
1194                if (mMode == DIRECTION_IN_AND_OUT) {
1195                    point = ((long) (time * sum * 2 * mRepeat));
1196                    if (point > sum) {
1197                        point = sum * 2 - point;
1198                    }
1199                } else {
1200                    point = Math.min(((long) (time * sum * mRepeat)), sum);
1201                }
1202
1203            } else { // repeat = -1
1204                if (mMode == DIRECTION_IN_AND_OUT) {
1205                    point = point % (sum * 2);
1206                    if (point > sum) {
1207                        point = sum * 2 - point;
1208                    }
1209                    time = point / (float) sum;
1210                } else {
1211                    point = point % sum;
1212                    time = point / (float) sum;
1213                }
1214            }
1215
1216            int transition = 0;
1217            while (point > mDuration[transition]) {
1218                point -= mDuration[transition++];
1219            }
1220            if (mPaths.length > (transition + 1)) {
1221                if (mPaths[transition].getID() != dest.getID()) {
1222                    dest.copyFrom(mPaths[transition]);
1223                }
1224                interpolate(mPaths[transition], mPaths[transition + 1],
1225                        point / (float) mDuration[transition], dest);
1226            } else {
1227                interpolate(mPaths[transition], mPaths[transition], 0, dest);
1228            }
1229            return dest;
1230        }
1231
1232        void fixMissingParameters() {
1233            // fix missing points
1234            float rotation = Float.NaN;
1235            float rotationY = Float.NaN;
1236            float rotationX = Float.NaN;
1237            for (int i = 0; i < mPaths.length; i++) {
1238                if (mPaths[i].mPivotX > 0) {
1239                    rotationX = mPaths[i].mPivotX;
1240                }
1241                if (mPaths[i].mPivotY > 0) {
1242                    rotationY = mPaths[i].mPivotY;
1243                }
1244                if (mPaths[i].mRotate > 0) {
1245                    rotation = mPaths[i].mRotate;
1246                }
1247            }
1248            if (rotation > 0) {
1249                for (int i = 0; i < mPaths.length; i++) {
1250                    if (mPaths[i].mPivotX == 0) {
1251                        mPaths[i].mPivotX = rotationX;
1252                    }
1253                    if (mPaths[i].mPivotY == 0) {
1254                        mPaths[i].mPivotY = rotationY;
1255                    }
1256                }
1257            }
1258        }
1259    }
1260
1261    private static class VGroup {
1262        private final HashMap<String, VPath> mVGPathMap = new HashMap<String, VPath>();
1263        private final ArrayList<VPath> mVGList = new ArrayList<VPath>();
1264
1265        public void add(VPath path) {
1266            String id = path.getID();
1267            mVGPathMap.put(id, path);
1268            mVGList.add(path);
1269         }
1270
1271        public VPath get(String name) {
1272            return mVGPathMap.get(name);
1273        }
1274
1275        /**
1276         * Must return in order of adding
1277         * @return ordered list of paths
1278         */
1279        public Collection<VPath> getPaths() {
1280            return mVGList;
1281        }
1282
1283        public int size() {
1284            return mVGPathMap.size();
1285        }
1286    }
1287
1288    private static class VPath {
1289        private static final int LIMIT_ALL = 0;
1290        private static final int LIMIT_PATH = 1;
1291        private static final int LIMIT_ROTATE = 2;
1292        private static final int LIMIT_TRIM_PATH_START = 3;
1293        private static final int LIMIT_TRIM_PATH_OFFSET = 5;
1294        private static final int LIMIT_TRIM_PATH_END = 4;
1295
1296        private static final int STATE_UNDEFINED=0;
1297        private static final int STATE_TRUE=1;
1298        private static final int STATE_FALSE=2;
1299
1300        private static final int MAX_STATES = 10;
1301
1302        private int[] mThemeAttrs;
1303
1304        int mStrokeColor = 0;
1305        float mStrokeWidth = 0;
1306        float mStrokeOpacity = Float.NaN;
1307
1308        int mFillColor = 0;
1309        int mFillRule;
1310        float mFillOpacity = Float.NaN;
1311
1312        float mRotate = 0;
1313        float mPivotX = 0;
1314        float mPivotY = 0;
1315
1316        float mTrimPathStart = 0;
1317        float mTrimPathEnd = 1;
1318        float mTrimPathOffset = 0;
1319
1320        boolean mAnimated = false;
1321        boolean mClip = false;
1322        Paint.Cap mStrokeLineCap = Paint.Cap.BUTT;
1323        Paint.Join mStrokeLineJoin = Paint.Join.MITER;
1324        float mStrokeMiterlimit = 4;
1325
1326        private VNode[] mNode = null;
1327        private String mId;
1328        private int[] mCheckState = new int[MAX_STATES];
1329        private boolean[] mCheckValue = new boolean[MAX_STATES];
1330        private int mNumberOfStates = 0;
1331        private int mNumberOfTrue = 0;
1332
1333        public VPath() {
1334            // Empty constructor.
1335        }
1336
1337        public VPath(VPath p) {
1338            copyFrom(p);
1339        }
1340
1341        public void addStateFilter(int state, boolean condition) {
1342            int k = 0;
1343            while (k < mNumberOfStates) {
1344                if (mCheckState[mNumberOfStates] == state)
1345                    break;
1346                k++;
1347            }
1348            mCheckState[k] = state;
1349            mCheckValue[k] = condition;
1350            if (k==mNumberOfStates){
1351                mNumberOfStates++;
1352            }
1353            if (condition) {
1354                mNumberOfTrue++;
1355            }
1356        }
1357
1358        private int getState(int state){
1359            for (int i = 0; i < mNumberOfStates; i++) {
1360                if (mCheckState[mNumberOfStates] == state){
1361                    return (mCheckValue[i])?STATE_TRUE:STATE_FALSE;
1362                }
1363            }
1364            return STATE_UNDEFINED;
1365        }
1366        /**
1367         * @return the name of the path
1368         */
1369        public String getName() {
1370            return mId;
1371        }
1372
1373        public void toPath(Path path) {
1374            path.reset();
1375            if (mNode != null) {
1376                VNode.createPath(mNode, path);
1377            }
1378        }
1379
1380        public String getID() {
1381            return mId;
1382        }
1383
1384        private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) {
1385            switch (id) {
1386                case LINECAP_BUTT:
1387                    return Paint.Cap.BUTT;
1388                case LINECAP_ROUND:
1389                    return Paint.Cap.ROUND;
1390                case LINECAP_SQUARE:
1391                    return Paint.Cap.SQUARE;
1392                default:
1393                    return defValue;
1394            }
1395        }
1396
1397        private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) {
1398            switch (id) {
1399                case LINEJOIN_MITER:
1400                    return Paint.Join.MITER;
1401                case LINEJOIN_ROUND:
1402                    return Paint.Join.ROUND;
1403                case LINEJOIN_BEVEL:
1404                    return Paint.Join.BEVEL;
1405                default:
1406                    return defValue;
1407            }
1408        }
1409
1410        public void inflate(Resources r, AttributeSet attrs, Theme theme) {
1411            final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawablePath);
1412            final int[] themeAttrs = a.extractThemeAttrs();
1413            mThemeAttrs = themeAttrs;
1414
1415            // NOTE: The set of attributes loaded here MUST match the
1416            // set of attributes loaded in applyTheme.
1417            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_clipToPath] == 0) {
1418                mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip);
1419            }
1420
1421            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_name] == 0) {
1422                mId = a.getString(R.styleable.VectorDrawablePath_name);
1423            }
1424
1425            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pathData] == 0) {
1426                mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData));
1427            }
1428
1429            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fill] == 0) {
1430                mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor);
1431            }
1432
1433            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fillOpacity] == 0) {
1434                mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity);
1435            }
1436
1437            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_rotation] == 0) {
1438                mRotate = a.getFloat(R.styleable.VectorDrawablePath_rotation, mRotate);
1439            }
1440
1441            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pivotX] == 0) {
1442                mPivotX = a.getFloat(R.styleable.VectorDrawablePath_pivotX, mPivotX);
1443            }
1444
1445            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pivotY] == 0) {
1446                mPivotY = a.getFloat(R.styleable.VectorDrawablePath_pivotY, mPivotY);
1447            }
1448
1449            if (themeAttrs == null
1450                    || themeAttrs[R.styleable.VectorDrawablePath_strokeLineCap] == 0) {
1451                mStrokeLineCap = getStrokeLineCap(
1452                        a.getInt(R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap);
1453            }
1454
1455            if (themeAttrs == null
1456                    || themeAttrs[R.styleable.VectorDrawablePath_strokeLineJoin] == 0) {
1457                mStrokeLineJoin = getStrokeLineJoin(
1458                        a.getInt(R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin);
1459            }
1460
1461            if (themeAttrs == null
1462                    || themeAttrs[R.styleable.VectorDrawablePath_strokeMiterLimit] == 0) {
1463                mStrokeMiterlimit = a.getFloat(
1464                        R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit);
1465            }
1466
1467            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_stroke] == 0) {
1468                mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor);
1469            }
1470
1471            if (themeAttrs == null
1472                    || themeAttrs[R.styleable.VectorDrawablePath_strokeOpacity] == 0) {
1473                mStrokeOpacity = a.getFloat(
1474                        R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity);
1475            }
1476
1477            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_strokeWidth] == 0) {
1478                mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth);
1479            }
1480
1481            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_trimPathEnd] == 0) {
1482                mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd);
1483            }
1484
1485            if (themeAttrs == null
1486                    || themeAttrs[R.styleable.VectorDrawablePath_trimPathOffset] == 0) {
1487                mTrimPathOffset = a.getFloat(
1488                        R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset);
1489            }
1490
1491            if (themeAttrs == null
1492                    || themeAttrs[R.styleable.VectorDrawablePath_trimPathStart] == 0) {
1493                mTrimPathStart = a.getFloat(
1494                        R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart);
1495            }
1496
1497            // TODO: Consider replacing this with existing state attributes.
1498            final int[] states = {
1499                    R.styleable.VectorDrawablePath_state_activated,
1500                    R.styleable.VectorDrawablePath_state_checkable,
1501                    R.styleable.VectorDrawablePath_state_checked,
1502                    R.styleable.VectorDrawablePath_state_enabled,
1503                    R.styleable.VectorDrawablePath_state_focused,
1504                    R.styleable.VectorDrawablePath_state_hovered,
1505                    R.styleable.VectorDrawablePath_state_pressed,
1506                    R.styleable.VectorDrawablePath_state_selected,
1507                    R.styleable.VectorDrawablePath_state_window_focused
1508            };
1509
1510            final int N = states.length;
1511            for (int i = 0; i < N; i++) {
1512                final int state = states[i];
1513                if (a.hasValue(state)) {
1514                    addStateFilter(state, a.getBoolean(state, false));
1515                }
1516            }
1517
1518            updateColorAlphas();
1519
1520            a.recycle();
1521        }
1522
1523        public boolean canApplyTheme() {
1524            return mThemeAttrs != null;
1525        }
1526
1527        public void applyTheme(Theme t) {
1528            if (mThemeAttrs == null) {
1529                return;
1530            }
1531
1532            final TypedArray a = t.resolveAttributes(
1533                    mThemeAttrs, R.styleable.VectorDrawablePath, 0, 0);
1534
1535            mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip);
1536
1537            if (a.hasValue(R.styleable.VectorDrawablePath_name)) {
1538                mId = a.getString(R.styleable.VectorDrawablePath_name);
1539            }
1540
1541            if (a.hasValue(R.styleable.VectorDrawablePath_pathData)) {
1542                mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData));
1543            }
1544
1545            mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor);
1546            mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity);
1547
1548            mRotate = a.getFloat(R.styleable.VectorDrawablePath_rotation, mRotate);
1549            mPivotX = a.getFloat(R.styleable.VectorDrawablePath_pivotX, mPivotX);
1550            mPivotY = a.getFloat(R.styleable.VectorDrawablePath_pivotY, mPivotY);
1551
1552            mStrokeLineCap = getStrokeLineCap(a.getInt(
1553                    R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap);
1554            mStrokeLineJoin = getStrokeLineJoin(a.getInt(
1555                    R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin);
1556            mStrokeMiterlimit = a.getFloat(
1557                    R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit);
1558            mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor);
1559            mStrokeOpacity = a.getFloat(
1560                    R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity);
1561            mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth);
1562
1563            mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd);
1564            mTrimPathOffset = a.getFloat(
1565                    R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset);
1566            mTrimPathStart = a.getFloat(
1567                    R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart);
1568
1569            updateColorAlphas();
1570        }
1571
1572        private void updateColorAlphas() {
1573            if (!Float.isNaN(mFillOpacity)) {
1574                mFillColor &= 0x00FFFFFF;
1575                mFillColor |= ((int) (0xFF * mFillOpacity)) << 24;
1576            }
1577
1578            if (!Float.isNaN(mStrokeOpacity)) {
1579                mStrokeColor &= 0x00FFFFFF;
1580                mStrokeColor |= ((int) (0xFF * mStrokeOpacity)) << 24;
1581            }
1582        }
1583
1584        private static int nextStart(String s, int end) {
1585            char c;
1586
1587            while (end < s.length()) {
1588                c = s.charAt(end);
1589                if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) {
1590                    return end;
1591                }
1592                end++;
1593            }
1594            return end;
1595        }
1596
1597        private void addNode(ArrayList<VectorDrawable.VNode> list, char cmd, float[] val) {
1598            list.add(new VectorDrawable.VNode(cmd, val));
1599        }
1600
1601        /**
1602         * parse the floats in the string
1603         * this is an optimized version of
1604         * parseFloat(s.split(",|\\s"));
1605         *
1606         * @param s the string containing a command and list of floats
1607         * @return array of floats
1608         */
1609        private static float[] getFloats(String s) {
1610            if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') {
1611                return new float[0];
1612            }
1613            try {
1614                float[] tmp = new float[s.length()];
1615                int count = 0;
1616                int pos = 1, end;
1617                while ((end = extract(s, pos)) >= 0) {
1618                    if (pos < end) {
1619                        tmp[count++] = Float.parseFloat(s.substring(pos, end));
1620                    }
1621                    pos = end + 1;
1622                }
1623                // handle the final float if there is one
1624                if (pos < s.length()) {
1625                    tmp[count++] = Float.parseFloat(s.substring(pos, s.length()));
1626                }
1627                return Arrays.copyOf(tmp, count);
1628            } catch (NumberFormatException e){
1629                Log.e(LOGTAG,"error in parsing \""+s+"\"");
1630                throw e;
1631            }
1632        }
1633
1634        /**
1635         * calculate the position of the next comma or space
1636         * @param s the string to search
1637         * @param start the position to start searching
1638         * @return the position of the next comma or space or -1 if none found
1639         */
1640        private static int extract(String s, int start) {
1641            int space = s.indexOf(' ', start);
1642            int comma = s.indexOf(',', start);
1643            if (space == -1) {
1644                return comma;
1645            }
1646            if (comma == -1) {
1647                return space;
1648            }
1649            return (comma > space) ? space : comma;
1650        }
1651
1652        private VectorDrawable.VNode[] parsePath(String value) {
1653            int start = 0;
1654            int end = 1;
1655
1656            ArrayList<VectorDrawable.VNode> list = new ArrayList<VectorDrawable.VNode>();
1657            while (end < value.length()) {
1658                end = nextStart(value, end);
1659                String s = value.substring(start, end);
1660                float[] val = getFloats(s);
1661                addNode(list, s.charAt(0), val);
1662
1663                start = end;
1664                end++;
1665            }
1666            if ((end - start) == 1 && start < value.length()) {
1667
1668                addNode(list, value.charAt(start), new float[0]);
1669            }
1670            return list.toArray(new VectorDrawable.VNode[list.size()]);
1671        }
1672
1673        public void copyFrom(VPath p1) {
1674            mNode = new VNode[p1.mNode.length];
1675            for (int i = 0; i < mNode.length; i++) {
1676                mNode[i] = new VNode(p1.mNode[i]);
1677            }
1678            mId = p1.mId;
1679            mStrokeColor = p1.mStrokeColor;
1680            mFillColor = p1.mFillColor;
1681            mStrokeWidth = p1.mStrokeWidth;
1682            mRotate = p1.mRotate;
1683            mPivotX = p1.mPivotX;
1684            mPivotY = p1.mPivotY;
1685            mAnimated = p1.mAnimated;
1686            mTrimPathStart = p1.mTrimPathStart;
1687            mTrimPathEnd = p1.mTrimPathEnd;
1688            mTrimPathOffset = p1.mTrimPathOffset;
1689            mStrokeLineCap = p1.mStrokeLineCap;
1690            mStrokeLineJoin = p1.mStrokeLineJoin;
1691            mStrokeMiterlimit = p1.mStrokeMiterlimit;
1692            mNumberOfStates = p1.mNumberOfStates;
1693            for (int i = 0; i < mNumberOfStates; i++) {
1694                mCheckState[i] = p1.mCheckState[i];
1695                mCheckValue[i] = p1.mCheckValue[i];
1696            }
1697
1698            mFillRule = p1.mFillRule;
1699        }
1700
1701        public static VPath interpolate(float t, VPath p1, VPath p2, VPath returnPath, int limit) {
1702            if (limit == LIMIT_ALL || limit == LIMIT_PATH) {
1703                if (returnPath.mNode == null || returnPath.mNode.length != p1.mNode.length) {
1704                    returnPath.mNode = new VNode[p1.mNode.length];
1705                }
1706                for (int i = 0; i < returnPath.mNode.length; i++) {
1707                    if (returnPath.mNode[i] == null) {
1708                        returnPath.mNode[i] = new VNode(p1.mNode[i], p2.mNode[i], t);
1709                    } else {
1710                        returnPath.mNode[i].interpolate(p1.mNode[i], p2.mNode[i], t);
1711                    }
1712                }
1713            }
1714            float t1 = 1 - t;
1715            switch (limit) {
1716                case LIMIT_ALL:
1717                    returnPath.mRotate = t1 * p1.mRotate + t * p2.mRotate;
1718                    returnPath.mPivotX = t1 * p1.mPivotX + t * p2.mPivotX;
1719                    returnPath.mPivotY = t1 * p1.mPivotY + t * p2.mPivotY;
1720                    returnPath.mClip = p1.mClip | p2.mClip;
1721
1722                    returnPath.mTrimPathStart = t1 * p1.mTrimPathStart + t * p2.mTrimPathStart;
1723                    returnPath.mTrimPathEnd = t1 * p1.mTrimPathEnd + t * p2.mTrimPathEnd;
1724                    returnPath.mTrimPathOffset = t1 * p1.mTrimPathOffset + t * p2.mTrimPathOffset;
1725                    returnPath.mStrokeMiterlimit =
1726                            t1 * p1.mStrokeMiterlimit + t * p2.mStrokeMiterlimit;
1727                    returnPath.mStrokeLineCap = p1.mStrokeLineCap;
1728                    if (returnPath.mStrokeLineCap == null) {
1729                        returnPath.mStrokeLineCap = p2.mStrokeLineCap;
1730                    }
1731                    returnPath.mStrokeLineJoin = p1.mStrokeLineJoin;
1732                    if (returnPath.mStrokeLineJoin == null) {
1733                        returnPath.mStrokeLineJoin = p2.mStrokeLineJoin;
1734                    }
1735                    returnPath.mFillRule = p1.mFillRule;
1736
1737                    returnPath.mStrokeColor = rgbInterpolate(t, p1.mStrokeColor, p2.mStrokeColor);
1738                    returnPath.mFillColor = rgbInterpolate(t, p1.mFillColor, p2.mFillColor);
1739                    returnPath.mStrokeWidth = t1 * p1.mStrokeWidth + t * p2.mStrokeWidth;
1740                    returnPath.mNumberOfStates = p1.mNumberOfStates;
1741                    for (int i = 0; i < returnPath.mNumberOfStates; i++) {
1742                        returnPath.mCheckState[i] = p1.mCheckState[i];
1743                        returnPath.mCheckValue[i] = p1.mCheckValue[i];
1744                    }
1745                    for (int i = 0; i < p2.mNumberOfStates; i++) {
1746                        returnPath.addStateFilter(p2.mCheckState[i], p2.mCheckValue[i]);
1747                    }
1748
1749                    int count = 0;
1750                    for (int i = 0; i < returnPath.mNumberOfStates; i++) {
1751                        if (returnPath.mCheckValue[i]) {
1752                            count++;
1753                        }
1754                    }
1755                    returnPath.mNumberOfTrue = count;
1756                    break;
1757                case LIMIT_ROTATE:
1758                    returnPath.mRotate = t1 * p1.mRotate + t * p2.mRotate;
1759                    break;
1760                case LIMIT_TRIM_PATH_END:
1761                    returnPath.mTrimPathEnd = t1 * p1.mTrimPathEnd + t * p2.mTrimPathEnd;
1762                    break;
1763                case LIMIT_TRIM_PATH_OFFSET:
1764                    returnPath.mTrimPathOffset = t1 * p1.mTrimPathOffset + t * p2.mTrimPathOffset;
1765                    break;
1766                case LIMIT_TRIM_PATH_START:
1767                    returnPath.mTrimPathStart = t1 * p1.mTrimPathStart + t * p2.mTrimPathStart;
1768                    break;
1769            }
1770            return returnPath;
1771        }
1772
1773        private static int rgbInterpolate(float fraction, int startColor, int endColor) {
1774            if (startColor == endColor) {
1775                return startColor;
1776            } else if (startColor == 0) {
1777                return endColor;
1778            } else if (endColor == 0) {
1779                return startColor;
1780            }
1781
1782            final int startA = (startColor >> 24) & 0xff;
1783            final int startR = (startColor >> 16) & 0xff;
1784            final int startG = (startColor >> 8) & 0xff;
1785            final int startB = startColor & 0xff;
1786
1787            final int endA = (endColor >> 24) & 0xff;
1788            final int endR = (endColor >> 16) & 0xff;
1789            final int endG = (endColor >> 8) & 0xff;
1790            final int endB = endColor & 0xff;
1791
1792            return ((startA + (int)(fraction * (endA - startA))) << 24) |
1793                    ((startR + (int)(fraction * (endR - startR))) << 16) |
1794                    ((startG + (int)(fraction * (endG - startG))) << 8) |
1795                    ((startB + (int)(fraction * (endB - startB))));
1796        }
1797
1798        public boolean isVisible(int[] state) {
1799            int match = 0;
1800            for (int i = 0; i < state.length; i++) {
1801                int v = getState(state[i]);
1802                if (v != STATE_UNDEFINED) {
1803                    if (v==STATE_TRUE) {
1804                        match++;
1805                    } else {
1806                        return false;
1807                    }
1808                }
1809            }
1810            return match == mNumberOfTrue;
1811        }
1812    }
1813
1814    private static class VNode {
1815        private char mType;
1816        private float[] mParams;
1817
1818        public VNode(char type, float[] params) {
1819            mType = type;
1820            mParams = params;
1821        }
1822
1823        public VNode(VNode n) {
1824            mType = n.mType;
1825            mParams = Arrays.copyOf(n.mParams, n.mParams.length);
1826        }
1827
1828        public VNode(VNode n1, VNode n2, float t) {
1829            mType = n1.mType;
1830            mParams = new float[n1.mParams.length];
1831            interpolate(n1, n2, t);
1832        }
1833
1834        private boolean match(VNode n) {
1835            if (n.mType != mType) {
1836                return false;
1837            }
1838            return (mParams.length == n.mParams.length);
1839        }
1840
1841        public void interpolate(VNode n1, VNode n2, float t) {
1842            for (int i = 0; i < n1.mParams.length; i++) {
1843                mParams[i] = n1.mParams[i] * (1 - t) + n2.mParams[i] * t;
1844            }
1845        }
1846
1847        public static void createPath(VNode[] node, Path path) {
1848            float[] current = new float[4];
1849            char previousCommand = 'm';
1850            for (int i = 0; i < node.length; i++) {
1851                addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
1852                previousCommand = node[i].mType;
1853            }
1854        }
1855
1856        private static void addCommand(Path path, float[] current,
1857                char previousCmd, char cmd, float[] val) {
1858
1859            int incr = 2;
1860            float currentX = current[0];
1861            float currentY = current[1];
1862            float ctrlPointX = current[2];
1863            float ctrlPointY = current[3];
1864            float reflectiveCtrlPointX;
1865            float reflectiveCtrlPointY;
1866
1867            switch (cmd) {
1868                case 'z':
1869                case 'Z':
1870                    path.close();
1871                    return;
1872                case 'm':
1873                case 'M':
1874                case 'l':
1875                case 'L':
1876                case 't':
1877                case 'T':
1878                    incr = 2;
1879                    break;
1880                case 'h':
1881                case 'H':
1882                case 'v':
1883                case 'V':
1884                    incr = 1;
1885                    break;
1886                case 'c':
1887                case 'C':
1888                    incr = 6;
1889                    break;
1890                case 's':
1891                case 'S':
1892                case 'q':
1893                case 'Q':
1894                    incr = 4;
1895                    break;
1896                case 'a':
1897                case 'A':
1898                    incr = 7;
1899                    break;
1900            }
1901            for (int k = 0; k < val.length; k += incr) {
1902                // TODO: build test to prove all permutations work
1903                switch (cmd) {
1904                    case 'm': // moveto - Start a new sub-path (relative)
1905                        path.rMoveTo(val[k + 0], val[k + 1]);
1906                        currentX += val[k + 0];
1907                        currentY += val[k + 1];
1908                        break;
1909                    case 'M': // moveto - Start a new sub-path
1910                        path.moveTo(val[k + 0], val[k + 1]);
1911                        currentX = val[k + 0];
1912                        currentY = val[k + 1];
1913                        break;
1914                    case 'l': // lineto - Draw a line from the current point (relative)
1915                        path.rLineTo(val[k + 0], val[k + 1]);
1916                        currentX += val[k + 0];
1917                        currentY += val[k + 1];
1918                        break;
1919                    case 'L': // lineto - Draw a line from the current point
1920                        path.lineTo(val[k + 0], val[k + 1]);
1921                        currentX = val[k + 0];
1922                        currentY = val[k + 1];
1923                        break;
1924                    case 'z': // closepath - Close the current subpath
1925                    case 'Z': // closepath - Close the current subpath
1926                        path.close();
1927                        break;
1928                    case 'h': // horizontal lineto - Draws a horizontal line (relative)
1929                        path.rLineTo(val[k + 0], 0);
1930                        currentX += val[k + 0];
1931                        break;
1932                    case 'H': // horizontal lineto - Draws a horizontal line
1933                        path.lineTo(val[k + 0], currentY);
1934                        currentX = val[k + 0];
1935                        break;
1936                    case 'v': // vertical lineto - Draws a vertical line from the current point (r)
1937                        path.rLineTo(0, val[k + 0]);
1938                        currentY += val[k + 0];
1939                        break;
1940                    case 'V': // vertical lineto - Draws a vertical line from the current point
1941                        path.lineTo(currentX, val[k + 0]);
1942                        currentY = val[k + 0];
1943                        break;
1944                    case 'c': // curveto - Draws a cubic Bézier curve (relative)
1945                        path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
1946                                val[k + 4], val[k + 5]);
1947
1948                        ctrlPointX = currentX + val[k + 2];
1949                        ctrlPointY = currentY + val[k + 3];
1950                        currentX += val[k + 4];
1951                        currentY += val[k + 5];
1952
1953                        break;
1954                    case 'C': // curveto - Draws a cubic Bézier curve
1955                        path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
1956                                val[k + 4], val[k + 5]);
1957                        currentX = val[k + 4];
1958                        currentY = val[k + 5];
1959                        ctrlPointX = val[k + 2];
1960                        ctrlPointY = val[k + 3];
1961                        break;
1962                    case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
1963                        reflectiveCtrlPointX = 0;
1964                        reflectiveCtrlPointY = 0;
1965                        if (previousCmd == 'c' || previousCmd == 's'
1966                                || previousCmd == 'C' || previousCmd == 'S') {
1967                            reflectiveCtrlPointX = currentX - ctrlPointX;
1968                            reflectiveCtrlPointY = currentY - ctrlPointY;
1969                        }
1970                        path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
1971                                val[k + 0], val[k + 1],
1972                                val[k + 2], val[k + 3]);
1973
1974                        ctrlPointX = currentX + val[k + 0];
1975                        ctrlPointY = currentY + val[k + 1];
1976                        currentX += val[k + 2];
1977                        currentY += val[k + 3];
1978                        break;
1979                    case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
1980                        reflectiveCtrlPointX = currentX;
1981                        reflectiveCtrlPointY = currentY;
1982                        if (previousCmd == 'c' || previousCmd == 's'
1983                                || previousCmd == 'C' || previousCmd == 'S') {
1984                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
1985                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
1986                        }
1987                        path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
1988                                val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
1989                        ctrlPointX = val[k + 0];
1990                        ctrlPointY = val[k + 1];
1991                        currentX = val[k + 2];
1992                        currentY = val[k + 3];
1993                        break;
1994                    case 'q': // Draws a quadratic Bézier (relative)
1995                        path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
1996                        ctrlPointX = currentX + val[k + 0];
1997                        ctrlPointY = currentY + val[k + 1];
1998                        currentX += val[k + 2];
1999                        currentY += val[k + 3];
2000                        break;
2001                    case 'Q': // Draws a quadratic Bézier
2002                        path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
2003                        ctrlPointX = val[k + 0];
2004                        ctrlPointY = val[k + 1];
2005                        currentX = val[k + 2];
2006                        currentY = val[k + 3];
2007                        break;
2008                    case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
2009                        reflectiveCtrlPointX = 0;
2010                        reflectiveCtrlPointY = 0;
2011                        if (previousCmd == 'q' || previousCmd == 't'
2012                                || previousCmd == 'Q' || previousCmd == 'T') {
2013                            reflectiveCtrlPointX = currentX - ctrlPointX;
2014                            reflectiveCtrlPointY = currentY - ctrlPointY;
2015                        }
2016                        path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
2017                                val[k + 0], val[k + 1]);
2018                        ctrlPointX = currentX + reflectiveCtrlPointX;
2019                        ctrlPointY = currentY + reflectiveCtrlPointY;
2020                        currentX += val[k + 0];
2021                        currentY += val[k + 1];
2022                        break;
2023                    case 'T': // Draws a quadratic Bézier curve (reflective control point)
2024                        reflectiveCtrlPointX = currentX;
2025                        reflectiveCtrlPointY = currentY;
2026                        if (previousCmd == 'q' || previousCmd == 't'
2027                                || previousCmd == 'Q' || previousCmd == 'T') {
2028                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
2029                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
2030                        }
2031                        path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
2032                                val[k + 0], val[k + 1]);
2033                        ctrlPointX = reflectiveCtrlPointX;
2034                        ctrlPointY = reflectiveCtrlPointY;
2035                        currentX = val[k + 0];
2036                        currentY = val[k + 1];
2037                        break;
2038                    case 'a': // Draws an elliptical arc
2039                        // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
2040                        drawArc(path,
2041                                currentX,
2042                                currentY,
2043                                val[k + 5] + currentX,
2044                                val[k + 6] + currentY,
2045                                val[k + 0],
2046                                val[k + 1],
2047                                val[k + 2],
2048                                val[k + 3] != 0,
2049                                val[k + 4] != 0);
2050                        currentX += val[k + 5];
2051                        currentY += val[k + 6];
2052                        ctrlPointX = currentX;
2053                        ctrlPointY = currentY;
2054                        break;
2055                    case 'A': // Draws an elliptical arc
2056                        drawArc(path,
2057                                currentX,
2058                                currentY,
2059                                val[k + 5],
2060                                val[k + 6],
2061                                val[k + 0],
2062                                val[k + 1],
2063                                val[k + 2],
2064                                val[k + 3] != 0,
2065                                val[k + 4] != 0);
2066                        currentX = val[k + 5];
2067                        currentY = val[k + 6];
2068                        ctrlPointX = currentX;
2069                        ctrlPointY = currentY;
2070                        break;
2071                }
2072                previousCmd = cmd;
2073            }
2074            current[0] = currentX;
2075            current[1] = currentY;
2076            current[2] = ctrlPointX;
2077            current[3] = ctrlPointY;
2078        }
2079
2080        private static void drawArc(Path p,
2081                float x0,
2082                float y0,
2083                float x1,
2084                float y1,
2085                float a,
2086                float b,
2087                float theta,
2088                boolean isMoreThanHalf,
2089                boolean isPositiveArc) {
2090
2091            /* Convert rotation angle from degrees to radians */
2092            double thetaD = Math.toRadians(theta);
2093            /* Pre-compute rotation matrix entries */
2094            double cosTheta = Math.cos(thetaD);
2095            double sinTheta = Math.sin(thetaD);
2096            /* Transform (x0, y0) and (x1, y1) into unit space */
2097            /* using (inverse) rotation, followed by (inverse) scale */
2098            double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
2099            double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
2100            double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
2101            double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
2102
2103            /* Compute differences and averages */
2104            double dx = x0p - x1p;
2105            double dy = y0p - y1p;
2106            double xm = (x0p + x1p) / 2;
2107            double ym = (y0p + y1p) / 2;
2108            /* Solve for intersecting unit circles */
2109            double dsq = dx * dx + dy * dy;
2110            if (dsq == 0.0) {
2111                Log.w(LOGTAG, " Points are coincident");
2112                return; /* Points are coincident */
2113            }
2114            double disc = 1.0 / dsq - 1.0 / 4.0;
2115            if (disc < 0.0) {
2116                Log.w(LOGTAG, "Points are too far apart " + dsq);
2117                float adjust = (float) (Math.sqrt(dsq) / 1.99999);
2118                drawArc(p, x0, y0, x1, y1, a * adjust,
2119                        b * adjust, theta, isMoreThanHalf, isPositiveArc);
2120                return; /* Points are too far apart */
2121            }
2122            double s = Math.sqrt(disc);
2123            double sdx = s * dx;
2124            double sdy = s * dy;
2125            double cx;
2126            double cy;
2127            if (isMoreThanHalf == isPositiveArc) {
2128                cx = xm - sdy;
2129                cy = ym + sdx;
2130            } else {
2131                cx = xm + sdy;
2132                cy = ym - sdx;
2133            }
2134
2135            double eta0 = Math.atan2((y0p - cy), (x0p - cx));
2136
2137            double eta1 = Math.atan2((y1p - cy), (x1p - cx));
2138
2139            double sweep = (eta1 - eta0);
2140            if (isPositiveArc != (sweep >= 0)) {
2141                if (sweep > 0) {
2142                    sweep -= 2 * Math.PI;
2143                } else {
2144                    sweep += 2 * Math.PI;
2145                }
2146            }
2147
2148            cx *= a;
2149            cy *= b;
2150            double tcx = cx;
2151            cx = cx * cosTheta - cy * sinTheta;
2152            cy = tcx * sinTheta + cy * cosTheta;
2153
2154            arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
2155        }
2156
2157        /**
2158         * Converts an arc to cubic Bezier segments and records them in p.
2159         *
2160         * @param p The target for the cubic Bezier segments
2161         * @param cx The x coordinate center of the ellipse
2162         * @param cy The y coordinate center of the ellipse
2163         * @param a The radius of the ellipse in the horizontal direction
2164         * @param b The radius of the ellipse in the vertical direction
2165         * @param e1x E(eta1) x coordinate of the starting point of the arc
2166         * @param e1y E(eta2) y coordinate of the starting point of the arc
2167         * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
2168         * @param start The start angle of the arc on the ellipse
2169         * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
2170         */
2171        private static void arcToBezier(Path p,
2172                double cx,
2173                double cy,
2174                double a,
2175                double b,
2176                double e1x,
2177                double e1y,
2178                double theta,
2179                double start,
2180                double sweep) {
2181            // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
2182            // and http://www.spaceroots.org/documents/ellipse/node22.html
2183
2184            // Maximum of 45 degrees per cubic Bezier segment
2185            int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI));
2186
2187            double eta1 = start;
2188            double cosTheta = Math.cos(theta);
2189            double sinTheta = Math.sin(theta);
2190            double cosEta1 = Math.cos(eta1);
2191            double sinEta1 = Math.sin(eta1);
2192            double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
2193            double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
2194
2195            double anglePerSegment = sweep / numSegments;
2196            for (int i = 0; i < numSegments; i++) {
2197                double eta2 = eta1 + anglePerSegment;
2198                double sinEta2 = Math.sin(eta2);
2199                double cosEta2 = Math.cos(eta2);
2200                double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
2201                double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
2202                double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
2203                double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
2204                double tanDiff2 = Math.tan((eta2 - eta1) / 2);
2205                double alpha =
2206                        Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
2207                double q1x = e1x + alpha * ep1x;
2208                double q1y = e1y + alpha * ep1y;
2209                double q2x = e2x - alpha * ep2x;
2210                double q2y = e2y - alpha * ep2y;
2211
2212                p.cubicTo((float) q1x,
2213                        (float) q1y,
2214                        (float) q2x,
2215                        (float) q2y,
2216                        (float) e2x,
2217                        (float) e2y);
2218                eta1 = eta2;
2219                e1x = e2x;
2220                e1y = e2y;
2221                ep1x = ep2x;
2222                ep1y = ep2y;
2223            }
2224        }
2225
2226    }
2227}
2228