VectorDrawable.java revision 482eb53c8a7d5180a2d0ce3b0a779c18960aa8cd
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.content.res.ColorStateList;
18import android.content.res.Resources;
19import android.content.res.Resources.Theme;
20import android.content.res.TypedArray;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.ColorFilter;
25import android.graphics.Matrix;
26import android.graphics.Paint;
27import android.graphics.Path;
28import android.graphics.PathMeasure;
29import android.graphics.PixelFormat;
30import android.graphics.PorterDuffColorFilter;
31import android.graphics.Rect;
32import android.graphics.Region;
33import android.graphics.PorterDuff.Mode;
34import android.util.ArrayMap;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.util.PathParser;
38import android.util.Xml;
39
40import com.android.internal.R;
41
42import org.xmlpull.v1.XmlPullParser;
43import org.xmlpull.v1.XmlPullParserException;
44import org.xmlpull.v1.XmlPullParserFactory;
45
46import java.io.IOException;
47import java.util.ArrayList;
48import java.util.Stack;
49
50/**
51 * This lets you create a drawable based on an XML vector graphic It can be
52 * defined in an XML file with the <code>&lt;vector></code> element.
53 * <p/>
54 * The vector drawable has the following elements:
55 * <p/>
56 * <dl>
57 * <dt><code>&lt;vector></code></dt>
58 * <dd>Used to defined a vector drawable</dd>
59 * <dt><code>&lt;size></code></dt>
60 * <dd>Used to defined the intrinsic Width Height size of the drawable using
61 * <code>android:width</code> and <code>android:height</code></dd>
62 * <dt><code>&lt;viewport></code></dt>
63 * <dd>Used to defined the size of the virtual canvas the paths are drawn on.
64 * The size is defined using the attributes <code>android:viewportHeight</code>
65 * <code>android:viewportWidth</code></dd>
66 * <dt><code>&lt;group></code></dt>
67 * <dd>Defines a group of paths or subgroups, plus transformation information.
68 * The transformations are defined in the same coordinates as the viewport.
69 * And the transformations are applied in the order of scale, rotate then translate. </dd>
70 * <dt><code>android:rotation</code>
71 * <dd>The degrees of rotation of the group.</dd></dt>
72 * <dt><code>android:pivotX</code>
73 * <dd>The X coordinate of the pivot for the scale and rotation of the group</dd></dt>
74 * <dt><code>android:pivotY</code>
75 * <dd>The Y coordinate of the pivot for the scale and rotation of the group</dd></dt>
76 * <dt><code>android:scaleX</code>
77 * <dd>The amount of scale on the X Coordinate</dd></dt>
78 * <dt><code>android:scaleY</code>
79 * <dd>The amount of scale on the Y coordinate</dd></dt>
80 * <dt><code>android:translateX</code>
81 * <dd>The amount of translation on the X coordinate</dd></dt>
82 * <dt><code>android:translateY</code>
83 * <dd>The amount of translation on the Y coordinate</dd></dt>
84 * <dt><code>&lt;path></code></dt>
85 * <dd>Defines paths to be drawn.
86 * <dl>
87 * <dt><code>android:name</code>
88 * <dd>Defines the name of the path.</dd></dt>
89 * <dt><code>android:pathData</code>
90 * <dd>Defines path string. This is using exactly same format as "d" attribute
91 * in the SVG's path data</dd></dt>
92 * <dt><code>android:fill</code>
93 * <dd>Defines the color to fill the path (none if not present).</dd></dt>
94 * <dt><code>android:stroke</code>
95 * <dd>Defines the color to draw the path outline (none if not present).</dd>
96 * </dt>
97 * <dt><code>android:strokeWidth</code>
98 * <dd>The width a path stroke</dd></dt>
99 * <dt><code>android:strokeOpacity</code>
100 * <dd>The opacity of a path stroke</dd></dt>
101 * <dt><code>android:fillOpacity</code>
102 * <dd>The opacity to fill the path with</dd></dt>
103 * <dt><code>android:trimPathStart</code>
104 * <dd>The fraction of the path to trim from the start from 0 to 1</dd></dt>
105 * <dt><code>android:trimPathEnd</code>
106 * <dd>The fraction of the path to trim from the end from 0 to 1</dd></dt>
107 * <dt><code>android:trimPathOffset</code>
108 * <dd>Shift trim region (allows showed region to include the start and end)
109 * from 0 to 1</dd></dt>
110 * <dt><code>android:clipToPath</code>
111 * <dd>Path will set the clip path</dd></dt>
112 * <dt><code>android:strokeLineCap</code>
113 * <dd>Sets the linecap for a stroked path: butt, round, square</dd></dt>
114 * <dt><code>android:strokeLineJoin</code>
115 * <dd>Sets the lineJoin for a stroked path: miter,round,bevel</dd></dt>
116 * <dt><code>android:strokeMiterLimit</code>
117 * <dd>Sets the Miter limit for a stroked path</dd></dt>
118 * </dl>
119 * </dd>
120 */
121public class VectorDrawable extends Drawable {
122    private static final String LOGTAG = VectorDrawable.class.getSimpleName();
123
124    private static final String SHAPE_SIZE = "size";
125    private static final String SHAPE_VIEWPORT = "viewport";
126    private static final String SHAPE_GROUP = "group";
127    private static final String SHAPE_PATH = "path";
128    private static final String SHAPE_VECTOR = "vector";
129
130    private static final int LINECAP_BUTT = 0;
131    private static final int LINECAP_ROUND = 1;
132    private static final int LINECAP_SQUARE = 2;
133
134    private static final int LINEJOIN_MITER = 0;
135    private static final int LINEJOIN_ROUND = 1;
136    private static final int LINEJOIN_BEVEL = 2;
137
138    private static final boolean DBG_VECTOR_DRAWABLE = false;
139
140    private VectorDrawableState mVectorState;
141
142    private PorterDuffColorFilter mTintFilter;
143
144    private boolean mMutated;
145
146    // AnimatedVectorDrawable needs to turn off the cache all the time, otherwise,
147    // caching the bitmap by default is allowed.
148    private boolean mAllowCaching = true;
149
150    public VectorDrawable() {
151        mVectorState = new VectorDrawableState();
152    }
153
154    private VectorDrawable(VectorDrawableState state, Resources res, Theme theme) {
155        if (theme != null && state.canApplyTheme()) {
156            // If we need to apply a theme, implicitly mutate.
157            mVectorState = new VectorDrawableState(state);
158            applyTheme(theme);
159        } else {
160            mVectorState = state;
161        }
162
163        mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
164        mVectorState.mVPathRenderer.setColorFilter(mTintFilter);
165    }
166
167    @Override
168    public Drawable mutate() {
169        if (!mMutated && super.mutate() == this) {
170            mVectorState = new VectorDrawableState(mVectorState);
171            mMutated = true;
172        }
173        return this;
174    }
175
176    Object getTargetByName(String name) {
177        return mVectorState.mVPathRenderer.mVGTargetsMap.get(name);
178    }
179
180    @Override
181    public ConstantState getConstantState() {
182        mVectorState.mChangingConfigurations = getChangingConfigurations();
183        return mVectorState;
184    }
185
186    @Override
187    public void draw(Canvas canvas) {
188        final int saveCount = canvas.save();
189        final Rect bounds = getBounds();
190        canvas.translate(bounds.left, bounds.top);
191
192        if (!mAllowCaching) {
193            mVectorState.mVPathRenderer.draw(canvas, bounds.width(), bounds.height());
194        } else {
195            Bitmap bitmap = mVectorState.mCachedBitmap;
196            if (bitmap == null || !mVectorState.canReuseCache(bounds.width(),
197                    bounds.height())) {
198                bitmap = Bitmap.createBitmap(bounds.width(), bounds.height(),
199                        Bitmap.Config.ARGB_8888);
200                Canvas tmpCanvas = new Canvas(bitmap);
201                mVectorState.mVPathRenderer.draw(tmpCanvas, bounds.width(), bounds.height());
202                mVectorState.mCachedBitmap = bitmap;
203
204                mVectorState.updateCacheStates();
205            }
206            canvas.drawBitmap(bitmap, null, bounds, null);
207        }
208        canvas.restoreToCount(saveCount);
209    }
210
211    @Override
212    public int getAlpha() {
213        return mVectorState.mVPathRenderer.getRootAlpha();
214    }
215
216    @Override
217    public void setAlpha(int alpha) {
218        if (mVectorState.mVPathRenderer.getRootAlpha() != alpha) {
219            mVectorState.mVPathRenderer.setRootAlpha(alpha);
220            invalidateSelf();
221        }
222    }
223
224    @Override
225    public void setColorFilter(ColorFilter colorFilter) {
226        final VectorDrawableState state = mVectorState;
227        if (colorFilter != null) {
228            // Color filter overrides tint.
229            mTintFilter = null;
230        } else if (state.mTint != null && state.mTintMode != null) {
231            // Restore the tint filter, if we need one.
232            final int color = state.mTint.getColorForState(getState(), Color.TRANSPARENT);
233            mTintFilter = new PorterDuffColorFilter(color, state.mTintMode);
234            colorFilter = mTintFilter;
235        }
236
237        state.mVPathRenderer.setColorFilter(colorFilter);
238        invalidateSelf();
239    }
240
241    @Override
242    public void setTint(ColorStateList tint, Mode tintMode) {
243        final VectorDrawableState state = mVectorState;
244        if (state.mTint != tint || state.mTintMode != tintMode) {
245            state.mTint = tint;
246            state.mTintMode = tintMode;
247
248            mTintFilter = updateTintFilter(mTintFilter, tint, tintMode);
249            mVectorState.mVPathRenderer.setColorFilter(mTintFilter);
250            invalidateSelf();
251        }
252    }
253
254    @Override
255    protected boolean onStateChange(int[] stateSet) {
256        final VectorDrawableState state = mVectorState;
257        if (state.mTint != null && state.mTintMode != null) {
258            mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
259            mVectorState.mVPathRenderer.setColorFilter(mTintFilter);
260            return true;
261        }
262        return false;
263    }
264
265    @Override
266    public int getOpacity() {
267        return PixelFormat.TRANSLUCENT;
268    }
269
270    @Override
271    public int getIntrinsicWidth() {
272        return (int) mVectorState.mVPathRenderer.mBaseWidth;
273    }
274
275    @Override
276    public int getIntrinsicHeight() {
277        return (int) mVectorState.mVPathRenderer.mBaseHeight;
278    }
279
280    @Override
281    public boolean canApplyTheme() {
282        return super.canApplyTheme() || mVectorState != null && mVectorState.canApplyTheme();
283    }
284
285    @Override
286    public void applyTheme(Theme t) {
287        super.applyTheme(t);
288
289        final VectorDrawableState state = mVectorState;
290        final VPathRenderer path = state.mVPathRenderer;
291        if (path != null && path.canApplyTheme()) {
292            path.applyTheme(t);
293        }
294    }
295
296    /** @hide */
297    public static VectorDrawable create(Resources resources, int rid) {
298        try {
299            final XmlPullParser xpp = resources.getXml(rid);
300            final AttributeSet attrs = Xml.asAttributeSet(xpp);
301            final XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
302            factory.setNamespaceAware(true);
303
304            final VectorDrawable drawable = new VectorDrawable();
305            drawable.inflate(resources, xpp, attrs);
306
307            return drawable;
308        } catch (XmlPullParserException e) {
309            Log.e(LOGTAG, "parser error", e);
310        } catch (IOException e) {
311            Log.e(LOGTAG, "parser error", e);
312        }
313        return null;
314    }
315
316    private static int applyAlpha(int color, float alpha) {
317        int alphaBytes = Color.alpha(color);
318        color &= 0x00FFFFFF;
319        color |= ((int) (alphaBytes * alpha)) << 24;
320        return color;
321    }
322
323    @Override
324    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
325            throws XmlPullParserException, IOException {
326        final TypedArray a = obtainAttributes(res, theme, attrs, R.styleable.VectorDrawable);
327        updateStateFromTypedArray(a);
328        a.recycle();
329
330        final VectorDrawableState state = mVectorState;
331        inflateInternal(res, parser, attrs, theme);
332
333        mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
334        state.mVPathRenderer.setColorFilter(mTintFilter);
335    }
336
337    private void updateStateFromTypedArray(TypedArray a) {
338        final VectorDrawableState state = mVectorState;
339
340        // Account for any configuration changes.
341        state.mChangingConfigurations |= a.getChangingConfigurations();
342
343        // Extract the theme attributes, if any.
344        state.mThemeAttrs = a.extractThemeAttrs();
345
346        final int tintMode = a.getInt(R.styleable.VectorDrawable_tintMode, -1);
347        if (tintMode != -1) {
348            state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN);
349        }
350
351        final ColorStateList tint = a.getColorStateList(R.styleable.VectorDrawable_tint);
352        if (tint != null) {
353            state.mTint = tint;
354        }
355    }
356
357    private void inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs,
358            Theme theme) throws XmlPullParserException, IOException {
359        final VectorDrawableState state = mVectorState;
360        final VPathRenderer pathRenderer = new VPathRenderer();
361        state.mVPathRenderer = pathRenderer;
362
363        boolean noSizeTag = true;
364        boolean noViewportTag = true;
365        boolean noPathTag = true;
366
367        // Use a stack to help to build the group tree.
368        // The top of the stack is always the current group.
369        final Stack<VGroup> groupStack = new Stack<VGroup>();
370        groupStack.push(pathRenderer.mRootGroup);
371
372        int eventType = parser.getEventType();
373        while (eventType != XmlPullParser.END_DOCUMENT) {
374            if (eventType == XmlPullParser.START_TAG) {
375                final String tagName = parser.getName();
376                final VGroup currentGroup = groupStack.peek();
377
378                if (SHAPE_PATH.equals(tagName)) {
379                    final VPath path = new VPath();
380                    path.inflate(res, attrs, theme);
381                    currentGroup.mChildren.add(path);
382                    if (path.getPathName() != null) {
383                        pathRenderer.mVGTargetsMap.put(path.getPathName(), path);
384                    }
385                    noPathTag = false;
386                    state.mChangingConfigurations |= path.mChangingConfigurations;
387                } else if (SHAPE_SIZE.equals(tagName)) {
388                    pathRenderer.parseSize(res, attrs);
389                    noSizeTag = false;
390                    state.mChangingConfigurations |= pathRenderer.mChangingConfigurations;
391                } else if (SHAPE_VIEWPORT.equals(tagName)) {
392                    pathRenderer.parseViewport(res, attrs);
393                    noViewportTag = false;
394                    state.mChangingConfigurations |= pathRenderer.mChangingConfigurations;
395                } else if (SHAPE_GROUP.equals(tagName)) {
396                    VGroup newChildGroup = new VGroup();
397                    newChildGroup.inflate(res, attrs, theme);
398                    currentGroup.mChildren.add(newChildGroup);
399                    groupStack.push(newChildGroup);
400                    if (newChildGroup.getGroupName() != null) {
401                        pathRenderer.mVGTargetsMap.put(newChildGroup.getGroupName(),
402                                newChildGroup);
403                    }
404                    state.mChangingConfigurations |= newChildGroup.mChangingConfigurations;
405                }
406            } else if (eventType == XmlPullParser.END_TAG) {
407                final String tagName = parser.getName();
408                if (SHAPE_GROUP.equals(tagName)) {
409                    groupStack.pop();
410                }
411            }
412            eventType = parser.next();
413        }
414
415        // Print the tree out for debug.
416        if (DBG_VECTOR_DRAWABLE) {
417            printGroupTree(pathRenderer.mRootGroup, 0);
418        }
419
420        if (noSizeTag || noViewportTag || noPathTag) {
421            final StringBuffer tag = new StringBuffer();
422
423            if (noSizeTag) {
424                tag.append(SHAPE_SIZE);
425            }
426
427            if (noViewportTag) {
428                if (tag.length() > 0) {
429                    tag.append(" & ");
430                }
431                tag.append(SHAPE_SIZE);
432            }
433
434            if (noPathTag) {
435                if (tag.length() > 0) {
436                    tag.append(" or ");
437                }
438                tag.append(SHAPE_PATH);
439            }
440
441            throw new XmlPullParserException("no " + tag + " defined");
442        }
443    }
444
445    private void printGroupTree(VGroup currentGroup, int level) {
446        String indent = "";
447        for (int i = 0; i < level; i++) {
448            indent += "    ";
449        }
450        // Print the current node
451        Log.v(LOGTAG, indent + "current group is :" + currentGroup.getGroupName()
452                + " rotation is " + currentGroup.mRotate);
453        Log.v(LOGTAG, indent + "matrix is :" + currentGroup.getLocalMatrix().toString());
454        // Then print all the children groups
455        for (int i = 0; i < currentGroup.mChildren.size(); i++) {
456            Object child = currentGroup.mChildren.get(i);
457            if (child instanceof VGroup) {
458                printGroupTree((VGroup) child, level + 1);
459            }
460        }
461    }
462
463    @Override
464    public int getChangingConfigurations() {
465        return super.getChangingConfigurations() | mVectorState.mChangingConfigurations;
466    }
467
468    void setAllowCaching(boolean allowCaching) {
469        mAllowCaching = allowCaching;
470    }
471
472    private static class VectorDrawableState extends ConstantState {
473        int[] mThemeAttrs;
474        int mChangingConfigurations;
475        VPathRenderer mVPathRenderer;
476        ColorStateList mTint;
477        Mode mTintMode;
478
479        Bitmap mCachedBitmap;
480        int[] mCachedThemeAttrs;
481        ColorStateList mCachedTint;
482        Mode mCachedTintMode;
483        int mCachedRootAlpha;
484
485        // Deep copy for mutate() or implicitly mutate.
486        public VectorDrawableState(VectorDrawableState copy) {
487            if (copy != null) {
488                mThemeAttrs = copy.mThemeAttrs;
489                mChangingConfigurations = copy.mChangingConfigurations;
490                mVPathRenderer = new VPathRenderer(copy.mVPathRenderer);
491                mTint = copy.mTint;
492                mTintMode = copy.mTintMode;
493            }
494        }
495
496        public boolean canReuseCache(int width, int height) {
497            if (mCachedThemeAttrs == mThemeAttrs
498                    && mCachedTint == mTint
499                    && mCachedTintMode == mTintMode
500                    && width == mCachedBitmap.getWidth()
501                    && height == mCachedBitmap.getHeight()
502                    && mCachedRootAlpha == mVPathRenderer.getRootAlpha())  {
503                return true;
504            }
505            return false;
506        }
507
508        public void updateCacheStates() {
509            // Use shallow copy here and shallow comparison in canReuseCache(),
510            // likely hit cache miss more, but practically not much difference.
511            mCachedThemeAttrs = mThemeAttrs;
512            mCachedTint = mTint;
513            mCachedTintMode = mTintMode;
514            mCachedRootAlpha = mVPathRenderer.getRootAlpha();
515        }
516
517        public VectorDrawableState() {
518            mVPathRenderer = new VPathRenderer();
519        }
520
521        @Override
522        public Drawable newDrawable() {
523            return new VectorDrawable(this, null, null);
524        }
525
526        @Override
527        public Drawable newDrawable(Resources res) {
528            return new VectorDrawable(this, res, null);
529        }
530
531        @Override
532        public Drawable newDrawable(Resources res, Theme theme) {
533            return new VectorDrawable(this, res, theme);
534        }
535
536        @Override
537        public int getChangingConfigurations() {
538            return mChangingConfigurations;
539        }
540    }
541
542    private static class VPathRenderer {
543        /* Right now the internal data structure is organized as a tree.
544         * Each node can be a group node, or a path.
545         * A group node can have groups or paths as children, but a path node has
546         * no children.
547         * One example can be:
548         *                 Root Group
549         *                /    |     \
550         *           Group    Path    Group
551         *          /     \             |
552         *         Path   Path         Path
553         *
554         */
555        // Variables that only used temporarily inside the draw() call, so there
556        // is no need for deep copying.
557        private final Path mPath = new Path();
558        private final Path mRenderPath = new Path();
559        private static final Matrix IDENTITY_MATRIX = new Matrix();
560        private final Matrix mFinalPathMatrix = new Matrix();
561
562        private Paint mStrokePaint;
563        private Paint mFillPaint;
564        private ColorFilter mColorFilter;
565        private PathMeasure mPathMeasure;
566
567        /////////////////////////////////////////////////////
568        // Variables below need to be copied (deep copy if applicable) for mutation.
569        private int mChangingConfigurations;
570        private final VGroup mRootGroup;
571        private float mBaseWidth = 0;
572        private float mBaseHeight = 0;
573        private float mViewportWidth = 0;
574        private float mViewportHeight = 0;
575        private int mRootAlpha = 0xFF;
576
577        final ArrayMap<String, Object> mVGTargetsMap = new ArrayMap<String, Object>();
578
579        public VPathRenderer() {
580            mRootGroup = new VGroup();
581        }
582
583        public void setRootAlpha(int alpha) {
584            mRootAlpha = alpha;
585        }
586
587        public int getRootAlpha() {
588            return mRootAlpha;
589        }
590
591        public VPathRenderer(VPathRenderer copy) {
592            mRootGroup = new VGroup(copy.mRootGroup, mVGTargetsMap);
593            mBaseWidth = copy.mBaseWidth;
594            mBaseHeight = copy.mBaseHeight;
595            mViewportWidth = copy.mViewportHeight;
596            mViewportHeight = copy.mViewportHeight;
597            mChangingConfigurations = copy.mChangingConfigurations;
598            mRootAlpha = copy.mRootAlpha;
599        }
600
601        public boolean canApplyTheme() {
602            // If one of the paths can apply theme, then return true;
603            return recursiveCanApplyTheme(mRootGroup);
604        }
605
606        private boolean recursiveCanApplyTheme(VGroup currentGroup) {
607            // We can do a tree traverse here, if there is one path return true,
608            // then we return true for the whole tree.
609            final ArrayList<Object> children = currentGroup.mChildren;
610
611            for (int i = 0; i < children.size(); i++) {
612                Object child = children.get(i);
613                if (child instanceof VGroup) {
614                    VGroup childGroup = (VGroup) child;
615                    if (childGroup.canApplyTheme()
616                            || recursiveCanApplyTheme(childGroup)) {
617                        return true;
618                    }
619                } else if (child instanceof VPath) {
620                    VPath childPath = (VPath) child;
621                    if (childPath.canApplyTheme()) {
622                        return true;
623                    }
624                }
625            }
626            return false;
627        }
628
629        public void applyTheme(Theme t) {
630            // Apply theme to every path of the tree.
631            recursiveApplyTheme(mRootGroup, t);
632        }
633
634        private void recursiveApplyTheme(VGroup currentGroup, Theme t) {
635            // We can do a tree traverse here, apply theme to all paths which
636            // can apply theme.
637            final ArrayList<Object> children = currentGroup.mChildren;
638            for (int i = 0; i < children.size(); i++) {
639                Object child = children.get(i);
640                if (child instanceof VGroup) {
641                    VGroup childGroup = (VGroup) child;
642                    if (childGroup.canApplyTheme()) {
643                        childGroup.applyTheme(t);
644                    }
645                    recursiveApplyTheme(childGroup, t);
646                } else if (child instanceof VPath) {
647                    VPath childPath = (VPath) child;
648                    if (childPath.canApplyTheme()) {
649                        childPath.applyTheme(t);
650                    }
651                }
652            }
653        }
654
655        public void setColorFilter(ColorFilter colorFilter) {
656            mColorFilter = colorFilter;
657
658            if (mFillPaint != null) {
659                mFillPaint.setColorFilter(colorFilter);
660            }
661
662            if (mStrokePaint != null) {
663                mStrokePaint.setColorFilter(colorFilter);
664            }
665
666        }
667
668        private void drawGroupTree(VGroup currentGroup, Matrix currentMatrix,
669                float currentAlpha, Canvas canvas, int w, int h) {
670            // Calculate current group's matrix by preConcat the parent's and
671            // and the current one on the top of the stack.
672            // Basically the Mfinal = Mviewport * M0 * M1 * M2;
673            // Mi the local matrix at level i of the group tree.
674            currentGroup.mStackedMatrix.set(currentMatrix);
675
676            currentGroup.mStackedMatrix.preConcat(currentGroup.mLocalMatrix);
677
678            float stackedAlpha = currentAlpha * currentGroup.mGroupAlpha;
679
680            // Draw the group tree in the same order as the XML file.
681            for (int i = 0; i < currentGroup.mChildren.size(); i++) {
682                Object child = currentGroup.mChildren.get(i);
683                if (child instanceof VGroup) {
684                    VGroup childGroup = (VGroup) child;
685                    drawGroupTree(childGroup, currentGroup.mStackedMatrix,
686                            stackedAlpha, canvas, w, h);
687                } else if (child instanceof VPath) {
688                    VPath childPath = (VPath) child;
689                    drawPath(currentGroup, childPath, stackedAlpha, canvas, w, h);
690                }
691            }
692        }
693
694        public void draw(Canvas canvas, int w, int h) {
695            // Travese the tree in pre-order to draw.
696            drawGroupTree(mRootGroup, IDENTITY_MATRIX, ((float) mRootAlpha) / 0xFF, canvas, w, h);
697        }
698
699        private void drawPath(VGroup vGroup, VPath vPath, float stackedAlpha,
700                Canvas canvas, int w, int h) {
701            final float scale = Math.min(h / mViewportHeight, w / mViewportWidth);
702
703            mFinalPathMatrix.set(vGroup.mStackedMatrix);
704            mFinalPathMatrix.postScale(scale, scale, mViewportWidth / 2f, mViewportHeight / 2f);
705            mFinalPathMatrix.postTranslate(
706                    w / 2f - mViewportWidth / 2f, h / 2f - mViewportHeight / 2f);
707
708            vPath.toPath(mPath);
709            final Path path = mPath;
710
711            if (vPath.mTrimPathStart != 0.0f || vPath.mTrimPathEnd != 1.0f) {
712                float start = (vPath.mTrimPathStart + vPath.mTrimPathOffset) % 1.0f;
713                float end = (vPath.mTrimPathEnd + vPath.mTrimPathOffset) % 1.0f;
714
715                if (mPathMeasure == null) {
716                    mPathMeasure = new PathMeasure();
717                }
718                mPathMeasure.setPath(mPath, false);
719
720                float len = mPathMeasure.getLength();
721                start = start * len;
722                end = end * len;
723                path.reset();
724                if (start > end) {
725                    mPathMeasure.getSegment(start, len, path, true);
726                    mPathMeasure.getSegment(0f, end, path, true);
727                } else {
728                    mPathMeasure.getSegment(start, end, path, true);
729                }
730                path.rLineTo(0, 0); // fix bug in measure
731            }
732
733            mRenderPath.reset();
734
735            mRenderPath.addPath(path, mFinalPathMatrix);
736
737            if (vPath.mClip) {
738                canvas.clipPath(mRenderPath, Region.Op.REPLACE);
739            } else {
740                if (vPath.mFillColor != 0) {
741                    if (mFillPaint == null) {
742                        mFillPaint = new Paint();
743                        mFillPaint.setColorFilter(mColorFilter);
744                        mFillPaint.setStyle(Paint.Style.FILL);
745                        mFillPaint.setAntiAlias(true);
746                    }
747                    mFillPaint.setColor(applyAlpha(vPath.mFillColor, stackedAlpha));
748                    canvas.drawPath(mRenderPath, mFillPaint);
749                }
750
751                if (vPath.mStrokeColor != 0) {
752                    if (mStrokePaint == null) {
753                        mStrokePaint = new Paint();
754                        mStrokePaint.setColorFilter(mColorFilter);
755                        mStrokePaint.setStyle(Paint.Style.STROKE);
756                        mStrokePaint.setAntiAlias(true);
757                    }
758
759                    final Paint strokePaint = mStrokePaint;
760                    if (vPath.mStrokeLineJoin != null) {
761                        strokePaint.setStrokeJoin(vPath.mStrokeLineJoin);
762                    }
763
764                    if (vPath.mStrokeLineCap != null) {
765                        strokePaint.setStrokeCap(vPath.mStrokeLineCap);
766                    }
767
768                    strokePaint.setStrokeMiter(vPath.mStrokeMiterlimit * scale);
769
770                    strokePaint.setColor(applyAlpha(vPath.mStrokeColor, stackedAlpha));
771                    strokePaint.setStrokeWidth(vPath.mStrokeWidth * scale);
772                    canvas.drawPath(mRenderPath, strokePaint);
773                }
774            }
775        }
776
777        private void parseViewport(Resources r, AttributeSet attrs)
778                throws XmlPullParserException {
779            final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableViewport);
780
781            // Account for any configuration changes.
782            mChangingConfigurations |= a.getChangingConfigurations();
783
784            mViewportWidth = a.getFloat(
785                    R.styleable.VectorDrawableViewport_viewportWidth, mViewportWidth);
786            mViewportHeight = a.getFloat(
787                    R.styleable.VectorDrawableViewport_viewportHeight, mViewportHeight);
788
789            if (mViewportWidth <= 0) {
790                throw new XmlPullParserException(a.getPositionDescription() +
791                        "<viewport> tag requires viewportWidth > 0");
792            } else if (mViewportHeight <= 0) {
793                throw new XmlPullParserException(a.getPositionDescription() +
794                        "<viewport> tag requires viewportHeight > 0");
795            }
796
797            a.recycle();
798        }
799
800        private void parseSize(Resources r, AttributeSet attrs)
801                throws XmlPullParserException {
802            final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableSize);
803
804            // Account for any configuration changes.
805            mChangingConfigurations |= a.getChangingConfigurations();
806
807            mBaseWidth = a.getDimension(R.styleable.VectorDrawableSize_width, mBaseWidth);
808            mBaseHeight = a.getDimension(R.styleable.VectorDrawableSize_height, mBaseHeight);
809
810            if (mBaseWidth <= 0) {
811                throw new XmlPullParserException(a.getPositionDescription() +
812                        "<size> tag requires width > 0");
813            } else if (mBaseHeight <= 0) {
814                throw new XmlPullParserException(a.getPositionDescription() +
815                        "<size> tag requires height > 0");
816            }
817
818            a.recycle();
819        }
820
821    }
822
823    static class VGroup {
824        // mStackedMatrix is only used temporarily when drawing, it combines all
825        // the parents' local matrices with the current one.
826        private final Matrix mStackedMatrix = new Matrix();
827
828        /////////////////////////////////////////////////////
829        // Variables below need to be copied (deep copy if applicable) for mutation.
830        final ArrayList<Object> mChildren = new ArrayList<Object>();
831
832        private float mRotate = 0;
833        private float mPivotX = 0;
834        private float mPivotY = 0;
835        private float mScaleX = 1;
836        private float mScaleY = 1;
837        private float mTranslateX = 0;
838        private float mTranslateY = 0;
839        private float mGroupAlpha = 1;
840
841        // mLocalMatrix is updated based on the update of transformation information,
842        // either parsed from the XML or by animation.
843        private final Matrix mLocalMatrix = new Matrix();
844        private int mChangingConfigurations;
845        private int[] mThemeAttrs;
846        private String mGroupName = null;
847
848        public VGroup(VGroup copy, ArrayMap<String, Object> targetsMap) {
849            mRotate = copy.mRotate;
850            mPivotX = copy.mPivotX;
851            mPivotY = copy.mPivotY;
852            mScaleX = copy.mScaleX;
853            mScaleY = copy.mScaleY;
854            mTranslateX = copy.mTranslateX;
855            mTranslateY = copy.mTranslateY;
856            mGroupAlpha = copy.mGroupAlpha;
857            mThemeAttrs = copy.mThemeAttrs;
858            mGroupName = copy.mGroupName;
859            mChangingConfigurations = copy.mChangingConfigurations;
860            if (mGroupName != null) {
861                targetsMap.put(mGroupName, this);
862            }
863
864            mLocalMatrix.set(copy.mLocalMatrix);
865
866            final ArrayList<Object> children = copy.mChildren;
867            for (int i = 0; i < children.size(); i++) {
868                Object copyChild = children.get(i);
869                if (copyChild instanceof VGroup) {
870                    VGroup copyGroup = (VGroup) copyChild;
871                    mChildren.add(new VGroup(copyGroup, targetsMap));
872                } else if (copyChild instanceof VPath) {
873                    VPath copyPath = (VPath) copyChild;
874                    VPath newPath = new VPath(copyPath);
875                    mChildren.add(newPath);
876                    if (newPath.mPathName != null) {
877                        targetsMap.put(newPath.mPathName, newPath);
878                    }
879                }
880            }
881        }
882
883        public VGroup() {
884        }
885
886        /* Getter and Setter */
887        public float getRotation() {
888            return mRotate;
889        }
890
891        public void setRotation(float rotation) {
892            if (rotation != mRotate) {
893                mRotate = rotation;
894                updateLocalMatrix();
895            }
896        }
897
898        public float getPivotX() {
899            return mPivotX;
900        }
901
902        public void setPivotX(float pivotX) {
903            if (pivotX != mPivotX) {
904                mPivotX = pivotX;
905                updateLocalMatrix();
906            }
907        }
908
909        public float getPivotY() {
910            return mPivotY;
911        }
912
913        public void setPivotY(float pivotY) {
914            if (pivotY != mPivotY) {
915                mPivotY = pivotY;
916                updateLocalMatrix();
917            }
918        }
919
920        public float getScaleX() {
921            return mScaleX;
922        }
923
924        public void setScaleX(float scaleX) {
925            if (scaleX != mScaleX) {
926                mScaleX = scaleX;
927                updateLocalMatrix();
928            }
929        }
930
931        public float getScaleY() {
932            return mScaleY;
933        }
934
935        public void setScaleY(float scaleY) {
936            if (scaleY != mScaleY) {
937                mScaleY = scaleY;
938                updateLocalMatrix();
939            }
940        }
941
942        public float getTranslateX() {
943            return mTranslateX;
944        }
945
946        public void setTranslateX(float translateX) {
947            if (translateX != mTranslateX) {
948                mTranslateX = translateX;
949                updateLocalMatrix();
950            }
951        }
952
953        public float getTranslateY() {
954            return mTranslateY;
955        }
956
957        public void setTranslateY(float translateY) {
958            if (translateY != mTranslateY) {
959                mTranslateY = translateY;
960                updateLocalMatrix();
961            }
962        }
963
964        public float getAlpha() {
965            return mGroupAlpha;
966        }
967
968        public void setAlpha(float groupAlpha) {
969            if (groupAlpha != mGroupAlpha) {
970                mGroupAlpha = groupAlpha;
971            }
972        }
973
974        public String getGroupName() {
975            return mGroupName;
976        }
977
978        public Matrix getLocalMatrix() {
979            return mLocalMatrix;
980        }
981
982        public boolean canApplyTheme() {
983            return mThemeAttrs != null;
984        }
985
986        public void inflate(Resources res, AttributeSet attrs, Theme theme) {
987            final TypedArray a = obtainAttributes(res, theme, attrs,
988                    R.styleable.VectorDrawableGroup);
989            updateStateFromTypedArray(a);
990            a.recycle();
991        }
992
993        private void updateStateFromTypedArray(TypedArray a) {
994            // Account for any configuration changes.
995            mChangingConfigurations |= a.getChangingConfigurations();
996
997            // Extract the theme attributes, if any.
998            mThemeAttrs = a.extractThemeAttrs();
999
1000            mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate);
1001            mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX);
1002            mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY);
1003            mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX);
1004            mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY);
1005            mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX);
1006            mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY);
1007            mGroupAlpha = a.getFloat(R.styleable.VectorDrawableGroup_alpha, mGroupAlpha);
1008
1009            final String groupName = a.getString(R.styleable.VectorDrawableGroup_name);
1010            if (groupName != null) {
1011                mGroupName = groupName;
1012            }
1013
1014            updateLocalMatrix();
1015        }
1016
1017        public void applyTheme(Theme t) {
1018            if (mThemeAttrs == null) {
1019                return;
1020            }
1021
1022            final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawablePath);
1023            updateStateFromTypedArray(a);
1024            a.recycle();
1025        }
1026
1027        private void updateLocalMatrix() {
1028            // The order we apply is the same as the
1029            // RenderNode.cpp::applyViewPropertyTransforms().
1030            mLocalMatrix.reset();
1031            mLocalMatrix.postTranslate(-mPivotX, -mPivotY);
1032            mLocalMatrix.postScale(mScaleX, mScaleY);
1033            mLocalMatrix.postRotate(mRotate, 0, 0);
1034            mLocalMatrix.postTranslate(mTranslateX + mPivotX, mTranslateY + mPivotY);
1035        }
1036    }
1037
1038    private static class VPath {
1039        /////////////////////////////////////////////////////
1040        // Variables below need to be copied (deep copy if applicable) for mutation.
1041        private int[] mThemeAttrs;
1042
1043        int mStrokeColor = 0;
1044        float mStrokeWidth = 0;
1045        float mStrokeOpacity = Float.NaN;
1046        int mFillColor = Color.BLACK;
1047        int mFillRule;
1048        float mFillOpacity = Float.NaN;
1049        float mTrimPathStart = 0;
1050        float mTrimPathEnd = 1;
1051        float mTrimPathOffset = 0;
1052
1053        boolean mClip = false;
1054        Paint.Cap mStrokeLineCap = Paint.Cap.BUTT;
1055        Paint.Join mStrokeLineJoin = Paint.Join.MITER;
1056        float mStrokeMiterlimit = 4;
1057
1058        private PathParser.PathDataNode[] mNodes = null;
1059        String mPathName;
1060        private int mChangingConfigurations;
1061
1062        public VPath() {
1063            // Empty constructor.
1064        }
1065
1066        public VPath(VPath copy) {
1067            mThemeAttrs = copy.mThemeAttrs;
1068
1069            mStrokeColor = copy.mStrokeColor;
1070            mStrokeWidth = copy.mStrokeWidth;
1071            mStrokeOpacity = copy.mStrokeOpacity;
1072            mFillColor = copy.mFillColor;
1073            mFillRule = copy.mFillRule;
1074            mFillOpacity = copy.mFillOpacity;
1075            mTrimPathStart = copy.mTrimPathStart;
1076            mTrimPathEnd = copy.mTrimPathEnd;
1077            mTrimPathOffset = copy.mTrimPathOffset;
1078
1079            mClip = copy.mClip;
1080            mStrokeLineCap = copy.mStrokeLineCap;
1081            mStrokeLineJoin = copy.mStrokeLineJoin;
1082            mStrokeMiterlimit = copy.mStrokeMiterlimit;
1083
1084            mNodes = PathParser.deepCopyNodes(copy.mNodes);
1085            mPathName = copy.mPathName;
1086            mChangingConfigurations = copy.mChangingConfigurations;
1087        }
1088
1089        public void toPath(Path path) {
1090            path.reset();
1091            if (mNodes != null) {
1092                PathParser.PathDataNode.nodesToPath(mNodes, path);
1093            }
1094        }
1095
1096        public String getPathName() {
1097            return mPathName;
1098        }
1099
1100        private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) {
1101            switch (id) {
1102                case LINECAP_BUTT:
1103                    return Paint.Cap.BUTT;
1104                case LINECAP_ROUND:
1105                    return Paint.Cap.ROUND;
1106                case LINECAP_SQUARE:
1107                    return Paint.Cap.SQUARE;
1108                default:
1109                    return defValue;
1110            }
1111        }
1112
1113        private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) {
1114            switch (id) {
1115                case LINEJOIN_MITER:
1116                    return Paint.Join.MITER;
1117                case LINEJOIN_ROUND:
1118                    return Paint.Join.ROUND;
1119                case LINEJOIN_BEVEL:
1120                    return Paint.Join.BEVEL;
1121                default:
1122                    return defValue;
1123            }
1124        }
1125
1126        /* Setters and Getters, mostly used by animator from AnimatedVectorDrawable. */
1127        @SuppressWarnings("unused")
1128        public PathParser.PathDataNode[] getPathData() {
1129            return mNodes;
1130        }
1131
1132        @SuppressWarnings("unused")
1133        public void setPathData(PathParser.PathDataNode[] nodes) {
1134            if (!PathParser.canMorph(mNodes, nodes)) {
1135                // This should not happen in the middle of animation.
1136                mNodes = PathParser.deepCopyNodes(nodes);
1137            } else {
1138                PathParser.updateNodes(mNodes, nodes);
1139            }
1140        }
1141
1142        @SuppressWarnings("unused")
1143        int getStroke() {
1144            return mStrokeColor;
1145        }
1146
1147        @SuppressWarnings("unused")
1148        void setStroke(int strokeColor) {
1149            mStrokeColor = strokeColor;
1150        }
1151
1152        @SuppressWarnings("unused")
1153        float getStrokeWidth() {
1154            return mStrokeWidth;
1155        }
1156
1157        @SuppressWarnings("unused")
1158        void setStrokeWidth(float strokeWidth) {
1159            mStrokeWidth = strokeWidth;
1160        }
1161
1162        @SuppressWarnings("unused")
1163        float getStrokeOpacity() {
1164            return mStrokeOpacity;
1165        }
1166
1167        @SuppressWarnings("unused")
1168        void setStrokeOpacity(float strokeOpacity) {
1169            mStrokeOpacity = strokeOpacity;
1170        }
1171
1172        @SuppressWarnings("unused")
1173        int getFill() {
1174            return mFillColor;
1175        }
1176
1177        @SuppressWarnings("unused")
1178        void setFill(int fillColor) {
1179            mFillColor = fillColor;
1180        }
1181
1182        @SuppressWarnings("unused")
1183        float getFillOpacity() {
1184            return mFillOpacity;
1185        }
1186
1187        @SuppressWarnings("unused")
1188        void setFillOpacity(float fillOpacity) {
1189            mFillOpacity = fillOpacity;
1190        }
1191
1192        @SuppressWarnings("unused")
1193        float getTrimPathStart() {
1194            return mTrimPathStart;
1195        }
1196
1197        @SuppressWarnings("unused")
1198        void setTrimPathStart(float trimPathStart) {
1199            mTrimPathStart = trimPathStart;
1200        }
1201
1202        @SuppressWarnings("unused")
1203        float getTrimPathEnd() {
1204            return mTrimPathEnd;
1205        }
1206
1207        @SuppressWarnings("unused")
1208        void setTrimPathEnd(float trimPathEnd) {
1209            mTrimPathEnd = trimPathEnd;
1210        }
1211
1212        @SuppressWarnings("unused")
1213        float getTrimPathOffset() {
1214            return mTrimPathOffset;
1215        }
1216
1217        @SuppressWarnings("unused")
1218        void setTrimPathOffset(float trimPathOffset) {
1219            mTrimPathOffset = trimPathOffset;
1220        }
1221
1222        public boolean canApplyTheme() {
1223            return mThemeAttrs != null;
1224        }
1225
1226        public void inflate(Resources r, AttributeSet attrs, Theme theme) {
1227            final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawablePath);
1228            updateStateFromTypedArray(a);
1229            a.recycle();
1230        }
1231
1232        private void updateStateFromTypedArray(TypedArray a) {
1233            // Account for any configuration changes.
1234            mChangingConfigurations |= a.getChangingConfigurations();
1235
1236            // Extract the theme attributes, if any.
1237            mThemeAttrs = a.extractThemeAttrs();
1238
1239            mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip);
1240
1241            mPathName = a.getString(R.styleable.VectorDrawablePath_name);
1242            mNodes = PathParser.createNodesFromPathData(a.getString(
1243                    R.styleable.VectorDrawablePath_pathData));
1244
1245            mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor);
1246            mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity);
1247            mStrokeLineCap = getStrokeLineCap(a.getInt(
1248                    R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap);
1249            mStrokeLineJoin = getStrokeLineJoin(a.getInt(
1250                    R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin);
1251            mStrokeMiterlimit = a.getFloat(
1252                    R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit);
1253            mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor);
1254            mStrokeOpacity = a.getFloat(
1255                    R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity);
1256            mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth);
1257            mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd);
1258            mTrimPathOffset = a.getFloat(
1259                    R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset);
1260            mTrimPathStart = a.getFloat(
1261                    R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart);
1262
1263            updateColorAlphas();
1264        }
1265
1266        public void applyTheme(Theme t) {
1267            if (mThemeAttrs == null) {
1268                return;
1269            }
1270
1271            final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawablePath);
1272            updateStateFromTypedArray(a);
1273            a.recycle();
1274        }
1275
1276        private void updateColorAlphas() {
1277            if (!Float.isNaN(mFillOpacity)) {
1278                mFillColor = applyAlpha(mFillColor, mFillOpacity);
1279            }
1280
1281            if (!Float.isNaN(mStrokeOpacity)) {
1282                mStrokeColor = applyAlpha(mStrokeColor, mStrokeOpacity);
1283            }
1284        }
1285    }
1286}
1287