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