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