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