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><vector></code> element. 91 * <p/> 92 * The VectorDrawableCompat has the following elements: 93 * <p/> 94 * <dl> 95 * <dt><code><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><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><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><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