VectorDrawable.java revision 738177caf6a755a59ca6b17bb968be0aa4e8e10f
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        inflateInternal(res, parser, attrs, theme);
340
341        mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
342        state.mVPathRenderer.setColorFilter(mTintFilter);
343    }
344
345    private void updateStateFromTypedArray(TypedArray a) {
346        final VectorDrawableState state = mVectorState;
347
348        // Account for any configuration changes.
349        state.mChangingConfigurations |= a.getChangingConfigurations();
350
351        // Extract the theme attributes, if any.
352        state.mThemeAttrs = a.extractThemeAttrs();
353
354        final int tintMode = a.getInt(R.styleable.VectorDrawable_tintMode, -1);
355        if (tintMode != -1) {
356            state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN);
357        }
358
359        final ColorStateList tint = a.getColorStateList(R.styleable.VectorDrawable_tint);
360        if (tint != null) {
361            state.mTint = tint;
362        }
363
364        state.mAutoMirrored = a.getBoolean(
365                R.styleable.VectorDrawable_autoMirrored, state.mAutoMirrored);
366    }
367
368    private void inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs,
369            Theme theme) throws XmlPullParserException, IOException {
370        final VectorDrawableState state = mVectorState;
371        final VPathRenderer pathRenderer = new VPathRenderer();
372        state.mVPathRenderer = pathRenderer;
373
374        boolean noSizeTag = true;
375        boolean noViewportTag = true;
376        boolean noPathTag = true;
377
378        // Use a stack to help to build the group tree.
379        // The top of the stack is always the current group.
380        final Stack<VGroup> groupStack = new Stack<VGroup>();
381        groupStack.push(pathRenderer.mRootGroup);
382
383        int eventType = parser.getEventType();
384        while (eventType != XmlPullParser.END_DOCUMENT) {
385            if (eventType == XmlPullParser.START_TAG) {
386                final String tagName = parser.getName();
387                final VGroup currentGroup = groupStack.peek();
388
389                if (SHAPE_PATH.equals(tagName)) {
390                    final VPath path = new VPath();
391                    path.inflate(res, attrs, theme);
392                    currentGroup.mChildren.add(path);
393                    if (path.getPathName() != null) {
394                        pathRenderer.mVGTargetsMap.put(path.getPathName(), path);
395                    }
396                    noPathTag = false;
397                    state.mChangingConfigurations |= path.mChangingConfigurations;
398                } else if (SHAPE_SIZE.equals(tagName)) {
399                    pathRenderer.parseSize(res, attrs);
400                    noSizeTag = false;
401                    state.mChangingConfigurations |= pathRenderer.mChangingConfigurations;
402                } else if (SHAPE_VIEWPORT.equals(tagName)) {
403                    pathRenderer.parseViewport(res, attrs);
404                    noViewportTag = false;
405                    state.mChangingConfigurations |= pathRenderer.mChangingConfigurations;
406                } else if (SHAPE_GROUP.equals(tagName)) {
407                    VGroup newChildGroup = new VGroup();
408                    newChildGroup.inflate(res, attrs, theme);
409                    currentGroup.mChildren.add(newChildGroup);
410                    groupStack.push(newChildGroup);
411                    if (newChildGroup.getGroupName() != null) {
412                        pathRenderer.mVGTargetsMap.put(newChildGroup.getGroupName(),
413                                newChildGroup);
414                    }
415                    state.mChangingConfigurations |= newChildGroup.mChangingConfigurations;
416                }
417            } else if (eventType == XmlPullParser.END_TAG) {
418                final String tagName = parser.getName();
419                if (SHAPE_GROUP.equals(tagName)) {
420                    groupStack.pop();
421                }
422            }
423            eventType = parser.next();
424        }
425
426        // Print the tree out for debug.
427        if (DBG_VECTOR_DRAWABLE) {
428            printGroupTree(pathRenderer.mRootGroup, 0);
429        }
430
431        if (noSizeTag || noViewportTag || noPathTag) {
432            final StringBuffer tag = new StringBuffer();
433
434            if (noSizeTag) {
435                tag.append(SHAPE_SIZE);
436            }
437
438            if (noViewportTag) {
439                if (tag.length() > 0) {
440                    tag.append(" & ");
441                }
442                tag.append(SHAPE_SIZE);
443            }
444
445            if (noPathTag) {
446                if (tag.length() > 0) {
447                    tag.append(" or ");
448                }
449                tag.append(SHAPE_PATH);
450            }
451
452            throw new XmlPullParserException("no " + tag + " defined");
453        }
454    }
455
456    private void printGroupTree(VGroup currentGroup, int level) {
457        String indent = "";
458        for (int i = 0; i < level; i++) {
459            indent += "    ";
460        }
461        // Print the current node
462        Log.v(LOGTAG, indent + "current group is :" + currentGroup.getGroupName()
463                + " rotation is " + currentGroup.mRotate);
464        Log.v(LOGTAG, indent + "matrix is :" + currentGroup.getLocalMatrix().toString());
465        // Then print all the children groups
466        for (int i = 0; i < currentGroup.mChildren.size(); i++) {
467            Object child = currentGroup.mChildren.get(i);
468            if (child instanceof VGroup) {
469                printGroupTree((VGroup) child, level + 1);
470            }
471        }
472    }
473
474    @Override
475    public int getChangingConfigurations() {
476        return super.getChangingConfigurations() | mVectorState.mChangingConfigurations;
477    }
478
479    void setAllowCaching(boolean allowCaching) {
480        mAllowCaching = allowCaching;
481    }
482
483    private boolean needMirroring() {
484        return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
485    }
486
487    @Override
488    public void setAutoMirrored(boolean mirrored) {
489        if (mVectorState.mAutoMirrored != mirrored) {
490            mVectorState.mAutoMirrored = mirrored;
491            invalidateSelf();
492        }
493    }
494
495    @Override
496    public boolean isAutoMirrored() {
497        return mVectorState.mAutoMirrored;
498    }
499
500    private static class VectorDrawableState extends ConstantState {
501        int[] mThemeAttrs;
502        int mChangingConfigurations;
503        VPathRenderer mVPathRenderer;
504        ColorStateList mTint;
505        Mode mTintMode;
506        boolean mAutoMirrored;
507
508        Bitmap mCachedBitmap;
509        int[] mCachedThemeAttrs;
510        ColorStateList mCachedTint;
511        Mode mCachedTintMode;
512        int mCachedRootAlpha;
513
514        // Deep copy for mutate() or implicitly mutate.
515        public VectorDrawableState(VectorDrawableState copy) {
516            if (copy != null) {
517                mThemeAttrs = copy.mThemeAttrs;
518                mChangingConfigurations = copy.mChangingConfigurations;
519                mVPathRenderer = new VPathRenderer(copy.mVPathRenderer);
520                mTint = copy.mTint;
521                mTintMode = copy.mTintMode;
522                mAutoMirrored = copy.mAutoMirrored;
523            }
524        }
525
526        public boolean canReuseCache(int width, int height) {
527            if (mCachedThemeAttrs == mThemeAttrs
528                    && mCachedTint == mTint
529                    && mCachedTintMode == mTintMode
530                    && mAutoMirrored == mAutoMirrored
531                    && width == mCachedBitmap.getWidth()
532                    && height == mCachedBitmap.getHeight()
533                    && mCachedRootAlpha == mVPathRenderer.getRootAlpha())  {
534                return true;
535            }
536            return false;
537        }
538
539        public void updateCacheStates() {
540            // Use shallow copy here and shallow comparison in canReuseCache(),
541            // likely hit cache miss more, but practically not much difference.
542            mCachedThemeAttrs = mThemeAttrs;
543            mCachedTint = mTint;
544            mCachedTintMode = mTintMode;
545            mCachedRootAlpha = mVPathRenderer.getRootAlpha();
546        }
547
548        public VectorDrawableState() {
549            mVPathRenderer = new VPathRenderer();
550        }
551
552        @Override
553        public Drawable newDrawable() {
554            return new VectorDrawable(this, null, null);
555        }
556
557        @Override
558        public Drawable newDrawable(Resources res) {
559            return new VectorDrawable(this, res, null);
560        }
561
562        @Override
563        public Drawable newDrawable(Resources res, Theme theme) {
564            return new VectorDrawable(this, res, theme);
565        }
566
567        @Override
568        public int getChangingConfigurations() {
569            return mChangingConfigurations;
570        }
571    }
572
573    private static class VPathRenderer {
574        /* Right now the internal data structure is organized as a tree.
575         * Each node can be a group node, or a path.
576         * A group node can have groups or paths as children, but a path node has
577         * no children.
578         * One example can be:
579         *                 Root Group
580         *                /    |     \
581         *           Group    Path    Group
582         *          /     \             |
583         *         Path   Path         Path
584         *
585         */
586        // Variables that only used temporarily inside the draw() call, so there
587        // is no need for deep copying.
588        private final Path mPath = new Path();
589        private final Path mRenderPath = new Path();
590        private static final Matrix IDENTITY_MATRIX = new Matrix();
591        private final Matrix mFinalPathMatrix = new Matrix();
592
593        private Paint mStrokePaint;
594        private Paint mFillPaint;
595        private ColorFilter mColorFilter;
596        private PathMeasure mPathMeasure;
597
598        /////////////////////////////////////////////////////
599        // Variables below need to be copied (deep copy if applicable) for mutation.
600        private int mChangingConfigurations;
601        private final VGroup mRootGroup;
602        private float mBaseWidth = 0;
603        private float mBaseHeight = 0;
604        private float mViewportWidth = 0;
605        private float mViewportHeight = 0;
606        private int mRootAlpha = 0xFF;
607
608        final ArrayMap<String, Object> mVGTargetsMap = new ArrayMap<String, Object>();
609
610        public VPathRenderer() {
611            mRootGroup = new VGroup();
612        }
613
614        public void setRootAlpha(int alpha) {
615            mRootAlpha = alpha;
616        }
617
618        public int getRootAlpha() {
619            return mRootAlpha;
620        }
621
622        public VPathRenderer(VPathRenderer copy) {
623            mRootGroup = new VGroup(copy.mRootGroup, mVGTargetsMap);
624            mBaseWidth = copy.mBaseWidth;
625            mBaseHeight = copy.mBaseHeight;
626            mViewportWidth = copy.mViewportHeight;
627            mViewportHeight = copy.mViewportHeight;
628            mChangingConfigurations = copy.mChangingConfigurations;
629            mRootAlpha = copy.mRootAlpha;
630        }
631
632        public boolean canApplyTheme() {
633            // If one of the paths can apply theme, then return true;
634            return recursiveCanApplyTheme(mRootGroup);
635        }
636
637        private boolean recursiveCanApplyTheme(VGroup currentGroup) {
638            // We can do a tree traverse here, if there is one path return true,
639            // then we return true for the whole tree.
640            final ArrayList<Object> children = currentGroup.mChildren;
641
642            for (int i = 0; i < children.size(); i++) {
643                Object child = children.get(i);
644                if (child instanceof VGroup) {
645                    VGroup childGroup = (VGroup) child;
646                    if (childGroup.canApplyTheme()
647                            || recursiveCanApplyTheme(childGroup)) {
648                        return true;
649                    }
650                } else if (child instanceof VPath) {
651                    VPath childPath = (VPath) child;
652                    if (childPath.canApplyTheme()) {
653                        return true;
654                    }
655                }
656            }
657            return false;
658        }
659
660        public void applyTheme(Theme t) {
661            // Apply theme to every path of the tree.
662            recursiveApplyTheme(mRootGroup, t);
663        }
664
665        private void recursiveApplyTheme(VGroup currentGroup, Theme t) {
666            // We can do a tree traverse here, apply theme to all paths which
667            // can apply theme.
668            final ArrayList<Object> children = currentGroup.mChildren;
669            for (int i = 0; i < children.size(); i++) {
670                Object child = children.get(i);
671                if (child instanceof VGroup) {
672                    VGroup childGroup = (VGroup) child;
673                    if (childGroup.canApplyTheme()) {
674                        childGroup.applyTheme(t);
675                    }
676                    recursiveApplyTheme(childGroup, t);
677                } else if (child instanceof VPath) {
678                    VPath childPath = (VPath) child;
679                    if (childPath.canApplyTheme()) {
680                        childPath.applyTheme(t);
681                    }
682                }
683            }
684        }
685
686        public void setColorFilter(ColorFilter colorFilter) {
687            mColorFilter = colorFilter;
688
689            if (mFillPaint != null) {
690                mFillPaint.setColorFilter(colorFilter);
691            }
692
693            if (mStrokePaint != null) {
694                mStrokePaint.setColorFilter(colorFilter);
695            }
696
697        }
698
699        private void drawGroupTree(VGroup currentGroup, Matrix currentMatrix,
700                float currentAlpha, Canvas canvas, int w, int h) {
701            // Calculate current group's matrix by preConcat the parent's and
702            // and the current one on the top of the stack.
703            // Basically the Mfinal = Mviewport * M0 * M1 * M2;
704            // Mi the local matrix at level i of the group tree.
705            currentGroup.mStackedMatrix.set(currentMatrix);
706
707            currentGroup.mStackedMatrix.preConcat(currentGroup.mLocalMatrix);
708
709            float stackedAlpha = currentAlpha * currentGroup.mGroupAlpha;
710
711            // Draw the group tree in the same order as the XML file.
712            for (int i = 0; i < currentGroup.mChildren.size(); i++) {
713                Object child = currentGroup.mChildren.get(i);
714                if (child instanceof VGroup) {
715                    VGroup childGroup = (VGroup) child;
716                    drawGroupTree(childGroup, currentGroup.mStackedMatrix,
717                            stackedAlpha, canvas, w, h);
718                } else if (child instanceof VPath) {
719                    VPath childPath = (VPath) child;
720                    drawPath(currentGroup, childPath, stackedAlpha, canvas, w, h);
721                }
722            }
723        }
724
725        public void draw(Canvas canvas, int w, int h) {
726            // Travese the tree in pre-order to draw.
727            drawGroupTree(mRootGroup, IDENTITY_MATRIX, ((float) mRootAlpha) / 0xFF, canvas, w, h);
728        }
729
730        private void drawPath(VGroup vGroup, VPath vPath, float stackedAlpha,
731                Canvas canvas, int w, int h) {
732            final float scale = Math.min(h / mViewportHeight, w / mViewportWidth);
733
734            mFinalPathMatrix.set(vGroup.mStackedMatrix);
735            mFinalPathMatrix.postScale(scale, scale, mViewportWidth / 2f, mViewportHeight / 2f);
736            mFinalPathMatrix.postTranslate(
737                    w / 2f - mViewportWidth / 2f, h / 2f - mViewportHeight / 2f);
738
739            vPath.toPath(mPath);
740            final Path path = mPath;
741
742            if (vPath.mTrimPathStart != 0.0f || vPath.mTrimPathEnd != 1.0f) {
743                float start = (vPath.mTrimPathStart + vPath.mTrimPathOffset) % 1.0f;
744                float end = (vPath.mTrimPathEnd + vPath.mTrimPathOffset) % 1.0f;
745
746                if (mPathMeasure == null) {
747                    mPathMeasure = new PathMeasure();
748                }
749                mPathMeasure.setPath(mPath, false);
750
751                float len = mPathMeasure.getLength();
752                start = start * len;
753                end = end * len;
754                path.reset();
755                if (start > end) {
756                    mPathMeasure.getSegment(start, len, path, true);
757                    mPathMeasure.getSegment(0f, end, path, true);
758                } else {
759                    mPathMeasure.getSegment(start, end, path, true);
760                }
761                path.rLineTo(0, 0); // fix bug in measure
762            }
763
764            mRenderPath.reset();
765
766            mRenderPath.addPath(path, mFinalPathMatrix);
767
768            if (vPath.mClip) {
769                canvas.clipPath(mRenderPath, Region.Op.REPLACE);
770            } else {
771                if (vPath.mFillColor != 0) {
772                    if (mFillPaint == null) {
773                        mFillPaint = new Paint();
774                        mFillPaint.setColorFilter(mColorFilter);
775                        mFillPaint.setStyle(Paint.Style.FILL);
776                        mFillPaint.setAntiAlias(true);
777                    }
778                    mFillPaint.setColor(applyAlpha(vPath.mFillColor, stackedAlpha));
779                    canvas.drawPath(mRenderPath, mFillPaint);
780                }
781
782                if (vPath.mStrokeColor != 0) {
783                    if (mStrokePaint == null) {
784                        mStrokePaint = new Paint();
785                        mStrokePaint.setColorFilter(mColorFilter);
786                        mStrokePaint.setStyle(Paint.Style.STROKE);
787                        mStrokePaint.setAntiAlias(true);
788                    }
789
790                    final Paint strokePaint = mStrokePaint;
791                    if (vPath.mStrokeLineJoin != null) {
792                        strokePaint.setStrokeJoin(vPath.mStrokeLineJoin);
793                    }
794
795                    if (vPath.mStrokeLineCap != null) {
796                        strokePaint.setStrokeCap(vPath.mStrokeLineCap);
797                    }
798
799                    strokePaint.setStrokeMiter(vPath.mStrokeMiterlimit * scale);
800
801                    strokePaint.setColor(applyAlpha(vPath.mStrokeColor, stackedAlpha));
802                    strokePaint.setStrokeWidth(vPath.mStrokeWidth * scale);
803                    canvas.drawPath(mRenderPath, strokePaint);
804                }
805            }
806        }
807
808        private void parseViewport(Resources r, AttributeSet attrs)
809                throws XmlPullParserException {
810            final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableViewport);
811
812            // Account for any configuration changes.
813            mChangingConfigurations |= a.getChangingConfigurations();
814
815            mViewportWidth = a.getFloat(
816                    R.styleable.VectorDrawableViewport_viewportWidth, mViewportWidth);
817            mViewportHeight = a.getFloat(
818                    R.styleable.VectorDrawableViewport_viewportHeight, mViewportHeight);
819
820            if (mViewportWidth <= 0) {
821                throw new XmlPullParserException(a.getPositionDescription() +
822                        "<viewport> tag requires viewportWidth > 0");
823            } else if (mViewportHeight <= 0) {
824                throw new XmlPullParserException(a.getPositionDescription() +
825                        "<viewport> tag requires viewportHeight > 0");
826            }
827
828            a.recycle();
829        }
830
831        private void parseSize(Resources r, AttributeSet attrs)
832                throws XmlPullParserException {
833            final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableSize);
834
835            // Account for any configuration changes.
836            mChangingConfigurations |= a.getChangingConfigurations();
837
838            mBaseWidth = a.getDimension(R.styleable.VectorDrawableSize_width, mBaseWidth);
839            mBaseHeight = a.getDimension(R.styleable.VectorDrawableSize_height, mBaseHeight);
840
841            if (mBaseWidth <= 0) {
842                throw new XmlPullParserException(a.getPositionDescription() +
843                        "<size> tag requires width > 0");
844            } else if (mBaseHeight <= 0) {
845                throw new XmlPullParserException(a.getPositionDescription() +
846                        "<size> tag requires height > 0");
847            }
848
849            a.recycle();
850        }
851
852    }
853
854    static class VGroup {
855        // mStackedMatrix is only used temporarily when drawing, it combines all
856        // the parents' local matrices with the current one.
857        private final Matrix mStackedMatrix = new Matrix();
858
859        /////////////////////////////////////////////////////
860        // Variables below need to be copied (deep copy if applicable) for mutation.
861        final ArrayList<Object> mChildren = new ArrayList<Object>();
862
863        private float mRotate = 0;
864        private float mPivotX = 0;
865        private float mPivotY = 0;
866        private float mScaleX = 1;
867        private float mScaleY = 1;
868        private float mTranslateX = 0;
869        private float mTranslateY = 0;
870        private float mGroupAlpha = 1;
871
872        // mLocalMatrix is updated based on the update of transformation information,
873        // either parsed from the XML or by animation.
874        private final Matrix mLocalMatrix = new Matrix();
875        private int mChangingConfigurations;
876        private int[] mThemeAttrs;
877        private String mGroupName = null;
878
879        public VGroup(VGroup copy, ArrayMap<String, Object> targetsMap) {
880            mRotate = copy.mRotate;
881            mPivotX = copy.mPivotX;
882            mPivotY = copy.mPivotY;
883            mScaleX = copy.mScaleX;
884            mScaleY = copy.mScaleY;
885            mTranslateX = copy.mTranslateX;
886            mTranslateY = copy.mTranslateY;
887            mGroupAlpha = copy.mGroupAlpha;
888            mThemeAttrs = copy.mThemeAttrs;
889            mGroupName = copy.mGroupName;
890            mChangingConfigurations = copy.mChangingConfigurations;
891            if (mGroupName != null) {
892                targetsMap.put(mGroupName, this);
893            }
894
895            mLocalMatrix.set(copy.mLocalMatrix);
896
897            final ArrayList<Object> children = copy.mChildren;
898            for (int i = 0; i < children.size(); i++) {
899                Object copyChild = children.get(i);
900                if (copyChild instanceof VGroup) {
901                    VGroup copyGroup = (VGroup) copyChild;
902                    mChildren.add(new VGroup(copyGroup, targetsMap));
903                } else if (copyChild instanceof VPath) {
904                    VPath copyPath = (VPath) copyChild;
905                    VPath newPath = new VPath(copyPath);
906                    mChildren.add(newPath);
907                    if (newPath.mPathName != null) {
908                        targetsMap.put(newPath.mPathName, newPath);
909                    }
910                }
911            }
912        }
913
914        public VGroup() {
915        }
916
917        /* Getter and Setter */
918        public float getRotation() {
919            return mRotate;
920        }
921
922        public void setRotation(float rotation) {
923            if (rotation != mRotate) {
924                mRotate = rotation;
925                updateLocalMatrix();
926            }
927        }
928
929        public float getPivotX() {
930            return mPivotX;
931        }
932
933        public void setPivotX(float pivotX) {
934            if (pivotX != mPivotX) {
935                mPivotX = pivotX;
936                updateLocalMatrix();
937            }
938        }
939
940        public float getPivotY() {
941            return mPivotY;
942        }
943
944        public void setPivotY(float pivotY) {
945            if (pivotY != mPivotY) {
946                mPivotY = pivotY;
947                updateLocalMatrix();
948            }
949        }
950
951        public float getScaleX() {
952            return mScaleX;
953        }
954
955        public void setScaleX(float scaleX) {
956            if (scaleX != mScaleX) {
957                mScaleX = scaleX;
958                updateLocalMatrix();
959            }
960        }
961
962        public float getScaleY() {
963            return mScaleY;
964        }
965
966        public void setScaleY(float scaleY) {
967            if (scaleY != mScaleY) {
968                mScaleY = scaleY;
969                updateLocalMatrix();
970            }
971        }
972
973        public float getTranslateX() {
974            return mTranslateX;
975        }
976
977        public void setTranslateX(float translateX) {
978            if (translateX != mTranslateX) {
979                mTranslateX = translateX;
980                updateLocalMatrix();
981            }
982        }
983
984        public float getTranslateY() {
985            return mTranslateY;
986        }
987
988        public void setTranslateY(float translateY) {
989            if (translateY != mTranslateY) {
990                mTranslateY = translateY;
991                updateLocalMatrix();
992            }
993        }
994
995        public float getAlpha() {
996            return mGroupAlpha;
997        }
998
999        public void setAlpha(float groupAlpha) {
1000            if (groupAlpha != mGroupAlpha) {
1001                mGroupAlpha = groupAlpha;
1002            }
1003        }
1004
1005        public String getGroupName() {
1006            return mGroupName;
1007        }
1008
1009        public Matrix getLocalMatrix() {
1010            return mLocalMatrix;
1011        }
1012
1013        public boolean canApplyTheme() {
1014            return mThemeAttrs != null;
1015        }
1016
1017        public void inflate(Resources res, AttributeSet attrs, Theme theme) {
1018            final TypedArray a = obtainAttributes(res, theme, attrs,
1019                    R.styleable.VectorDrawableGroup);
1020            updateStateFromTypedArray(a);
1021            a.recycle();
1022        }
1023
1024        private void updateStateFromTypedArray(TypedArray a) {
1025            // Account for any configuration changes.
1026            mChangingConfigurations |= a.getChangingConfigurations();
1027
1028            // Extract the theme attributes, if any.
1029            mThemeAttrs = a.extractThemeAttrs();
1030
1031            mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate);
1032            mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX);
1033            mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY);
1034            mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX);
1035            mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY);
1036            mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX);
1037            mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY);
1038            mGroupAlpha = a.getFloat(R.styleable.VectorDrawableGroup_alpha, mGroupAlpha);
1039
1040            final String groupName = a.getString(R.styleable.VectorDrawableGroup_name);
1041            if (groupName != null) {
1042                mGroupName = groupName;
1043            }
1044
1045            updateLocalMatrix();
1046        }
1047
1048        public void applyTheme(Theme t) {
1049            if (mThemeAttrs == null) {
1050                return;
1051            }
1052
1053            final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawablePath);
1054            updateStateFromTypedArray(a);
1055            a.recycle();
1056        }
1057
1058        private void updateLocalMatrix() {
1059            // The order we apply is the same as the
1060            // RenderNode.cpp::applyViewPropertyTransforms().
1061            mLocalMatrix.reset();
1062            mLocalMatrix.postTranslate(-mPivotX, -mPivotY);
1063            mLocalMatrix.postScale(mScaleX, mScaleY);
1064            mLocalMatrix.postRotate(mRotate, 0, 0);
1065            mLocalMatrix.postTranslate(mTranslateX + mPivotX, mTranslateY + mPivotY);
1066        }
1067    }
1068
1069    private static class VPath {
1070        /////////////////////////////////////////////////////
1071        // Variables below need to be copied (deep copy if applicable) for mutation.
1072        private int[] mThemeAttrs;
1073
1074        int mStrokeColor = 0;
1075        float mStrokeWidth = 0;
1076        float mStrokeOpacity = Float.NaN;
1077        int mFillColor = Color.BLACK;
1078        int mFillRule;
1079        float mFillOpacity = Float.NaN;
1080        float mTrimPathStart = 0;
1081        float mTrimPathEnd = 1;
1082        float mTrimPathOffset = 0;
1083
1084        boolean mClip = false;
1085        Paint.Cap mStrokeLineCap = Paint.Cap.BUTT;
1086        Paint.Join mStrokeLineJoin = Paint.Join.MITER;
1087        float mStrokeMiterlimit = 4;
1088
1089        private PathParser.PathDataNode[] mNodes = null;
1090        String mPathName;
1091        private int mChangingConfigurations;
1092
1093        public VPath() {
1094            // Empty constructor.
1095        }
1096
1097        public VPath(VPath copy) {
1098            mThemeAttrs = copy.mThemeAttrs;
1099
1100            mStrokeColor = copy.mStrokeColor;
1101            mStrokeWidth = copy.mStrokeWidth;
1102            mStrokeOpacity = copy.mStrokeOpacity;
1103            mFillColor = copy.mFillColor;
1104            mFillRule = copy.mFillRule;
1105            mFillOpacity = copy.mFillOpacity;
1106            mTrimPathStart = copy.mTrimPathStart;
1107            mTrimPathEnd = copy.mTrimPathEnd;
1108            mTrimPathOffset = copy.mTrimPathOffset;
1109
1110            mClip = copy.mClip;
1111            mStrokeLineCap = copy.mStrokeLineCap;
1112            mStrokeLineJoin = copy.mStrokeLineJoin;
1113            mStrokeMiterlimit = copy.mStrokeMiterlimit;
1114
1115            mNodes = PathParser.deepCopyNodes(copy.mNodes);
1116            mPathName = copy.mPathName;
1117            mChangingConfigurations = copy.mChangingConfigurations;
1118        }
1119
1120        public void toPath(Path path) {
1121            path.reset();
1122            if (mNodes != null) {
1123                PathParser.PathDataNode.nodesToPath(mNodes, path);
1124            }
1125        }
1126
1127        public String getPathName() {
1128            return mPathName;
1129        }
1130
1131        private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) {
1132            switch (id) {
1133                case LINECAP_BUTT:
1134                    return Paint.Cap.BUTT;
1135                case LINECAP_ROUND:
1136                    return Paint.Cap.ROUND;
1137                case LINECAP_SQUARE:
1138                    return Paint.Cap.SQUARE;
1139                default:
1140                    return defValue;
1141            }
1142        }
1143
1144        private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) {
1145            switch (id) {
1146                case LINEJOIN_MITER:
1147                    return Paint.Join.MITER;
1148                case LINEJOIN_ROUND:
1149                    return Paint.Join.ROUND;
1150                case LINEJOIN_BEVEL:
1151                    return Paint.Join.BEVEL;
1152                default:
1153                    return defValue;
1154            }
1155        }
1156
1157        /* Setters and Getters, mostly used by animator from AnimatedVectorDrawable. */
1158        @SuppressWarnings("unused")
1159        public PathParser.PathDataNode[] getPathData() {
1160            return mNodes;
1161        }
1162
1163        @SuppressWarnings("unused")
1164        public void setPathData(PathParser.PathDataNode[] nodes) {
1165            if (!PathParser.canMorph(mNodes, nodes)) {
1166                // This should not happen in the middle of animation.
1167                mNodes = PathParser.deepCopyNodes(nodes);
1168            } else {
1169                PathParser.updateNodes(mNodes, nodes);
1170            }
1171        }
1172
1173        @SuppressWarnings("unused")
1174        int getStroke() {
1175            return mStrokeColor;
1176        }
1177
1178        @SuppressWarnings("unused")
1179        void setStroke(int strokeColor) {
1180            mStrokeColor = strokeColor;
1181        }
1182
1183        @SuppressWarnings("unused")
1184        float getStrokeWidth() {
1185            return mStrokeWidth;
1186        }
1187
1188        @SuppressWarnings("unused")
1189        void setStrokeWidth(float strokeWidth) {
1190            mStrokeWidth = strokeWidth;
1191        }
1192
1193        @SuppressWarnings("unused")
1194        float getStrokeOpacity() {
1195            return mStrokeOpacity;
1196        }
1197
1198        @SuppressWarnings("unused")
1199        void setStrokeOpacity(float strokeOpacity) {
1200            mStrokeOpacity = strokeOpacity;
1201        }
1202
1203        @SuppressWarnings("unused")
1204        int getFill() {
1205            return mFillColor;
1206        }
1207
1208        @SuppressWarnings("unused")
1209        void setFill(int fillColor) {
1210            mFillColor = fillColor;
1211        }
1212
1213        @SuppressWarnings("unused")
1214        float getFillOpacity() {
1215            return mFillOpacity;
1216        }
1217
1218        @SuppressWarnings("unused")
1219        void setFillOpacity(float fillOpacity) {
1220            mFillOpacity = fillOpacity;
1221        }
1222
1223        @SuppressWarnings("unused")
1224        float getTrimPathStart() {
1225            return mTrimPathStart;
1226        }
1227
1228        @SuppressWarnings("unused")
1229        void setTrimPathStart(float trimPathStart) {
1230            mTrimPathStart = trimPathStart;
1231        }
1232
1233        @SuppressWarnings("unused")
1234        float getTrimPathEnd() {
1235            return mTrimPathEnd;
1236        }
1237
1238        @SuppressWarnings("unused")
1239        void setTrimPathEnd(float trimPathEnd) {
1240            mTrimPathEnd = trimPathEnd;
1241        }
1242
1243        @SuppressWarnings("unused")
1244        float getTrimPathOffset() {
1245            return mTrimPathOffset;
1246        }
1247
1248        @SuppressWarnings("unused")
1249        void setTrimPathOffset(float trimPathOffset) {
1250            mTrimPathOffset = trimPathOffset;
1251        }
1252
1253        public boolean canApplyTheme() {
1254            return mThemeAttrs != null;
1255        }
1256
1257        public void inflate(Resources r, AttributeSet attrs, Theme theme) {
1258            final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawablePath);
1259            updateStateFromTypedArray(a);
1260            a.recycle();
1261        }
1262
1263        private void updateStateFromTypedArray(TypedArray a) {
1264            // Account for any configuration changes.
1265            mChangingConfigurations |= a.getChangingConfigurations();
1266
1267            // Extract the theme attributes, if any.
1268            mThemeAttrs = a.extractThemeAttrs();
1269
1270            mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip);
1271
1272            mPathName = a.getString(R.styleable.VectorDrawablePath_name);
1273            mNodes = PathParser.createNodesFromPathData(a.getString(
1274                    R.styleable.VectorDrawablePath_pathData));
1275
1276            mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor);
1277            mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity);
1278            mStrokeLineCap = getStrokeLineCap(a.getInt(
1279                    R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap);
1280            mStrokeLineJoin = getStrokeLineJoin(a.getInt(
1281                    R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin);
1282            mStrokeMiterlimit = a.getFloat(
1283                    R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit);
1284            mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor);
1285            mStrokeOpacity = a.getFloat(
1286                    R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity);
1287            mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth);
1288            mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd);
1289            mTrimPathOffset = a.getFloat(
1290                    R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset);
1291            mTrimPathStart = a.getFloat(
1292                    R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart);
1293
1294            updateColorAlphas();
1295        }
1296
1297        public void applyTheme(Theme t) {
1298            if (mThemeAttrs == null) {
1299                return;
1300            }
1301
1302            final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawablePath);
1303            updateStateFromTypedArray(a);
1304            a.recycle();
1305        }
1306
1307        private void updateColorAlphas() {
1308            if (!Float.isNaN(mFillOpacity)) {
1309                mFillColor = applyAlpha(mFillColor, mFillOpacity);
1310            }
1311
1312            if (!Float.isNaN(mStrokeOpacity)) {
1313                mStrokeColor = applyAlpha(mStrokeColor, mStrokeOpacity);
1314            }
1315        }
1316    }
1317}
1318