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