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