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