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