VectorDrawable.java revision 860126b78aa4d6e8db5208c7f96764a8556cf95f
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        public void inflate(Resources r, AttributeSet attrs, Theme theme) {
1390            final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawablePath);
1391            final int[] themeAttrs = a.extractThemeAttrs();
1392            mThemeAttrs = themeAttrs;
1393
1394            mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, false);
1395            mId = a.getString(R.styleable.VectorDrawablePath_name);
1396            mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData));
1397
1398            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fill] == 0) {
1399                mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, 0);
1400            }
1401
1402            if (themeAttrs == null
1403                    || themeAttrs[R.styleable.VectorDrawablePath_fillOpacity] == 0) {
1404                mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, Float.NaN);
1405            }
1406
1407            mRotate = a.getFloat(R.styleable.VectorDrawablePath_rotation, 0);
1408            mPivotX = a.getFloat(R.styleable.VectorDrawablePath_pivotX, 0);
1409            mPivotY = a.getFloat(R.styleable.VectorDrawablePath_pivotY, 0);
1410
1411            final int lineCap  = a.getInt(R.styleable.VectorDrawablePath_strokeLineCap, 0);
1412            switch (lineCap) {
1413                case LINECAP_BUTT:
1414                    mStrokelineCap = Paint.Cap.BUTT;
1415                    break;
1416                case LINECAP_ROUND:
1417                    mStrokelineCap = Paint.Cap.ROUND;
1418                    break;
1419                case LINECAP_SQUARE:
1420                    mStrokelineCap = Paint.Cap.SQUARE;
1421                    break;
1422            }
1423
1424            final int lineJoin =  a.getInt(R.styleable.VectorDrawablePath_strokeLineJoin, 0);
1425            switch (lineJoin) {
1426                case LINEJOIN_MITER:
1427                    mStrokelineJoin = Paint.Join.MITER;
1428                    break;
1429                case LINEJOIN_ROUND:
1430                    mStrokelineJoin = Paint.Join.ROUND;
1431                    break;
1432                case LINEJOIN_BEVEL:
1433                    mStrokelineJoin = Paint.Join.BEVEL;
1434                    break;
1435            }
1436
1437            mStrokeMiterlimit = a.getFloat(R.styleable.VectorDrawablePath_strokeMiterLimit,
1438                    mStrokeMiterlimit);
1439
1440            if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_stroke] == 0) {
1441                mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor);
1442            }
1443
1444            if (themeAttrs == null
1445                    || themeAttrs[R.styleable.VectorDrawablePath_strokeOpacity] == 0) {
1446                mStrokeOpacity = a.getFloat(
1447                        R.styleable.VectorDrawablePath_strokeOpacity, Float.NaN);
1448            }
1449
1450            mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, 0);
1451            mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, 1);
1452            mTrimPathOffset = a.getFloat(R.styleable.VectorDrawablePath_trimPathOffset, 0);
1453            mTrimPathStart = a.getFloat(R.styleable.VectorDrawablePath_trimPathStart, 0);
1454
1455            final int[] states = {
1456                    R.styleable.VectorDrawablePath_state_activated,
1457                    R.styleable.VectorDrawablePath_state_checkable,
1458                    R.styleable.VectorDrawablePath_state_checked,
1459                    R.styleable.VectorDrawablePath_state_enabled,
1460                    R.styleable.VectorDrawablePath_state_focused,
1461                    R.styleable.VectorDrawablePath_state_hovered,
1462                    R.styleable.VectorDrawablePath_state_pressed,
1463                    R.styleable.VectorDrawablePath_state_selected,
1464                    R.styleable.VectorDrawablePath_state_window_focused
1465            };
1466
1467            final int N = states.length;
1468            for (int i = 0; i < N; i++) {
1469                final int state = states[i];
1470                if (a.hasValue(state)) {
1471                    addStateFilter(state, a.getBoolean(state, false));
1472                }
1473            }
1474
1475            updateColorAlphas();
1476
1477            a.recycle();
1478        }
1479
1480        public boolean canApplyTheme() {
1481            return mThemeAttrs != null;
1482        }
1483
1484        public void applyTheme(Theme t) {
1485            if (mThemeAttrs == null) {
1486                return;
1487            }
1488
1489            final TypedArray a = t.resolveAttributes(
1490                    mThemeAttrs, R.styleable.VectorDrawablePath, 0, 0);
1491
1492            if (a.hasValue(R.styleable.VectorDrawablePath_fill)) {
1493                mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, 0);
1494            }
1495
1496            if (a.hasValue(R.styleable.VectorDrawablePath_fillOpacity)) {
1497                mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, Float.NaN);
1498            }
1499
1500            if (a.hasValue(R.styleable.VectorDrawablePath_stroke)) {
1501                mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor);
1502            }
1503
1504            if (a.hasValue(R.styleable.VectorDrawablePath_strokeOpacity)) {
1505                mStrokeOpacity = a.getFloat(
1506                        R.styleable.VectorDrawablePath_strokeOpacity, Float.NaN);
1507            }
1508
1509            updateColorAlphas();
1510        }
1511
1512        private void updateColorAlphas() {
1513            if (!Float.isNaN(mFillOpacity)) {
1514                mFillColor &= 0x00FFFFFF;
1515                mFillColor |= ((int) (0xFF * mFillOpacity)) << 24;
1516            }
1517
1518            if (!Float.isNaN(mStrokeOpacity)) {
1519                mStrokeColor &= 0x00FFFFFF;
1520                mStrokeColor |= ((int) (0xFF * mStrokeOpacity)) << 24;
1521            }
1522        }
1523
1524        private static int nextStart(String s, int end) {
1525            char c;
1526
1527            while (end < s.length()) {
1528                c = s.charAt(end);
1529                if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) {
1530                    return end;
1531                }
1532                end++;
1533            }
1534            return end;
1535        }
1536
1537        private void addNode(ArrayList<VectorDrawable.VNode> list, char cmd, float[] val) {
1538            list.add(new VectorDrawable.VNode(cmd, val));
1539        }
1540
1541        /**
1542         * parse the floats in the string
1543         * this is an optimized version of
1544         * parseFloat(s.split(",|\\s"));
1545         *
1546         * @param s the string containing a command and list of floats
1547         * @return array of floats
1548         */
1549        private static float[] getFloats(String s) {
1550            if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') {
1551                return new float[0];
1552            }
1553            try {
1554                float[] tmp = new float[s.length()];
1555                int count = 0;
1556                int pos = 1, end;
1557                while ((end = extract(s, pos)) >= 0) {
1558                    if (pos < end) {
1559                        tmp[count++] = Float.parseFloat(s.substring(pos, end));
1560                    }
1561                    pos = end + 1;
1562                }
1563                // handle the final float if there is one
1564                if (pos < s.length()) {
1565                    tmp[count++] = Float.parseFloat(s.substring(pos, s.length()));
1566                }
1567                return Arrays.copyOf(tmp, count);
1568            } catch (NumberFormatException e){
1569                Log.e(LOGTAG,"error in parsing \""+s+"\"");
1570                throw e;
1571            }
1572        }
1573
1574        /**
1575         * calculate the position of the next comma or space
1576         * @param s the string to search
1577         * @param start the position to start searching
1578         * @return the position of the next comma or space or -1 if none found
1579         */
1580        private static int extract(String s, int start) {
1581            int space = s.indexOf(' ', start);
1582            int comma = s.indexOf(',', start);
1583            if (space == -1) {
1584                return comma;
1585            }
1586            if (comma == -1) {
1587                return space;
1588            }
1589            return (comma > space) ? space : comma;
1590        }
1591
1592        private VectorDrawable.VNode[] parsePath(String value) {
1593            int start = 0;
1594            int end = 1;
1595
1596            ArrayList<VectorDrawable.VNode> list = new ArrayList<VectorDrawable.VNode>();
1597            while (end < value.length()) {
1598                end = nextStart(value, end);
1599                String s = value.substring(start, end);
1600                float[] val = getFloats(s);
1601                addNode(list, s.charAt(0), val);
1602
1603                start = end;
1604                end++;
1605            }
1606            if ((end - start) == 1 && start < value.length()) {
1607
1608                addNode(list, value.charAt(start), new float[0]);
1609            }
1610            return list.toArray(new VectorDrawable.VNode[list.size()]);
1611        }
1612
1613        public void copyFrom(VPath p1) {
1614            mNode = new VNode[p1.mNode.length];
1615            for (int i = 0; i < mNode.length; i++) {
1616                mNode[i] = new VNode(p1.mNode[i]);
1617            }
1618            mId = p1.mId;
1619            mStrokeColor = p1.mStrokeColor;
1620            mFillColor = p1.mFillColor;
1621            mStrokeWidth = p1.mStrokeWidth;
1622            mRotate = p1.mRotate;
1623            mPivotX = p1.mPivotX;
1624            mPivotY = p1.mPivotY;
1625            mAnimated = p1.mAnimated;
1626            mTrimPathStart = p1.mTrimPathStart;
1627            mTrimPathEnd = p1.mTrimPathEnd;
1628            mTrimPathOffset = p1.mTrimPathOffset;
1629            mStrokelineCap = p1.mStrokelineCap;
1630            mStrokelineJoin = p1.mStrokelineJoin;
1631            mStrokeMiterlimit = p1.mStrokeMiterlimit;
1632            mNumberOfStates = p1.mNumberOfStates;
1633            for (int i = 0; i < mNumberOfStates; i++) {
1634                mCheckState[i] = p1.mCheckState[i];
1635                mCheckValue[i] = p1.mCheckValue[i];
1636            }
1637
1638            mFillRule = p1.mFillRule;
1639        }
1640
1641        public static VPath interpolate(float t, VPath p1, VPath p2, VPath returnPath, int limit) {
1642            if (limit == LIMIT_ALL || limit == LIMIT_PATH) {
1643                if (returnPath.mNode == null || returnPath.mNode.length != p1.mNode.length) {
1644                    returnPath.mNode = new VNode[p1.mNode.length];
1645                }
1646                for (int i = 0; i < returnPath.mNode.length; i++) {
1647                    if (returnPath.mNode[i] == null) {
1648                        returnPath.mNode[i] = new VNode(p1.mNode[i], p2.mNode[i], t);
1649                    } else {
1650                        returnPath.mNode[i].interpolate(p1.mNode[i], p2.mNode[i], t);
1651                    }
1652                }
1653            }
1654            float t1 = 1 - t;
1655            switch (limit) {
1656                case LIMIT_ALL:
1657                    returnPath.mRotate = t1 * p1.mRotate + t * p2.mRotate;
1658                    returnPath.mPivotX = t1 * p1.mPivotX + t * p2.mPivotX;
1659                    returnPath.mPivotY = t1 * p1.mPivotY + t * p2.mPivotY;
1660                    returnPath.mClip = p1.mClip | p2.mClip;
1661
1662                    returnPath.mTrimPathStart = t1 * p1.mTrimPathStart + t * p2.mTrimPathStart;
1663                    returnPath.mTrimPathEnd = t1 * p1.mTrimPathEnd + t * p2.mTrimPathEnd;
1664                    returnPath.mTrimPathOffset = t1 * p1.mTrimPathOffset + t * p2.mTrimPathOffset;
1665                    returnPath.mStrokeMiterlimit =
1666                            t1 * p1.mStrokeMiterlimit + t * p2.mStrokeMiterlimit;
1667                    returnPath.mStrokelineCap = p1.mStrokelineCap;
1668                    if (returnPath.mStrokelineCap == null) {
1669                        returnPath.mStrokelineCap = p2.mStrokelineCap;
1670                    }
1671                    returnPath.mStrokelineJoin = p1.mStrokelineJoin;
1672                    if (returnPath.mStrokelineJoin == null) {
1673                        returnPath.mStrokelineJoin = p2.mStrokelineJoin;
1674                    }
1675                    returnPath.mFillRule = p1.mFillRule;
1676
1677                    returnPath.mStrokeColor = rgbInterpolate(t, p1.mStrokeColor, p2.mStrokeColor);
1678                    returnPath.mFillColor = rgbInterpolate(t, p1.mFillColor, p2.mFillColor);
1679                    returnPath.mStrokeWidth = t1 * p1.mStrokeWidth + t * p2.mStrokeWidth;
1680                    returnPath.mNumberOfStates = p1.mNumberOfStates;
1681                    for (int i = 0; i < returnPath.mNumberOfStates; i++) {
1682                        returnPath.mCheckState[i] = p1.mCheckState[i];
1683                        returnPath.mCheckValue[i] = p1.mCheckValue[i];
1684                    }
1685                    for (int i = 0; i < p2.mNumberOfStates; i++) {
1686                        returnPath.addStateFilter(p2.mCheckState[i], p2.mCheckValue[i]);
1687                    }
1688
1689                    int count = 0;
1690                    for (int i = 0; i < returnPath.mNumberOfStates; i++) {
1691                        if (returnPath.mCheckValue[i]) {
1692                            count++;
1693                        }
1694                    }
1695                    returnPath.mNumberOfTrue = count;
1696                    break;
1697                case LIMIT_ROTATE:
1698                    returnPath.mRotate = t1 * p1.mRotate + t * p2.mRotate;
1699                    break;
1700                case LIMIT_TRIM_PATH_END:
1701                    returnPath.mTrimPathEnd = t1 * p1.mTrimPathEnd + t * p2.mTrimPathEnd;
1702                    break;
1703                case LIMIT_TRIM_PATH_OFFSET:
1704                    returnPath.mTrimPathOffset = t1 * p1.mTrimPathOffset + t * p2.mTrimPathOffset;
1705                    break;
1706                case LIMIT_TRIM_PATH_START:
1707                    returnPath.mTrimPathStart = t1 * p1.mTrimPathStart + t * p2.mTrimPathStart;
1708                    break;
1709            }
1710            return returnPath;
1711        }
1712
1713        private static int rgbInterpolate(float t, int color1, int color2) {
1714            int ret;
1715            if (color1 == color2) {
1716                return color2;
1717            }
1718            if (color1 == 0) {
1719                return color2;
1720            }
1721            if (color2 == 0) {
1722                return color1;
1723            }
1724
1725            float t1 = 1 - t;
1726            ret = 0xFF & (((int) ((color1 & 0xFF) * t1 + (color2 & 0xFF) * t)));
1727            color1 >>= 8;
1728                    color2 >>= 8;
1729
1730                    ret |= 0xFF00 & (((int) ((color1 & 0xFF) * t1 + (color2 & 0xFF) * t)) << 8);
1731                    color1 >>= 8;
1732                    color2 >>= 8;
1733            ret |= 0xFF0000 & (((int) ((color1 & 0xFF) * t1 + (color2 & 0xFF) * t)) << 16);
1734            color1 >>= 8;
1735            color2 >>= 8;
1736            ret |= 0xFF000000 & (((int) ((color1 & 0xFF) * t1 + (color2 & 0xFF) * t)) << 24);
1737
1738            return ret;
1739        }
1740
1741        public boolean isVisible(int[] state) {
1742            int match = 0;
1743            for (int i = 0; i < state.length; i++) {
1744                int v = getState(state[i]);
1745                if (v != STATE_UNDEFINED) {
1746                    if (v==STATE_TRUE) {
1747                        match++;
1748                    } else {
1749                        return false;
1750                    }
1751                }
1752            }
1753            return match == mNumberOfTrue;
1754        }
1755    }
1756
1757    private static class VNode {
1758        private char mType;
1759        private float[] mParams;
1760
1761        public VNode(char type, float[] params) {
1762            mType = type;
1763            mParams = params;
1764        }
1765
1766        public VNode(VNode n) {
1767            mType = n.mType;
1768            mParams = Arrays.copyOf(n.mParams, n.mParams.length);
1769        }
1770
1771        public VNode(VNode n1, VNode n2, float t) {
1772            mType = n1.mType;
1773            mParams = new float[n1.mParams.length];
1774            interpolate(n1, n2, t);
1775        }
1776
1777        private boolean match(VNode n) {
1778            if (n.mType != mType) {
1779                return false;
1780            }
1781            return (mParams.length == n.mParams.length);
1782        }
1783
1784        public void interpolate(VNode n1, VNode n2, float t) {
1785            for (int i = 0; i < n1.mParams.length; i++) {
1786                mParams[i] = n1.mParams[i] * (1 - t) + n2.mParams[i] * t;
1787            }
1788        }
1789
1790        private void nodeListToPath(VNode[] node, Path path) {
1791            float[] current = new float[4];
1792            for (int i = 0; i < node.length; i++) {
1793                addCommand(path, current, node[i].mType, node[i].mParams);
1794            }
1795        }
1796
1797        public static void createPath(VNode[] node, Path path) {
1798            float[] current = new float[4];
1799            for (int i = 0; i < node.length; i++) {
1800                addCommand(path, current, node[i].mType, node[i].mParams);
1801            }
1802        }
1803
1804        private static void addCommand(Path path, float[] current, char cmd, float[] val) {
1805
1806            int incr = 2;
1807            float currentX = current[0];
1808            float currentY = current[1];
1809            float ctrlPointX = current[2];
1810            float ctrlPointY = current[3];
1811
1812            switch (cmd) {
1813                case 'z':
1814                case 'Z':
1815                    path.close();
1816                    return;
1817                case 'm':
1818                case 'M':
1819                case 'l':
1820                case 'L':
1821                case 't':
1822                case 'T':
1823                    incr = 2;
1824                    break;
1825                case 'h':
1826                case 'H':
1827                case 'v':
1828                case 'V':
1829                    incr = 1;
1830                    break;
1831                case 'c':
1832                case 'C':
1833                    incr = 6;
1834                    break;
1835                case 's':
1836                case 'S':
1837                case 'q':
1838                case 'Q':
1839                    incr = 4;
1840                    break;
1841                case 'a':
1842                case 'A':
1843                    incr = 7;
1844                    break;
1845            }
1846            for (int k = 0; k < val.length; k += incr) {
1847                // TODO: build test to prove all permutations work
1848                switch (cmd) {
1849                    case 'm': // moveto - Start a new sub-path (relative)
1850                        path.rMoveTo(val[k + 0], val[k + 1]);
1851                        currentX += val[k + 0];
1852                        currentY += val[k + 1];
1853                        break;
1854                    case 'M': // moveto - Start a new sub-path
1855                        path.moveTo(val[k + 0], val[k + 1]);
1856                        currentX = val[k + 0];
1857                        currentY = val[k + 1];
1858                        break;
1859                    case 'l': // lineto - Draw a line from the current point (relative)
1860                        path.rLineTo(val[k + 0], val[k + 1]);
1861                        currentX += val[k + 0];
1862                        currentY += val[k + 1];
1863                        break;
1864                    case 'L': // lineto - Draw a line from the current point
1865                        path.lineTo(val[k + 0], val[k + 1]);
1866                        currentX = val[k + 0];
1867                        currentY = val[k + 1];
1868                        break;
1869                    case 'z': // closepath - Close the current subpath
1870                    case 'Z': // closepath - Close the current subpath
1871                        path.close();
1872                        break;
1873                    case 'h': // horizontal lineto - Draws a horizontal line (relative)
1874                        path.rLineTo(val[k + 0], 0);
1875                        currentX += val[k + 0];
1876                        break;
1877                    case 'H': // horizontal lineto - Draws a horizontal line
1878                        path.lineTo(val[k + 0], currentY);
1879                        currentX = val[k + 0];
1880                        break;
1881                    case 'v': // vertical lineto - Draws a vertical line from the current point (r)
1882                        path.rLineTo(0, val[k + 0]);
1883                        currentY += val[k + 0];
1884                        break;
1885                    case 'V': // vertical lineto - Draws a vertical line from the current point
1886                        path.lineTo(currentX, val[k + 0]);
1887                        currentY = val[k + 0];
1888                        break;
1889                    case 'c': // curveto - Draws a cubic Bézier curve (relative)
1890                        path.rCubicTo(val[k + 0],
1891                                val[k + 1],
1892                                val[k + 2],
1893                                val[k + 3],
1894                                val[k + 4],
1895                                val[k + 5]);
1896
1897                        ctrlPointX = currentX + val[k + 2];
1898                        ctrlPointY = currentY + val[k + 3];
1899                        currentX += val[k + 4];
1900                        currentY += val[k + 5];
1901
1902                        break;
1903                    case 'C': // curveto - Draws a cubic Bézier curve
1904                        path.cubicTo(val[k + 0],
1905                                val[k + 1],
1906                                val[k + 2],
1907                                val[k + 3],
1908                                val[k + 4],
1909                                val[k + 5]);
1910                        currentX = val[k + 4];
1911                        currentY = val[k + 5];
1912                        ctrlPointX = val[k + 2];
1913                        ctrlPointY = val[k + 3];
1914
1915                        break;
1916                    case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
1917                        path.rCubicTo(currentX - ctrlPointX, currentY  - ctrlPointY,
1918                                val[k + 0], val[k + 1],
1919                                val[k + 2], val[k + 3]);
1920
1921                        ctrlPointX = currentX + val[k + 0];
1922                        ctrlPointY = currentY + val[k + 1];
1923                        currentX += val[k + 2];
1924                        currentY += val[k + 3];
1925                        break;
1926                    case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
1927                        path.cubicTo(2 * currentX - ctrlPointX,
1928                                2 * currentY - ctrlPointY,
1929                                val[k + 0],
1930                                val[k + 1],
1931                                val[k + 2],
1932                                val[k + 3]);
1933                        currentX = val[k + 2];
1934                        currentY = val[k + 3];
1935                        ctrlPointX = val[k + 0];
1936                        ctrlPointY = val[k + 1];
1937                        break;
1938                    case 'q': // Draws a quadratic Bézier (relative)
1939                        path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
1940                        currentX += val[k + 2];
1941                        currentY += val[k + 3];
1942                        ctrlPointX = val[k + 0];
1943                        ctrlPointY = val[k + 1];
1944                        break;
1945                    case 'Q': // Draws a quadratic Bézier
1946                        path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
1947                        currentX = val[k + 2];
1948                        currentY = val[k + 3];
1949                        ctrlPointX = val[k + 0];
1950                        ctrlPointY = val[k + 1];
1951                        break;
1952                    case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
1953                        path.rQuadTo(currentX - ctrlPointX, currentY - ctrlPointY,
1954                                val[k + 0], val[k + 1]);
1955                        ctrlPointX = ctrlPointX + currentX;
1956                        ctrlPointY = ctrlPointY + currentY;
1957                        currentX += val[k + 0];
1958                        currentY += val[k + 1];
1959
1960                        break;
1961                    case 'T': // Draws a quadratic Bézier curve (reflective control point)
1962                        path.quadTo(currentX * 2 - ctrlPointX, currentY * 2 - ctrlPointY,
1963                                val[k + 0], val[k + 1]);
1964                        currentX = val[k + 0];
1965                        currentY = val[k + 1]; // TODO: Check this logic
1966                        ctrlPointX = -(val[k + 0] - currentX);
1967                        ctrlPointY = -(val[k + 1] - currentY);
1968                        break;
1969                    case 'a': // Draws an elliptical arc
1970                        // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
1971                        drawArc(path,
1972                                currentX,
1973                                currentY,
1974                                val[k + 5] + currentX,
1975                                val[k + 6] + currentY,
1976                                val[k + 0],
1977                                val[k + 1],
1978                                val[k + 2],
1979                                val[k + 3] != 0,
1980                                val[k + 4] != 0);
1981                        currentX += val[k + 5];
1982                        currentY += val[k + 6];
1983                        ctrlPointX = currentX;
1984                        ctrlPointY = currentY;
1985
1986                        break;
1987                    case 'A': // Draws an elliptical arc
1988                        drawArc(path,
1989                                currentX,
1990                                currentY,
1991                                val[k + 5],
1992                                val[k + 6],
1993                                val[k + 0],
1994                                val[k + 1],
1995                                val[k + 2],
1996                                val[k + 3] != 0,
1997                                val[k + 4] != 0);
1998                        currentX = val[k + 5];
1999                        currentY = val[k + 6];
2000                        ctrlPointX = currentX;
2001                        ctrlPointY = currentY;
2002                        break;
2003                }
2004            }
2005            current[0] = currentX;
2006            current[1] = currentY;
2007            current[2] = ctrlPointX;
2008            current[3] = ctrlPointY;
2009        }
2010
2011        private static void drawArc(Path p,
2012                float x0,
2013                float y0,
2014                float x1,
2015                float y1,
2016                float a,
2017                float b,
2018                float theta,
2019                boolean isMoreThanHalf,
2020                boolean isPositiveArc) {
2021
2022            /* Convert rotation angle from degrees to radians */
2023            double thetaD = Math.toRadians(theta);
2024            /* Pre-compute rotation matrix entries */
2025            double cosTheta = Math.cos(thetaD);
2026            double sinTheta = Math.sin(thetaD);
2027            /* Transform (x0, y0) and (x1, y1) into unit space */
2028            /* using (inverse) rotation, followed by (inverse) scale */
2029            double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
2030            double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
2031            double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
2032            double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
2033
2034            /* Compute differences and averages */
2035            double dx = x0p - x1p;
2036            double dy = y0p - y1p;
2037            double xm = (x0p + x1p) / 2;
2038            double ym = (y0p + y1p) / 2;
2039            /* Solve for intersecting unit circles */
2040            double dsq = dx * dx + dy * dy;
2041            if (dsq == 0.0) {
2042                Log.w(LOGTAG, " Points are coincident");
2043                return; /* Points are coincident */
2044            }
2045            double disc = 1.0 / dsq - 1.0 / 4.0;
2046            if (disc < 0.0) {
2047                Log.w(LOGTAG, "Points are too far apart " + dsq);
2048                float adjust = (float) (Math.sqrt(dsq) / 1.99999);
2049                drawArc(p, x0, y0, x1, y1, a * adjust,
2050                        b * adjust, theta, isMoreThanHalf, isPositiveArc);
2051                return; /* Points are too far apart */
2052            }
2053            double s = Math.sqrt(disc);
2054            double sdx = s * dx;
2055            double sdy = s * dy;
2056            double cx;
2057            double cy;
2058            if (isMoreThanHalf == isPositiveArc) {
2059                cx = xm - sdy;
2060                cy = ym + sdx;
2061            } else {
2062                cx = xm + sdy;
2063                cy = ym - sdx;
2064            }
2065
2066            double eta0 = Math.atan2((y0p - cy), (x0p - cx));
2067
2068            double eta1 = Math.atan2((y1p - cy), (x1p - cx));
2069
2070            double sweep = (eta1 - eta0);
2071            if (isPositiveArc != (sweep >= 0)) {
2072                if (sweep > 0) {
2073                    sweep -= 2 * Math.PI;
2074                } else {
2075                    sweep += 2 * Math.PI;
2076                }
2077            }
2078
2079            cx *= a;
2080            cy *= b;
2081            double tcx = cx;
2082            cx = cx * cosTheta - cy * sinTheta;
2083            cy = tcx * sinTheta + cy * cosTheta;
2084
2085            arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
2086        }
2087
2088        /**
2089         * Converts an arc to cubic Bezier segments and records them in p.
2090         *
2091         * @param p The target for the cubic Bezier segments
2092         * @param cx The x coordinate center of the ellipse
2093         * @param cy The y coordinate center of the ellipse
2094         * @param a The radius of the ellipse in the horizontal direction
2095         * @param b The radius of the ellipse in the vertical direction
2096         * @param e1x E(eta1) x coordinate of the starting point of the arc
2097         * @param e1y E(eta2) y coordinate of the starting point of the arc
2098         * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
2099         * @param start The start angle of the arc on the ellipse
2100         * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
2101         */
2102        private static void arcToBezier(Path p,
2103                double cx,
2104                double cy,
2105                double a,
2106                double b,
2107                double e1x,
2108                double e1y,
2109                double theta,
2110                double start,
2111                double sweep) {
2112            // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
2113            // and http://www.spaceroots.org/documents/ellipse/node22.html
2114
2115            // Maximum of 45 degrees per cubic Bezier segment
2116            int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI));
2117
2118            double eta1 = start;
2119            double cosTheta = Math.cos(theta);
2120            double sinTheta = Math.sin(theta);
2121            double cosEta1 = Math.cos(eta1);
2122            double sinEta1 = Math.sin(eta1);
2123            double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
2124            double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
2125
2126            double anglePerSegment = sweep / numSegments;
2127            for (int i = 0; i < numSegments; i++) {
2128                double eta2 = eta1 + anglePerSegment;
2129                double sinEta2 = Math.sin(eta2);
2130                double cosEta2 = Math.cos(eta2);
2131                double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
2132                double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
2133                double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
2134                double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
2135                double tanDiff2 = Math.tan((eta2 - eta1) / 2);
2136                double alpha =
2137                        Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
2138                double q1x = e1x + alpha * ep1x;
2139                double q1y = e1y + alpha * ep1y;
2140                double q2x = e2x - alpha * ep2x;
2141                double q2y = e2y - alpha * ep2y;
2142
2143                p.cubicTo((float) q1x,
2144                        (float) q1y,
2145                        (float) q2x,
2146                        (float) q2y,
2147                        (float) e2x,
2148                        (float) e2y);
2149                eta1 = eta2;
2150                e1x = e2x;
2151                e1y = e2y;
2152                ep1x = ep2x;
2153                ep1y = ep2y;
2154            }
2155        }
2156
2157    }
2158}
2159