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