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