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