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