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