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