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