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