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