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