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