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