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