VectorDrawable.java revision 87e1938be8e6c2ac7e6163a63df5dd69633cc836
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.annotation.NonNull;
18import android.content.res.ColorStateList;
19import android.content.res.Resources;
20import android.content.res.Resources.Theme;
21import android.content.res.TypedArray;
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.ColorFilter;
26import android.graphics.Insets;
27import android.graphics.Matrix;
28import android.graphics.Paint;
29import android.graphics.Path;
30import android.graphics.PathMeasure;
31import android.graphics.PixelFormat;
32import android.graphics.PorterDuffColorFilter;
33import android.graphics.Rect;
34import android.graphics.Region;
35import android.graphics.PorterDuff.Mode;
36import android.util.ArrayMap;
37import android.util.AttributeSet;
38import android.util.LayoutDirection;
39import android.util.Log;
40import android.util.MathUtils;
41import android.util.PathParser;
42import android.util.Xml;
43
44import com.android.internal.R;
45
46import org.xmlpull.v1.XmlPullParser;
47import org.xmlpull.v1.XmlPullParserException;
48
49import java.io.IOException;
50import java.util.ArrayList;
51import java.util.Stack;
52
53/**
54 * This lets you create a drawable based on an XML vector graphic. It can be
55 * defined in an XML file with the <code>&lt;vector></code> element.
56 * <p/>
57 * The vector drawable has the following elements:
58 * <p/>
59 * <dt><code>&lt;vector></code></dt>
60 * <dl>
61 * <dd>Used to define a vector drawable
62 * <dl>
63 * <dt><code>android:name</code></dt>
64 * <dd>Defines the name of this vector drawable.</dd>
65 * <dt><code>android:width</code></dt>
66 * <dd>Used to define the intrinsic width of the drawable.
67 * This support all the dimension units, normally specified with dp.</dd>
68 * <dt><code>android:height</code></dt>
69 * <dd>Used to define the intrinsic height the drawable.
70 * This support all the dimension units, normally specified with dp.</dd>
71 * <dt><code>android:viewportWidth</code></dt>
72 * <dd>Used to define the width of the viewport space. Viewport is basically
73 * the virtual canvas where the paths are drawn on.</dd>
74 * <dt><code>android:viewportHeight</code></dt>
75 * <dd>Used to define the height of the viewport space. Viewport is basically
76 * the virtual canvas where the paths are drawn on.</dd>
77 * <dt><code>android:tint</code></dt>
78 * <dd>The color to apply to the drawable as a tint. By default, no tint is applied.</dd>
79 * <dt><code>android:tintMode</code></dt>
80 * <dd>The Porter-Duff blending mode for the tint color. The default value is src_in.</dd>
81 * <dt><code>android:autoMirrored</code></dt>
82 * <dd>Indicates if the drawable needs to be mirrored when its layout direction is
83 * RTL (right-to-left).</dd>
84 * <dt><code>android:alpha</code></dt>
85 * <dd>The opacity of this drawable.</dd>
86 * </dl></dd>
87 * </dl>
88 *
89 * <dl>
90 * <dt><code>&lt;group></code></dt>
91 * <dd>Defines a group of paths or subgroups, plus transformation information.
92 * The transformations are defined in the same coordinates as the viewport.
93 * And the transformations are applied in the order of scale, rotate then translate.
94 * <dl>
95 * <dt><code>android:name</code></dt>
96 * <dd>Defines the name of the group.</dd>
97 * <dt><code>android:rotation</code></dt>
98 * <dd>The degrees of rotation of the group.</dd>
99 * <dt><code>android:pivotX</code></dt>
100 * <dd>The X coordinate of the pivot for the scale and rotation of the group.
101 * This is defined in the viewport space.</dd>
102 * <dt><code>android:pivotY</code></dt>
103 * <dd>The Y coordinate of the pivot for the scale and rotation of the group.
104 * This is defined in the viewport space.</dd>
105 * <dt><code>android:scaleX</code></dt>
106 * <dd>The amount of scale on the X Coordinate.</dd>
107 * <dt><code>android:scaleY</code></dt>
108 * <dd>The amount of scale on the Y coordinate.</dd>
109 * <dt><code>android:translateX</code></dt>
110 * <dd>The amount of translation on the X coordinate.
111 * This is defined in the viewport space.</dd>
112 * <dt><code>android:translateY</code></dt>
113 * <dd>The amount of translation on the Y coordinate.
114 * This is defined in the viewport space.</dd>
115 * </dl></dd>
116 * </dl>
117 *
118 * <dl>
119 * <dt><code>&lt;path></code></dt>
120 * <dd>Defines paths to be drawn.
121 * <dl>
122 * <dt><code>android:name</code></dt>
123 * <dd>Defines the name of the path.</dd>
124 * <dt><code>android:pathData</code></dt>
125 * <dd>Defines path data using exactly same format as "d" attribute
126 * in the SVG's path data. This is defined in the viewport space.</dd>
127 * <dt><code>android:fillColor</code></dt>
128 * <dd>Defines the color to fill the path (none if not present).</dd>
129 * <dt><code>android:strokeColor</code></dt>
130 * <dd>Defines the color to draw the path outline (none if not present).</dd>
131 * <dt><code>android:strokeWidth</code></dt>
132 * <dd>The width a path stroke.</dd>
133 * <dt><code>android:strokeAlpha</code></dt>
134 * <dd>The opacity of a path stroke.</dd>
135 * <dt><code>android:fillAlpha</code></dt>
136 * <dd>The opacity to fill the path with.</dd>
137 * <dt><code>android:trimPathStart</code></dt>
138 * <dd>The fraction of the path to trim from the start, in the range from 0 to 1.</dd>
139 * <dt><code>android:trimPathEnd</code></dt>
140 * <dd>The fraction of the path to trim from the end, in the range from 0 to 1.</dd>
141 * <dt><code>android:trimPathOffset</code></dt>
142 * <dd>Shift trim region (allows showed region to include the start and end), in the range
143 * from 0 to 1.</dd>
144 * <dt><code>android:strokeLineCap</code></dt>
145 * <dd>Sets the linecap for a stroked path: butt, round, square.</dd>
146 * <dt><code>android:strokeLineJoin</code></dt>
147 * <dd>Sets the lineJoin for a stroked path: miter,round,bevel.</dd>
148 * <dt><code>android:strokeMiterLimit</code></dt>
149 * <dd>Sets the Miter limit for a stroked path.</dd>
150 * </dl></dd>
151 * </dl>
152 *
153 * <dl>
154 * <dt><code>&lt;clip-path></code></dt>
155 * <dd>Defines path to be the current clip. Note that the clip path only apply to
156 * the current group and its children.
157 * <dl>
158 * <dt><code>android:name</code></dt>
159 * <dd>Defines the name of the clip path.</dd>
160 * <dt><code>android:pathData</code></dt>
161 * <dd>Defines clip path using the same format as "d" attribute
162 * in the SVG's path data.</dd>
163 * </dl></dd>
164 * </dl>
165 * <li>Here is a simple VectorDrawable in this vectordrawable.xml file.
166 * <pre>
167 * &lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android";
168 *     android:height=&quot;64dp&quot;
169 *     android:width=&quot;64dp&quot;
170 *     android:viewportHeight=&quot;600&quot;
171 *     android:viewportWidth=&quot;600&quot; &gt;
172 *     &lt;group
173 *         android:name=&quot;rotationGroup&quot;
174 *         android:pivotX=&quot;300.0&quot;
175 *         android:pivotY=&quot;300.0&quot;
176 *         android:rotation=&quot;45.0&quot; &gt;
177 *         &lt;path
178 *             android:name=&quot;v&quot;
179 *             android:fillColor=&quot;#000000&quot;
180 *             android:pathData=&quot;M300,70 l 0,-70 70,70 0,0 -70,70z&quot; /&gt;
181 *     &lt;/group&gt;
182 * &lt;/vector&gt;
183 * </pre></li>
184 */
185
186public class VectorDrawable extends Drawable {
187    private static final String LOGTAG = VectorDrawable.class.getSimpleName();
188
189    private static final String SHAPE_CLIP_PATH = "clip-path";
190    private static final String SHAPE_GROUP = "group";
191    private static final String SHAPE_PATH = "path";
192    private static final String SHAPE_VECTOR = "vector";
193
194    private static final int LINECAP_BUTT = 0;
195    private static final int LINECAP_ROUND = 1;
196    private static final int LINECAP_SQUARE = 2;
197
198    private static final int LINEJOIN_MITER = 0;
199    private static final int LINEJOIN_ROUND = 1;
200    private static final int LINEJOIN_BEVEL = 2;
201
202    private static final boolean DBG_VECTOR_DRAWABLE = false;
203
204    private VectorDrawableState mVectorState;
205
206    private PorterDuffColorFilter mTintFilter;
207    private ColorFilter mColorFilter;
208
209    private boolean mMutated;
210
211    // AnimatedVectorDrawable needs to turn off the cache all the time, otherwise,
212    // caching the bitmap by default is allowed.
213    private boolean mAllowCaching = true;
214
215    public VectorDrawable() {
216        mVectorState = new VectorDrawableState();
217    }
218
219    private VectorDrawable(@NonNull VectorDrawableState state) {
220        mVectorState = state;
221        mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
222    }
223
224    @Override
225    public Drawable mutate() {
226        if (!mMutated && super.mutate() == this) {
227            mVectorState = new VectorDrawableState(mVectorState);
228            mMutated = true;
229        }
230        return this;
231    }
232
233    /**
234     * @hide
235     */
236    public void clearMutated() {
237        super.clearMutated();
238        mMutated = false;
239    }
240
241    Object getTargetByName(String name) {
242        return mVectorState.mVPathRenderer.mVGTargetsMap.get(name);
243    }
244
245    @Override
246    public ConstantState getConstantState() {
247        mVectorState.mChangingConfigurations = getChangingConfigurations();
248        return mVectorState;
249    }
250
251    @Override
252    public void draw(Canvas canvas) {
253        final Rect bounds = getBounds();
254        if (bounds.width() <= 0 || bounds.height() <= 0) {
255            // Nothing to draw
256            return;
257        }
258
259        final int saveCount = canvas.save();
260        final boolean needMirroring = needMirroring();
261
262        canvas.translate(bounds.left, bounds.top);
263        if (needMirroring) {
264            canvas.translate(bounds.width(), 0);
265            canvas.scale(-1.0f, 1.0f);
266        }
267
268        // Color filters always override tint filters.
269        final ColorFilter colorFilter = mColorFilter == null ? mTintFilter : mColorFilter;
270
271        if (!mAllowCaching) {
272            // AnimatedVectorDrawable
273            if (!mVectorState.hasTranslucentRoot()) {
274                mVectorState.mVPathRenderer.draw(
275                        canvas, bounds.width(), bounds.height(), colorFilter);
276            } else {
277                mVectorState.createCachedBitmapIfNeeded(bounds);
278                mVectorState.updateCachedBitmap(bounds);
279                mVectorState.drawCachedBitmapWithRootAlpha(canvas, colorFilter);
280            }
281        } else {
282            // Static Vector Drawable case.
283            mVectorState.createCachedBitmapIfNeeded(bounds);
284            if (!mVectorState.canReuseCache()) {
285                mVectorState.updateCachedBitmap(bounds);
286                mVectorState.updateCacheStates();
287            }
288            mVectorState.drawCachedBitmapWithRootAlpha(canvas, colorFilter);
289        }
290
291        canvas.restoreToCount(saveCount);
292    }
293
294    @Override
295    public int getAlpha() {
296        return mVectorState.mVPathRenderer.getRootAlpha();
297    }
298
299    @Override
300    public void setAlpha(int alpha) {
301        if (mVectorState.mVPathRenderer.getRootAlpha() != alpha) {
302            mVectorState.mVPathRenderer.setRootAlpha(alpha);
303            invalidateSelf();
304        }
305    }
306
307    @Override
308    public void setColorFilter(ColorFilter colorFilter) {
309        mColorFilter = colorFilter;
310        invalidateSelf();
311    }
312
313    @Override
314    public void setTintList(ColorStateList tint) {
315        final VectorDrawableState state = mVectorState;
316        if (state.mTint != tint) {
317            state.mTint = tint;
318            mTintFilter = updateTintFilter(mTintFilter, tint, state.mTintMode);
319            invalidateSelf();
320        }
321    }
322
323    @Override
324    public void setTintMode(Mode tintMode) {
325        final VectorDrawableState state = mVectorState;
326        if (state.mTintMode != tintMode) {
327            state.mTintMode = tintMode;
328            mTintFilter = updateTintFilter(mTintFilter, state.mTint, tintMode);
329            invalidateSelf();
330        }
331    }
332
333    @Override
334    public boolean isStateful() {
335        return super.isStateful() || (mVectorState != null && mVectorState.mTint != null
336                && mVectorState.mTint.isStateful());
337    }
338
339    @Override
340    protected boolean onStateChange(int[] stateSet) {
341        final VectorDrawableState state = mVectorState;
342        if (state.mTint != null && state.mTintMode != null) {
343            mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
344            invalidateSelf();
345            return true;
346        }
347        return false;
348    }
349
350    @Override
351    public int getOpacity() {
352        return PixelFormat.TRANSLUCENT;
353    }
354
355    @Override
356    public int getIntrinsicWidth() {
357        return (int) mVectorState.mVPathRenderer.mBaseWidth;
358    }
359
360    @Override
361    public int getIntrinsicHeight() {
362        return (int) mVectorState.mVPathRenderer.mBaseHeight;
363    }
364
365    /** @hide */
366    @Override
367    public Insets getOpticalInsets() {
368        return mVectorState.mVPathRenderer.mOpticalInsets;
369    }
370
371    @Override
372    public boolean canApplyTheme() {
373        return (mVectorState != null && mVectorState.canApplyTheme()) || super.canApplyTheme();
374    }
375
376    @Override
377    public void applyTheme(Theme t) {
378        super.applyTheme(t);
379
380        final VectorDrawableState state = mVectorState;
381        if (state == null) {
382            return;
383        }
384
385        if (state.mThemeAttrs != null) {
386            final TypedArray a = t.resolveAttributes(
387                    state.mThemeAttrs, R.styleable.VectorDrawable);
388            try {
389                state.mCacheDirty = true;
390                updateStateFromTypedArray(a);
391            } catch (XmlPullParserException e) {
392                throw new RuntimeException(e);
393            } finally {
394                a.recycle();
395            }
396        }
397
398        // Apply theme to contained color state list.
399        if (state.mTint != null && state.mTint.canApplyTheme()) {
400            state.mTint = state.mTint.obtainForTheme(t);
401        }
402
403        final VPathRenderer path = state.mVPathRenderer;
404        if (path != null && path.canApplyTheme()) {
405            path.applyTheme(t);
406        }
407
408        // Update local state.
409        mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
410    }
411
412    /**
413     * The size of a pixel when scaled from the intrinsic dimension to the viewport dimension.
414     * This is used to calculate the path animation accuracy.
415     *
416     * @hide
417     */
418    public float getPixelSize() {
419        if (mVectorState == null || mVectorState.mVPathRenderer == null ||
420                mVectorState.mVPathRenderer.mBaseWidth == 0 ||
421                mVectorState.mVPathRenderer.mBaseHeight == 0 ||
422                mVectorState.mVPathRenderer.mViewportHeight == 0 ||
423                mVectorState.mVPathRenderer.mViewportWidth == 0) {
424            return 1; // fall back to 1:1 pixel mapping.
425        }
426        float intrinsicWidth = mVectorState.mVPathRenderer.mBaseWidth;
427        float intrinsicHeight = mVectorState.mVPathRenderer.mBaseHeight;
428        float viewportWidth = mVectorState.mVPathRenderer.mViewportWidth;
429        float viewportHeight = mVectorState.mVPathRenderer.mViewportHeight;
430        float scaleX = viewportWidth / intrinsicWidth;
431        float scaleY = viewportHeight / intrinsicHeight;
432        return Math.min(scaleX, scaleY);
433    }
434
435    /** @hide */
436    public static VectorDrawable create(Resources resources, int rid) {
437        try {
438            final XmlPullParser parser = resources.getXml(rid);
439            final AttributeSet attrs = Xml.asAttributeSet(parser);
440            int type;
441            while ((type=parser.next()) != XmlPullParser.START_TAG &&
442                    type != XmlPullParser.END_DOCUMENT) {
443                // Empty loop
444            }
445            if (type != XmlPullParser.START_TAG) {
446                throw new XmlPullParserException("No start tag found");
447            }
448
449            final VectorDrawable drawable = new VectorDrawable();
450            drawable.inflate(resources, parser, attrs);
451
452            return drawable;
453        } catch (XmlPullParserException e) {
454            Log.e(LOGTAG, "parser error", e);
455        } catch (IOException e) {
456            Log.e(LOGTAG, "parser error", e);
457        }
458        return null;
459    }
460
461    private static int applyAlpha(int color, float alpha) {
462        int alphaBytes = Color.alpha(color);
463        color &= 0x00FFFFFF;
464        color |= ((int) (alphaBytes * alpha)) << 24;
465        return color;
466    }
467
468    @Override
469    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
470            throws XmlPullParserException, IOException {
471        final VectorDrawableState state = mVectorState;
472        final VPathRenderer pathRenderer = new VPathRenderer();
473        state.mVPathRenderer = pathRenderer;
474
475        final TypedArray a = obtainAttributes(res, theme, attrs, R.styleable.VectorDrawable);
476        updateStateFromTypedArray(a);
477        a.recycle();
478
479        state.mCacheDirty = true;
480        inflateInternal(res, parser, attrs, theme);
481
482        mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
483    }
484
485    private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
486        final VectorDrawableState state = mVectorState;
487        final VPathRenderer pathRenderer = state.mVPathRenderer;
488
489        // Account for any configuration changes.
490        state.mChangingConfigurations |= a.getChangingConfigurations();
491
492        // Extract the theme attributes, if any.
493        state.mThemeAttrs = a.extractThemeAttrs();
494
495        final int tintMode = a.getInt(R.styleable.VectorDrawable_tintMode, -1);
496        if (tintMode != -1) {
497            state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN);
498        }
499
500        final ColorStateList tint = a.getColorStateList(R.styleable.VectorDrawable_tint);
501        if (tint != null) {
502            state.mTint = tint;
503        }
504
505        state.mAutoMirrored = a.getBoolean(
506                R.styleable.VectorDrawable_autoMirrored, state.mAutoMirrored);
507
508        pathRenderer.mViewportWidth = a.getFloat(
509                R.styleable.VectorDrawable_viewportWidth, pathRenderer.mViewportWidth);
510        pathRenderer.mViewportHeight = a.getFloat(
511                R.styleable.VectorDrawable_viewportHeight, pathRenderer.mViewportHeight);
512
513        if (pathRenderer.mViewportWidth <= 0) {
514            throw new XmlPullParserException(a.getPositionDescription() +
515                    "<vector> tag requires viewportWidth > 0");
516        } else if (pathRenderer.mViewportHeight <= 0) {
517            throw new XmlPullParserException(a.getPositionDescription() +
518                    "<vector> tag requires viewportHeight > 0");
519        }
520
521        pathRenderer.mBaseWidth = a.getDimension(
522                R.styleable.VectorDrawable_width, pathRenderer.mBaseWidth);
523        pathRenderer.mBaseHeight = a.getDimension(
524                R.styleable.VectorDrawable_height, pathRenderer.mBaseHeight);
525
526        if (pathRenderer.mBaseWidth <= 0) {
527            throw new XmlPullParserException(a.getPositionDescription() +
528                    "<vector> tag requires width > 0");
529        } else if (pathRenderer.mBaseHeight <= 0) {
530            throw new XmlPullParserException(a.getPositionDescription() +
531                    "<vector> tag requires height > 0");
532        }
533
534        final int insetLeft = a.getDimensionPixelSize(
535                R.styleable.VectorDrawable_opticalInsetLeft, pathRenderer.mOpticalInsets.left);
536        final int insetTop = a.getDimensionPixelSize(
537                R.styleable.VectorDrawable_opticalInsetTop, pathRenderer.mOpticalInsets.top);
538        final int insetRight = a.getDimensionPixelSize(
539                R.styleable.VectorDrawable_opticalInsetRight, pathRenderer.mOpticalInsets.right);
540        final int insetBottom = a.getDimensionPixelSize(
541                R.styleable.VectorDrawable_opticalInsetBottom, pathRenderer.mOpticalInsets.bottom);
542        pathRenderer.mOpticalInsets = Insets.of(insetLeft, insetTop, insetRight, insetBottom);
543
544        final float alphaInFloat = a.getFloat(R.styleable.VectorDrawable_alpha,
545                pathRenderer.getAlpha());
546        pathRenderer.setAlpha(alphaInFloat);
547
548        final String name = a.getString(R.styleable.VectorDrawable_name);
549        if (name != null) {
550            pathRenderer.mRootName = name;
551            pathRenderer.mVGTargetsMap.put(name, pathRenderer);
552        }
553    }
554
555    private void inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs,
556            Theme theme) throws XmlPullParserException, IOException {
557        final VectorDrawableState state = mVectorState;
558        final VPathRenderer pathRenderer = state.mVPathRenderer;
559        boolean noPathTag = true;
560
561        // Use a stack to help to build the group tree.
562        // The top of the stack is always the current group.
563        final Stack<VGroup> groupStack = new Stack<VGroup>();
564        groupStack.push(pathRenderer.mRootGroup);
565
566        int eventType = parser.getEventType();
567        while (eventType != XmlPullParser.END_DOCUMENT) {
568            if (eventType == XmlPullParser.START_TAG) {
569                final String tagName = parser.getName();
570                final VGroup currentGroup = groupStack.peek();
571
572                if (SHAPE_PATH.equals(tagName)) {
573                    final VFullPath path = new VFullPath();
574                    path.inflate(res, attrs, theme);
575                    currentGroup.mChildren.add(path);
576                    if (path.getPathName() != null) {
577                        pathRenderer.mVGTargetsMap.put(path.getPathName(), path);
578                    }
579                    noPathTag = false;
580                    state.mChangingConfigurations |= path.mChangingConfigurations;
581                } else if (SHAPE_CLIP_PATH.equals(tagName)) {
582                    final VClipPath path = new VClipPath();
583                    path.inflate(res, attrs, theme);
584                    currentGroup.mChildren.add(path);
585                    if (path.getPathName() != null) {
586                        pathRenderer.mVGTargetsMap.put(path.getPathName(), path);
587                    }
588                    state.mChangingConfigurations |= path.mChangingConfigurations;
589                } else if (SHAPE_GROUP.equals(tagName)) {
590                    VGroup newChildGroup = new VGroup();
591                    newChildGroup.inflate(res, attrs, theme);
592                    currentGroup.mChildren.add(newChildGroup);
593                    groupStack.push(newChildGroup);
594                    if (newChildGroup.getGroupName() != null) {
595                        pathRenderer.mVGTargetsMap.put(newChildGroup.getGroupName(),
596                                newChildGroup);
597                    }
598                    state.mChangingConfigurations |= newChildGroup.mChangingConfigurations;
599                }
600            } else if (eventType == XmlPullParser.END_TAG) {
601                final String tagName = parser.getName();
602                if (SHAPE_GROUP.equals(tagName)) {
603                    groupStack.pop();
604                }
605            }
606            eventType = parser.next();
607        }
608
609        // Print the tree out for debug.
610        if (DBG_VECTOR_DRAWABLE) {
611            printGroupTree(pathRenderer.mRootGroup, 0);
612        }
613
614        if (noPathTag) {
615            final StringBuffer tag = new StringBuffer();
616
617            if (tag.length() > 0) {
618                tag.append(" or ");
619            }
620            tag.append(SHAPE_PATH);
621
622            throw new XmlPullParserException("no " + tag + " defined");
623        }
624    }
625
626    private void printGroupTree(VGroup currentGroup, int level) {
627        String indent = "";
628        for (int i = 0; i < level; i++) {
629            indent += "    ";
630        }
631        // Print the current node
632        Log.v(LOGTAG, indent + "current group is :" + currentGroup.getGroupName()
633                + " rotation is " + currentGroup.mRotate);
634        Log.v(LOGTAG, indent + "matrix is :" + currentGroup.getLocalMatrix().toString());
635        // Then print all the children groups
636        for (int i = 0; i < currentGroup.mChildren.size(); i++) {
637            Object child = currentGroup.mChildren.get(i);
638            if (child instanceof VGroup) {
639                printGroupTree((VGroup) child, level + 1);
640            }
641        }
642    }
643
644    @Override
645    public int getChangingConfigurations() {
646        return super.getChangingConfigurations() | mVectorState.getChangingConfigurations();
647    }
648
649    void setAllowCaching(boolean allowCaching) {
650        mAllowCaching = allowCaching;
651    }
652
653    private boolean needMirroring() {
654        return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
655    }
656
657    @Override
658    public void setAutoMirrored(boolean mirrored) {
659        if (mVectorState.mAutoMirrored != mirrored) {
660            mVectorState.mAutoMirrored = mirrored;
661            invalidateSelf();
662        }
663    }
664
665    @Override
666    public boolean isAutoMirrored() {
667        return mVectorState.mAutoMirrored;
668    }
669
670    private static class VectorDrawableState extends ConstantState {
671        int[] mThemeAttrs;
672        int mChangingConfigurations;
673        VPathRenderer mVPathRenderer;
674        ColorStateList mTint = null;
675        Mode mTintMode = DEFAULT_TINT_MODE;
676        boolean mAutoMirrored;
677
678        Bitmap mCachedBitmap;
679        int[] mCachedThemeAttrs;
680        ColorStateList mCachedTint;
681        Mode mCachedTintMode;
682        int mCachedRootAlpha;
683        boolean mCachedAutoMirrored;
684        boolean mCacheDirty;
685
686        /** Temporary paint object used to draw cached bitmaps. */
687        Paint mTempPaint;
688
689        // Deep copy for mutate() or implicitly mutate.
690        public VectorDrawableState(VectorDrawableState copy) {
691            if (copy != null) {
692                mThemeAttrs = copy.mThemeAttrs;
693                mChangingConfigurations = copy.mChangingConfigurations;
694                mVPathRenderer = new VPathRenderer(copy.mVPathRenderer);
695                if (copy.mVPathRenderer.mFillPaint != null) {
696                    mVPathRenderer.mFillPaint = new Paint(copy.mVPathRenderer.mFillPaint);
697                }
698                if (copy.mVPathRenderer.mStrokePaint != null) {
699                    mVPathRenderer.mStrokePaint = new Paint(copy.mVPathRenderer.mStrokePaint);
700                }
701                mTint = copy.mTint;
702                mTintMode = copy.mTintMode;
703                mAutoMirrored = copy.mAutoMirrored;
704            }
705        }
706
707        public void drawCachedBitmapWithRootAlpha(Canvas canvas, ColorFilter filter) {
708            // The bitmap's size is the same as the bounds.
709            final Paint p = getPaint(filter);
710            canvas.drawBitmap(mCachedBitmap, 0, 0, p);
711        }
712
713        public boolean hasTranslucentRoot() {
714            return mVPathRenderer.getRootAlpha() < 255;
715        }
716
717        /**
718         * @return null when there is no need for alpha paint.
719         */
720        public Paint getPaint(ColorFilter filter) {
721            if (!hasTranslucentRoot() && filter == null) {
722                return null;
723            }
724
725            if (mTempPaint == null) {
726                mTempPaint = new Paint();
727                mTempPaint.setFilterBitmap(true);
728            }
729            mTempPaint.setAlpha(mVPathRenderer.getRootAlpha());
730            mTempPaint.setColorFilter(filter);
731            return mTempPaint;
732        }
733
734        public void updateCachedBitmap(Rect bounds) {
735            mCachedBitmap.eraseColor(Color.TRANSPARENT);
736            Canvas tmpCanvas = new Canvas(mCachedBitmap);
737            mVPathRenderer.draw(tmpCanvas, bounds.width(), bounds.height(), null);
738        }
739
740        public void createCachedBitmapIfNeeded(Rect bounds) {
741            if (mCachedBitmap == null || !canReuseBitmap(bounds.width(),
742                    bounds.height())) {
743                mCachedBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(),
744                        Bitmap.Config.ARGB_8888);
745                mCacheDirty = true;
746            }
747
748        }
749
750        public boolean canReuseBitmap(int width, int height) {
751            if (width == mCachedBitmap.getWidth()
752                    && height == mCachedBitmap.getHeight()) {
753                return true;
754            }
755            return false;
756        }
757
758        public boolean canReuseCache() {
759            if (!mCacheDirty
760                    && mCachedThemeAttrs == mThemeAttrs
761                    && mCachedTint == mTint
762                    && mCachedTintMode == mTintMode
763                    && mCachedAutoMirrored == mAutoMirrored
764                    && mCachedRootAlpha == mVPathRenderer.getRootAlpha()) {
765                return true;
766            }
767            return false;
768        }
769
770        public void updateCacheStates() {
771            // Use shallow copy here and shallow comparison in canReuseCache(),
772            // likely hit cache miss more, but practically not much difference.
773            mCachedThemeAttrs = mThemeAttrs;
774            mCachedTint = mTint;
775            mCachedTintMode = mTintMode;
776            mCachedRootAlpha = mVPathRenderer.getRootAlpha();
777            mCachedAutoMirrored = mAutoMirrored;
778            mCacheDirty = false;
779        }
780
781        @Override
782        public boolean canApplyTheme() {
783            return mThemeAttrs != null
784                    || (mVPathRenderer != null && mVPathRenderer.canApplyTheme())
785                    || (mTint != null && mTint.canApplyTheme())
786                    || super.canApplyTheme();
787        }
788
789        public VectorDrawableState() {
790            mVPathRenderer = new VPathRenderer();
791        }
792
793        @Override
794        public Drawable newDrawable() {
795            return new VectorDrawable(this);
796        }
797
798        @Override
799        public Drawable newDrawable(Resources res) {
800            return new VectorDrawable(this);
801        }
802
803        @Override
804        public int getChangingConfigurations() {
805            return mChangingConfigurations
806                    | (mTint != null ? mTint.getChangingConfigurations() : 0);
807        }
808    }
809
810    private static class VPathRenderer {
811        /* Right now the internal data structure is organized as a tree.
812         * Each node can be a group node, or a path.
813         * A group node can have groups or paths as children, but a path node has
814         * no children.
815         * One example can be:
816         *                 Root Group
817         *                /    |     \
818         *           Group    Path    Group
819         *          /     \             |
820         *         Path   Path         Path
821         *
822         */
823        // Variables that only used temporarily inside the draw() call, so there
824        // is no need for deep copying.
825        private final Path mPath;
826        private final Path mRenderPath;
827        private final Matrix mFinalPathMatrix = new Matrix();
828
829        private Paint mStrokePaint;
830        private Paint mFillPaint;
831        private PathMeasure mPathMeasure;
832
833        /////////////////////////////////////////////////////
834        // Variables below need to be copied (deep copy if applicable) for mutation.
835        private int mChangingConfigurations;
836        private final VGroup mRootGroup;
837        float mBaseWidth = 0;
838        float mBaseHeight = 0;
839        float mViewportWidth = 0;
840        float mViewportHeight = 0;
841        Insets mOpticalInsets = Insets.NONE;
842        int mRootAlpha = 0xFF;
843        String mRootName = null;
844
845        final ArrayMap<String, Object> mVGTargetsMap = new ArrayMap<String, Object>();
846
847        public VPathRenderer() {
848            mRootGroup = new VGroup();
849            mPath = new Path();
850            mRenderPath = new Path();
851        }
852
853        public void setRootAlpha(int alpha) {
854            mRootAlpha = alpha;
855        }
856
857        public int getRootAlpha() {
858            return mRootAlpha;
859        }
860
861        // setAlpha() and getAlpha() are used mostly for animation purpose, since
862        // Animator like to use alpha from 0 to 1.
863        public void setAlpha(float alpha) {
864            setRootAlpha((int) (alpha * 255));
865        }
866
867        @SuppressWarnings("unused")
868        public float getAlpha() {
869            return getRootAlpha() / 255.0f;
870        }
871
872        public VPathRenderer(VPathRenderer copy) {
873            mRootGroup = new VGroup(copy.mRootGroup, mVGTargetsMap);
874            mPath = new Path(copy.mPath);
875            mRenderPath = new Path(copy.mRenderPath);
876            mBaseWidth = copy.mBaseWidth;
877            mBaseHeight = copy.mBaseHeight;
878            mViewportWidth = copy.mViewportWidth;
879            mViewportHeight = copy.mViewportHeight;
880            mOpticalInsets = copy.mOpticalInsets;
881            mChangingConfigurations = copy.mChangingConfigurations;
882            mRootAlpha = copy.mRootAlpha;
883            mRootName = copy.mRootName;
884            if (copy.mRootName != null) {
885                mVGTargetsMap.put(copy.mRootName, this);
886            }
887        }
888
889        public boolean canApplyTheme() {
890            // If one of the paths can apply theme, then return true;
891            return recursiveCanApplyTheme(mRootGroup);
892        }
893
894        private boolean recursiveCanApplyTheme(VGroup currentGroup) {
895            // We can do a tree traverse here, if there is one path return true,
896            // then we return true for the whole tree.
897            final ArrayList<Object> children = currentGroup.mChildren;
898
899            for (int i = 0; i < children.size(); i++) {
900                Object child = children.get(i);
901                if (child instanceof VGroup) {
902                    VGroup childGroup = (VGroup) child;
903                    if (childGroup.canApplyTheme()
904                            || recursiveCanApplyTheme(childGroup)) {
905                        return true;
906                    }
907                } else if (child instanceof VPath) {
908                    VPath childPath = (VPath) child;
909                    if (childPath.canApplyTheme()) {
910                        return true;
911                    }
912                }
913            }
914            return false;
915        }
916
917        public void applyTheme(Theme t) {
918            // Apply theme to every path of the tree.
919            recursiveApplyTheme(mRootGroup, t);
920        }
921
922        private void recursiveApplyTheme(VGroup currentGroup, Theme t) {
923            // We can do a tree traverse here, apply theme to all paths which
924            // can apply theme.
925            final ArrayList<Object> children = currentGroup.mChildren;
926            for (int i = 0; i < children.size(); i++) {
927                Object child = children.get(i);
928                if (child instanceof VGroup) {
929                    VGroup childGroup = (VGroup) child;
930                    if (childGroup.canApplyTheme()) {
931                        childGroup.applyTheme(t);
932                    }
933                    recursiveApplyTheme(childGroup, t);
934                } else if (child instanceof VPath) {
935                    VPath childPath = (VPath) child;
936                    if (childPath.canApplyTheme()) {
937                        childPath.applyTheme(t);
938                    }
939                }
940            }
941        }
942
943        private void drawGroupTree(VGroup currentGroup, Matrix currentMatrix,
944                Canvas canvas, int w, int h, ColorFilter filter) {
945            // Calculate current group's matrix by preConcat the parent's and
946            // and the current one on the top of the stack.
947            // Basically the Mfinal = Mviewport * M0 * M1 * M2;
948            // Mi the local matrix at level i of the group tree.
949            currentGroup.mStackedMatrix.set(currentMatrix);
950            currentGroup.mStackedMatrix.preConcat(currentGroup.mLocalMatrix);
951
952            // Save the current clip information, which is local to this group.
953            canvas.save();
954            // Draw the group tree in the same order as the XML file.
955            for (int i = 0; i < currentGroup.mChildren.size(); i++) {
956                Object child = currentGroup.mChildren.get(i);
957                if (child instanceof VGroup) {
958                    VGroup childGroup = (VGroup) child;
959                    drawGroupTree(childGroup, currentGroup.mStackedMatrix,
960                            canvas, w, h, filter);
961                } else if (child instanceof VPath) {
962                    VPath childPath = (VPath) child;
963                    drawPath(currentGroup, childPath, canvas, w, h, filter);
964                }
965            }
966            canvas.restore();
967        }
968
969        public void draw(Canvas canvas, int w, int h, ColorFilter filter) {
970            // Travese the tree in pre-order to draw.
971            drawGroupTree(mRootGroup, Matrix.IDENTITY_MATRIX, canvas, w, h, filter);
972        }
973
974        private void drawPath(VGroup vGroup, VPath vPath, Canvas canvas, int w, int h,
975                ColorFilter filter) {
976            final float scaleX = w / mViewportWidth;
977            final float scaleY = h / mViewportHeight;
978            final float minScale = Math.min(scaleX, scaleY);
979            final Matrix groupStackedMatrix = vGroup.mStackedMatrix;
980
981            mFinalPathMatrix.set(groupStackedMatrix);
982            mFinalPathMatrix.postScale(scaleX, scaleY);
983
984            final float matrixScale = getMatrixScale(groupStackedMatrix);
985            if (matrixScale == 0) {
986                // When either x or y is scaled to 0, we don't need to draw anything.
987                return;
988            }
989            vPath.toPath(mPath);
990            final Path path = mPath;
991
992            mRenderPath.reset();
993
994            if (vPath.isClipPath()) {
995                mRenderPath.addPath(path, mFinalPathMatrix);
996                canvas.clipPath(mRenderPath);
997            } else {
998                VFullPath fullPath = (VFullPath) vPath;
999                if (fullPath.mTrimPathStart != 0.0f || fullPath.mTrimPathEnd != 1.0f) {
1000                    float start = (fullPath.mTrimPathStart + fullPath.mTrimPathOffset) % 1.0f;
1001                    float end = (fullPath.mTrimPathEnd + fullPath.mTrimPathOffset) % 1.0f;
1002
1003                    if (mPathMeasure == null) {
1004                        mPathMeasure = new PathMeasure();
1005                    }
1006                    mPathMeasure.setPath(mPath, false);
1007
1008                    float len = mPathMeasure.getLength();
1009                    start = start * len;
1010                    end = end * len;
1011                    path.reset();
1012                    if (start > end) {
1013                        mPathMeasure.getSegment(start, len, path, true);
1014                        mPathMeasure.getSegment(0f, end, path, true);
1015                    } else {
1016                        mPathMeasure.getSegment(start, end, path, true);
1017                    }
1018                    path.rLineTo(0, 0); // fix bug in measure
1019                }
1020                mRenderPath.addPath(path, mFinalPathMatrix);
1021
1022                if (fullPath.mFillColor != Color.TRANSPARENT) {
1023                    if (mFillPaint == null) {
1024                        mFillPaint = new Paint();
1025                        mFillPaint.setStyle(Paint.Style.FILL);
1026                        mFillPaint.setAntiAlias(true);
1027                    }
1028
1029                    final Paint fillPaint = mFillPaint;
1030                    fillPaint.setColor(applyAlpha(fullPath.mFillColor, fullPath.mFillAlpha));
1031                    fillPaint.setColorFilter(filter);
1032                    canvas.drawPath(mRenderPath, fillPaint);
1033                }
1034
1035                if (fullPath.mStrokeColor != Color.TRANSPARENT) {
1036                    if (mStrokePaint == null) {
1037                        mStrokePaint = new Paint();
1038                        mStrokePaint.setStyle(Paint.Style.STROKE);
1039                        mStrokePaint.setAntiAlias(true);
1040                    }
1041
1042                    final Paint strokePaint = mStrokePaint;
1043                    if (fullPath.mStrokeLineJoin != null) {
1044                        strokePaint.setStrokeJoin(fullPath.mStrokeLineJoin);
1045                    }
1046
1047                    if (fullPath.mStrokeLineCap != null) {
1048                        strokePaint.setStrokeCap(fullPath.mStrokeLineCap);
1049                    }
1050
1051                    strokePaint.setStrokeMiter(fullPath.mStrokeMiterlimit);
1052                    strokePaint.setColor(applyAlpha(fullPath.mStrokeColor, fullPath.mStrokeAlpha));
1053                    strokePaint.setColorFilter(filter);
1054                    final float finalStrokeScale = minScale * matrixScale;
1055                    strokePaint.setStrokeWidth(fullPath.mStrokeWidth * finalStrokeScale);
1056                    canvas.drawPath(mRenderPath, strokePaint);
1057                }
1058            }
1059        }
1060
1061        private float getMatrixScale(Matrix groupStackedMatrix) {
1062            // Given unit vectors A = (0, 1) and B = (1, 0).
1063            // After matrix mapping, we got A' and B'. Let theta = the angel b/t A' and B'.
1064            // Therefore, the final scale we want is min(|A'| * sin(theta), |B'| * sin(theta)),
1065            // which is (|A'| * |B'| * sin(theta)) / max (|A'|, |B'|);
1066            // If  max (|A'|, |B'|) = 0, that means either x or y has a scale of 0.
1067            //
1068            // For non-skew case, which is most of the cases, matrix scale is computing exactly the
1069            // scale on x and y axis, and take the minimal of these two.
1070            // For skew case, an unit square will mapped to a parallelogram. And this function will
1071            // return the minimal height of the 2 bases.
1072            float[] unitVectors = new float[] {0, 1, 1, 0};
1073            groupStackedMatrix.mapVectors(unitVectors);
1074            float scaleX = MathUtils.mag(unitVectors[0], unitVectors[1]);
1075            float scaleY = MathUtils.mag(unitVectors[2], unitVectors[3]);
1076            float crossProduct = MathUtils.cross(unitVectors[0], unitVectors[1],
1077                    unitVectors[2], unitVectors[3]);
1078            float maxScale = MathUtils.max(scaleX, scaleY);
1079
1080            float matrixScale = 0;
1081            if (maxScale > 0) {
1082                matrixScale = MathUtils.abs(crossProduct) / maxScale;
1083            }
1084            if (DBG_VECTOR_DRAWABLE) {
1085                Log.d(LOGTAG, "Scale x " + scaleX + " y " + scaleY + " final " + matrixScale);
1086            }
1087            return matrixScale;
1088        }
1089    }
1090
1091    private static class VGroup {
1092        // mStackedMatrix is only used temporarily when drawing, it combines all
1093        // the parents' local matrices with the current one.
1094        private final Matrix mStackedMatrix = new Matrix();
1095
1096        /////////////////////////////////////////////////////
1097        // Variables below need to be copied (deep copy if applicable) for mutation.
1098        final ArrayList<Object> mChildren = new ArrayList<Object>();
1099
1100        private float mRotate = 0;
1101        private float mPivotX = 0;
1102        private float mPivotY = 0;
1103        private float mScaleX = 1;
1104        private float mScaleY = 1;
1105        private float mTranslateX = 0;
1106        private float mTranslateY = 0;
1107
1108        // mLocalMatrix is updated based on the update of transformation information,
1109        // either parsed from the XML or by animation.
1110        private final Matrix mLocalMatrix = new Matrix();
1111        private int mChangingConfigurations;
1112        private int[] mThemeAttrs;
1113        private String mGroupName = null;
1114
1115        public VGroup(VGroup copy, ArrayMap<String, Object> targetsMap) {
1116            mRotate = copy.mRotate;
1117            mPivotX = copy.mPivotX;
1118            mPivotY = copy.mPivotY;
1119            mScaleX = copy.mScaleX;
1120            mScaleY = copy.mScaleY;
1121            mTranslateX = copy.mTranslateX;
1122            mTranslateY = copy.mTranslateY;
1123            mThemeAttrs = copy.mThemeAttrs;
1124            mGroupName = copy.mGroupName;
1125            mChangingConfigurations = copy.mChangingConfigurations;
1126            if (mGroupName != null) {
1127                targetsMap.put(mGroupName, this);
1128            }
1129
1130            mLocalMatrix.set(copy.mLocalMatrix);
1131
1132            final ArrayList<Object> children = copy.mChildren;
1133            for (int i = 0; i < children.size(); i++) {
1134                Object copyChild = children.get(i);
1135                if (copyChild instanceof VGroup) {
1136                    VGroup copyGroup = (VGroup) copyChild;
1137                    mChildren.add(new VGroup(copyGroup, targetsMap));
1138                } else {
1139                    VPath newPath = null;
1140                    if (copyChild instanceof VFullPath) {
1141                        newPath = new VFullPath((VFullPath) copyChild);
1142                    } else if (copyChild instanceof VClipPath) {
1143                        newPath = new VClipPath((VClipPath) copyChild);
1144                    } else {
1145                        throw new IllegalStateException("Unknown object in the tree!");
1146                    }
1147                    mChildren.add(newPath);
1148                    if (newPath.mPathName != null) {
1149                        targetsMap.put(newPath.mPathName, newPath);
1150                    }
1151                }
1152            }
1153        }
1154
1155        public VGroup() {
1156        }
1157
1158        public String getGroupName() {
1159            return mGroupName;
1160        }
1161
1162        public Matrix getLocalMatrix() {
1163            return mLocalMatrix;
1164        }
1165
1166        public void inflate(Resources res, AttributeSet attrs, Theme theme) {
1167            final TypedArray a = obtainAttributes(res, theme, attrs,
1168                    R.styleable.VectorDrawableGroup);
1169            updateStateFromTypedArray(a);
1170            a.recycle();
1171        }
1172
1173        private void updateStateFromTypedArray(TypedArray a) {
1174            // Account for any configuration changes.
1175            mChangingConfigurations |= a.getChangingConfigurations();
1176
1177            // Extract the theme attributes, if any.
1178            mThemeAttrs = a.extractThemeAttrs();
1179
1180            mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate);
1181            mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX);
1182            mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY);
1183            mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX);
1184            mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY);
1185            mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX);
1186            mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY);
1187
1188            final String groupName = a.getString(R.styleable.VectorDrawableGroup_name);
1189            if (groupName != null) {
1190                mGroupName = groupName;
1191            }
1192
1193            updateLocalMatrix();
1194        }
1195
1196        public boolean canApplyTheme() {
1197            return mThemeAttrs != null;
1198        }
1199
1200        public void applyTheme(Theme t) {
1201            if (mThemeAttrs == null) {
1202                return;
1203            }
1204
1205            final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawableGroup);
1206            updateStateFromTypedArray(a);
1207            a.recycle();
1208        }
1209
1210        private void updateLocalMatrix() {
1211            // The order we apply is the same as the
1212            // RenderNode.cpp::applyViewPropertyTransforms().
1213            mLocalMatrix.reset();
1214            mLocalMatrix.postTranslate(-mPivotX, -mPivotY);
1215            mLocalMatrix.postScale(mScaleX, mScaleY);
1216            mLocalMatrix.postRotate(mRotate, 0, 0);
1217            mLocalMatrix.postTranslate(mTranslateX + mPivotX, mTranslateY + mPivotY);
1218        }
1219
1220        /* Setters and Getters, used by animator from AnimatedVectorDrawable. */
1221        @SuppressWarnings("unused")
1222        public float getRotation() {
1223            return mRotate;
1224        }
1225
1226        @SuppressWarnings("unused")
1227        public void setRotation(float rotation) {
1228            if (rotation != mRotate) {
1229                mRotate = rotation;
1230                updateLocalMatrix();
1231            }
1232        }
1233
1234        @SuppressWarnings("unused")
1235        public float getPivotX() {
1236            return mPivotX;
1237        }
1238
1239        @SuppressWarnings("unused")
1240        public void setPivotX(float pivotX) {
1241            if (pivotX != mPivotX) {
1242                mPivotX = pivotX;
1243                updateLocalMatrix();
1244            }
1245        }
1246
1247        @SuppressWarnings("unused")
1248        public float getPivotY() {
1249            return mPivotY;
1250        }
1251
1252        @SuppressWarnings("unused")
1253        public void setPivotY(float pivotY) {
1254            if (pivotY != mPivotY) {
1255                mPivotY = pivotY;
1256                updateLocalMatrix();
1257            }
1258        }
1259
1260        @SuppressWarnings("unused")
1261        public float getScaleX() {
1262            return mScaleX;
1263        }
1264
1265        @SuppressWarnings("unused")
1266        public void setScaleX(float scaleX) {
1267            if (scaleX != mScaleX) {
1268                mScaleX = scaleX;
1269                updateLocalMatrix();
1270            }
1271        }
1272
1273        @SuppressWarnings("unused")
1274        public float getScaleY() {
1275            return mScaleY;
1276        }
1277
1278        @SuppressWarnings("unused")
1279        public void setScaleY(float scaleY) {
1280            if (scaleY != mScaleY) {
1281                mScaleY = scaleY;
1282                updateLocalMatrix();
1283            }
1284        }
1285
1286        @SuppressWarnings("unused")
1287        public float getTranslateX() {
1288            return mTranslateX;
1289        }
1290
1291        @SuppressWarnings("unused")
1292        public void setTranslateX(float translateX) {
1293            if (translateX != mTranslateX) {
1294                mTranslateX = translateX;
1295                updateLocalMatrix();
1296            }
1297        }
1298
1299        @SuppressWarnings("unused")
1300        public float getTranslateY() {
1301            return mTranslateY;
1302        }
1303
1304        @SuppressWarnings("unused")
1305        public void setTranslateY(float translateY) {
1306            if (translateY != mTranslateY) {
1307                mTranslateY = translateY;
1308                updateLocalMatrix();
1309            }
1310        }
1311    }
1312
1313    /**
1314     * Common Path information for clip path and normal path.
1315     */
1316    private static class VPath {
1317        protected PathParser.PathDataNode[] mNodes = null;
1318        String mPathName;
1319        int mChangingConfigurations;
1320
1321        public VPath() {
1322            // Empty constructor.
1323        }
1324
1325        public VPath(VPath copy) {
1326            mPathName = copy.mPathName;
1327            mChangingConfigurations = copy.mChangingConfigurations;
1328            mNodes = PathParser.deepCopyNodes(copy.mNodes);
1329        }
1330
1331        public void toPath(Path path) {
1332            path.reset();
1333            if (mNodes != null) {
1334                PathParser.PathDataNode.nodesToPath(mNodes, path);
1335            }
1336        }
1337
1338        public String getPathName() {
1339            return mPathName;
1340        }
1341
1342        public boolean canApplyTheme() {
1343            return false;
1344        }
1345
1346        public void applyTheme(Theme t) {
1347        }
1348
1349        public boolean isClipPath() {
1350            return false;
1351        }
1352
1353        /* Setters and Getters, used by animator from AnimatedVectorDrawable. */
1354        @SuppressWarnings("unused")
1355        public PathParser.PathDataNode[] getPathData() {
1356            return mNodes;
1357        }
1358
1359        @SuppressWarnings("unused")
1360        public void setPathData(PathParser.PathDataNode[] nodes) {
1361            if (!PathParser.canMorph(mNodes, nodes)) {
1362                // This should not happen in the middle of animation.
1363                mNodes = PathParser.deepCopyNodes(nodes);
1364            } else {
1365                PathParser.updateNodes(mNodes, nodes);
1366            }
1367        }
1368    }
1369
1370    /**
1371     * Clip path, which only has name and pathData.
1372     */
1373    private static class VClipPath extends VPath {
1374        public VClipPath() {
1375            // Empty constructor.
1376        }
1377
1378        public VClipPath(VClipPath copy) {
1379            super(copy);
1380        }
1381
1382        public void inflate(Resources r, AttributeSet attrs, Theme theme) {
1383            final TypedArray a = obtainAttributes(r, theme, attrs,
1384                    R.styleable.VectorDrawableClipPath);
1385            updateStateFromTypedArray(a);
1386            a.recycle();
1387        }
1388
1389        private void updateStateFromTypedArray(TypedArray a) {
1390            // Account for any configuration changes.
1391            mChangingConfigurations |= a.getChangingConfigurations();
1392
1393            final String pathName = a.getString(R.styleable.VectorDrawableClipPath_name);
1394            if (pathName != null) {
1395                mPathName = pathName;
1396            }
1397
1398            final String pathData = a.getString(R.styleable.VectorDrawableClipPath_pathData);
1399            if (pathData != null) {
1400                mNodes = PathParser.createNodesFromPathData(pathData);
1401            }
1402        }
1403
1404        @Override
1405        public boolean isClipPath() {
1406            return true;
1407        }
1408    }
1409
1410    /**
1411     * Normal path, which contains all the fill / paint information.
1412     */
1413    private static class VFullPath extends VPath {
1414        /////////////////////////////////////////////////////
1415        // Variables below need to be copied (deep copy if applicable) for mutation.
1416        private int[] mThemeAttrs;
1417
1418        int mStrokeColor = Color.TRANSPARENT;
1419        float mStrokeWidth = 0;
1420
1421        int mFillColor = Color.TRANSPARENT;
1422        float mStrokeAlpha = 1.0f;
1423        int mFillRule;
1424        float mFillAlpha = 1.0f;
1425        float mTrimPathStart = 0;
1426        float mTrimPathEnd = 1;
1427        float mTrimPathOffset = 0;
1428
1429        Paint.Cap mStrokeLineCap = Paint.Cap.BUTT;
1430        Paint.Join mStrokeLineJoin = Paint.Join.MITER;
1431        float mStrokeMiterlimit = 4;
1432
1433        public VFullPath() {
1434            // Empty constructor.
1435        }
1436
1437        public VFullPath(VFullPath copy) {
1438            super(copy);
1439            mThemeAttrs = copy.mThemeAttrs;
1440
1441            mStrokeColor = copy.mStrokeColor;
1442            mStrokeWidth = copy.mStrokeWidth;
1443            mStrokeAlpha = copy.mStrokeAlpha;
1444            mFillColor = copy.mFillColor;
1445            mFillRule = copy.mFillRule;
1446            mFillAlpha = copy.mFillAlpha;
1447            mTrimPathStart = copy.mTrimPathStart;
1448            mTrimPathEnd = copy.mTrimPathEnd;
1449            mTrimPathOffset = copy.mTrimPathOffset;
1450
1451            mStrokeLineCap = copy.mStrokeLineCap;
1452            mStrokeLineJoin = copy.mStrokeLineJoin;
1453            mStrokeMiterlimit = copy.mStrokeMiterlimit;
1454        }
1455
1456        private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) {
1457            switch (id) {
1458                case LINECAP_BUTT:
1459                    return Paint.Cap.BUTT;
1460                case LINECAP_ROUND:
1461                    return Paint.Cap.ROUND;
1462                case LINECAP_SQUARE:
1463                    return Paint.Cap.SQUARE;
1464                default:
1465                    return defValue;
1466            }
1467        }
1468
1469        private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) {
1470            switch (id) {
1471                case LINEJOIN_MITER:
1472                    return Paint.Join.MITER;
1473                case LINEJOIN_ROUND:
1474                    return Paint.Join.ROUND;
1475                case LINEJOIN_BEVEL:
1476                    return Paint.Join.BEVEL;
1477                default:
1478                    return defValue;
1479            }
1480        }
1481
1482        @Override
1483        public boolean canApplyTheme() {
1484            return mThemeAttrs != null;
1485        }
1486
1487        public void inflate(Resources r, AttributeSet attrs, Theme theme) {
1488            final TypedArray a = obtainAttributes(r, theme, attrs,
1489                    R.styleable.VectorDrawablePath);
1490            updateStateFromTypedArray(a);
1491            a.recycle();
1492        }
1493
1494        private void updateStateFromTypedArray(TypedArray a) {
1495            // Account for any configuration changes.
1496            mChangingConfigurations |= a.getChangingConfigurations();
1497
1498            // Extract the theme attributes, if any.
1499            mThemeAttrs = a.extractThemeAttrs();
1500
1501            final String pathName = a.getString(R.styleable.VectorDrawablePath_name);
1502            if (pathName != null) {
1503                mPathName = pathName;
1504            }
1505
1506            final String pathData = a.getString(R.styleable.VectorDrawablePath_pathData);
1507            if (pathData != null) {
1508                mNodes = PathParser.createNodesFromPathData(pathData);
1509            }
1510
1511            mFillColor = a.getColor(R.styleable.VectorDrawablePath_fillColor,
1512                    mFillColor);
1513            mFillAlpha = a.getFloat(R.styleable.VectorDrawablePath_fillAlpha,
1514                    mFillAlpha);
1515            mStrokeLineCap = getStrokeLineCap(a.getInt(
1516                    R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap);
1517            mStrokeLineJoin = getStrokeLineJoin(a.getInt(
1518                    R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin);
1519            mStrokeMiterlimit = a.getFloat(
1520                    R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit);
1521            mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_strokeColor,
1522                    mStrokeColor);
1523            mStrokeAlpha = a.getFloat(R.styleable.VectorDrawablePath_strokeAlpha,
1524                    mStrokeAlpha);
1525            mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth,
1526                    mStrokeWidth);
1527            mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd,
1528                    mTrimPathEnd);
1529            mTrimPathOffset = a.getFloat(
1530                    R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset);
1531            mTrimPathStart = a.getFloat(
1532                    R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart);
1533        }
1534
1535        @Override
1536        public void applyTheme(Theme t) {
1537            if (mThemeAttrs == null) {
1538                return;
1539            }
1540
1541            final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawablePath);
1542            updateStateFromTypedArray(a);
1543            a.recycle();
1544        }
1545
1546        /* Setters and Getters, used by animator from AnimatedVectorDrawable. */
1547        @SuppressWarnings("unused")
1548        int getStrokeColor() {
1549            return mStrokeColor;
1550        }
1551
1552        @SuppressWarnings("unused")
1553        void setStrokeColor(int strokeColor) {
1554            mStrokeColor = strokeColor;
1555        }
1556
1557        @SuppressWarnings("unused")
1558        float getStrokeWidth() {
1559            return mStrokeWidth;
1560        }
1561
1562        @SuppressWarnings("unused")
1563        void setStrokeWidth(float strokeWidth) {
1564            mStrokeWidth = strokeWidth;
1565        }
1566
1567        @SuppressWarnings("unused")
1568        float getStrokeAlpha() {
1569            return mStrokeAlpha;
1570        }
1571
1572        @SuppressWarnings("unused")
1573        void setStrokeAlpha(float strokeAlpha) {
1574            mStrokeAlpha = strokeAlpha;
1575        }
1576
1577        @SuppressWarnings("unused")
1578        int getFillColor() {
1579            return mFillColor;
1580        }
1581
1582        @SuppressWarnings("unused")
1583        void setFillColor(int fillColor) {
1584            mFillColor = fillColor;
1585        }
1586
1587        @SuppressWarnings("unused")
1588        float getFillAlpha() {
1589            return mFillAlpha;
1590        }
1591
1592        @SuppressWarnings("unused")
1593        void setFillAlpha(float fillAlpha) {
1594            mFillAlpha = fillAlpha;
1595        }
1596
1597        @SuppressWarnings("unused")
1598        float getTrimPathStart() {
1599            return mTrimPathStart;
1600        }
1601
1602        @SuppressWarnings("unused")
1603        void setTrimPathStart(float trimPathStart) {
1604            mTrimPathStart = trimPathStart;
1605        }
1606
1607        @SuppressWarnings("unused")
1608        float getTrimPathEnd() {
1609            return mTrimPathEnd;
1610        }
1611
1612        @SuppressWarnings("unused")
1613        void setTrimPathEnd(float trimPathEnd) {
1614            mTrimPathEnd = trimPathEnd;
1615        }
1616
1617        @SuppressWarnings("unused")
1618        float getTrimPathOffset() {
1619            return mTrimPathOffset;
1620        }
1621
1622        @SuppressWarnings("unused")
1623        void setTrimPathOffset(float trimPathOffset) {
1624            mTrimPathOffset = trimPathOffset;
1625        }
1626    }
1627}
1628