VectorDrawable.java revision 4b17118aca1e67963254ab83504b0753a3eac7ce
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.content.res.ColorStateList; 18import android.content.res.Resources; 19import android.content.res.Resources.Theme; 20import android.content.res.TypedArray; 21import android.graphics.Canvas; 22import android.graphics.Color; 23import android.graphics.ColorFilter; 24import android.graphics.Matrix; 25import android.graphics.Paint; 26import android.graphics.Path; 27import android.graphics.PathMeasure; 28import android.graphics.PixelFormat; 29import android.graphics.PorterDuffColorFilter; 30import android.graphics.Rect; 31import android.graphics.Region; 32import android.graphics.PorterDuff.Mode; 33import android.util.ArrayMap; 34import android.util.AttributeSet; 35import android.util.Log; 36import android.util.Xml; 37 38import com.android.internal.R; 39 40import org.xmlpull.v1.XmlPullParser; 41import org.xmlpull.v1.XmlPullParserException; 42import org.xmlpull.v1.XmlPullParserFactory; 43 44import java.io.IOException; 45import java.util.ArrayList; 46import java.util.Arrays; 47import java.util.Stack; 48 49/** 50 * This lets you create a drawable based on an XML vector graphic It can be 51 * defined in an XML file with the <code><vector></code> element. 52 * <p/> 53 * The vector drawable has the following elements: 54 * <p/> 55 * <dl> 56 * <dt><code><vector></code></dt> 57 * <dd>Used to defined a vector drawable</dd> 58 * <dt><code><size></code></dt> 59 * <dd>Used to defined the intrinsic Width Height size of the drawable using 60 * <code>android:width</code> and <code>android:height</code></dd> 61 * <dt><code><viewport></code></dt> 62 * <dd>Used to defined the size of the virtual canvas the paths are drawn on. 63 * The size is defined using the attributes <code>android:viewportHeight</code> 64 * <code>android:viewportWidth</code></dd> 65 * <dt><code><group></code></dt> 66 * <dd>Defines a group of paths or subgroups, plus transformation information. 67 * The transformations are defined in the same coordinates as the viewport. 68 * And the transformations are applied in the order of scale, rotate then translate. </dd> 69 * <dt><code>android:rotation</code> 70 * <dd>The degrees of rotation of the group.</dd></dt> 71 * <dt><code>android:pivotX</code> 72 * <dd>The X coordinate of the pivot for the scale and rotation of the group</dd></dt> 73 * <dt><code>android:pivotY</code> 74 * <dd>The Y coordinate of the pivot for the scale and rotation of the group</dd></dt> 75 * <dt><code>android:scaleX</code> 76 * <dd>The amount of scale on the X Coordinate</dd></dt> 77 * <dt><code>android:scaleY</code> 78 * <dd>The amount of scale on the Y coordinate</dd></dt> 79 * <dt><code>android:translateX</code> 80 * <dd>The amount of translation on the X coordinate</dd></dt> 81 * <dt><code>android:translateY</code> 82 * <dd>The amount of translation on the Y coordinate</dd></dt> 83 * <dt><code><path></code></dt> 84 * <dd>Defines paths to be drawn. 85 * <dl> 86 * <dt><code>android:name</code> 87 * <dd>Defines the name of the path.</dd></dt> 88 * <dt><code>android:pathData</code> 89 * <dd>Defines path string. This is using exactly same format as "d" attribute 90 * in the SVG's path data</dd></dt> 91 * <dt><code>android:fill</code> 92 * <dd>Defines the color to fill the path (none if not present).</dd></dt> 93 * <dt><code>android:stroke</code> 94 * <dd>Defines the color to draw the path outline (none if not present).</dd> 95 * </dt> 96 * <dt><code>android:strokeWidth</code> 97 * <dd>The width a path stroke</dd></dt> 98 * <dt><code>android:strokeOpacity</code> 99 * <dd>The opacity of a path stroke</dd></dt> 100 * <dt><code>android:fillOpacity</code> 101 * <dd>The opacity to fill the path with</dd></dt> 102 * <dt><code>android:trimPathStart</code> 103 * <dd>The fraction of the path to trim from the start from 0 to 1</dd></dt> 104 * <dt><code>android:trimPathEnd</code> 105 * <dd>The fraction of the path to trim from the end from 0 to 1</dd></dt> 106 * <dt><code>android:trimPathOffset</code> 107 * <dd>Shift trim region (allows showed region to include the start and end) 108 * from 0 to 1</dd></dt> 109 * <dt><code>android:clipToPath</code> 110 * <dd>Path will set the clip path</dd></dt> 111 * <dt><code>android:strokeLineCap</code> 112 * <dd>Sets the linecap for a stroked path: butt, round, square</dd></dt> 113 * <dt><code>android:strokeLineJoin</code> 114 * <dd>Sets the lineJoin for a stroked path: miter,round,bevel</dd></dt> 115 * <dt><code>android:strokeMiterLimit</code> 116 * <dd>Sets the Miter limit for a stroked path</dd></dt> 117 * </dl> 118 * </dd> 119 */ 120public class VectorDrawable extends Drawable { 121 private static final String LOGTAG = VectorDrawable.class.getSimpleName(); 122 123 private static final String SHAPE_SIZE = "size"; 124 private static final String SHAPE_VIEWPORT = "viewport"; 125 private static final String SHAPE_GROUP = "group"; 126 private static final String SHAPE_PATH = "path"; 127 private static final String SHAPE_VECTOR = "vector"; 128 129 private static final int LINECAP_BUTT = 0; 130 private static final int LINECAP_ROUND = 1; 131 private static final int LINECAP_SQUARE = 2; 132 133 private static final int LINEJOIN_MITER = 0; 134 private static final int LINEJOIN_ROUND = 1; 135 private static final int LINEJOIN_BEVEL = 2; 136 137 private static final boolean DBG_VECTOR_DRAWABLE = false; 138 139 private final VectorDrawableState mVectorState; 140 141 private final ArrayMap<String, Object> mVGTargetsMap = new ArrayMap<String, Object>(); 142 143 private PorterDuffColorFilter mTintFilter; 144 145 public VectorDrawable() { 146 mVectorState = new VectorDrawableState(null); 147 } 148 149 private VectorDrawable(VectorDrawableState state, Resources res, Theme theme) { 150 mVectorState = new VectorDrawableState(state); 151 152 if (theme != null && canApplyTheme()) { 153 applyTheme(theme); 154 } 155 } 156 157 Object getTargetByName(String name) { 158 return mVGTargetsMap.get(name); 159 } 160 161 @Override 162 public ConstantState getConstantState() { 163 return mVectorState; 164 } 165 166 @Override 167 public void draw(Canvas canvas) { 168 final int saveCount = canvas.save(); 169 final Rect bounds = getBounds(); 170 canvas.translate(bounds.left, bounds.top); 171 mVectorState.mVPathRenderer.draw(canvas, bounds.width(), bounds.height()); 172 canvas.restoreToCount(saveCount); 173 } 174 175 @Override 176 public int getAlpha() { 177 return mVectorState.mVPathRenderer.getRootAlpha(); 178 } 179 180 @Override 181 public void setAlpha(int alpha) { 182 if (mVectorState.mVPathRenderer.getRootAlpha() != alpha) { 183 mVectorState.mVPathRenderer.setRootAlpha(alpha); 184 invalidateSelf(); 185 } 186 } 187 188 @Override 189 public void setColorFilter(ColorFilter colorFilter) { 190 final VectorDrawableState state = mVectorState; 191 if (colorFilter != null) { 192 // Color filter overrides tint. 193 mTintFilter = null; 194 } else if (state.mTint != null && state.mTintMode != null) { 195 // Restore the tint filter, if we need one. 196 final int color = state.mTint.getColorForState(getState(), Color.TRANSPARENT); 197 mTintFilter = new PorterDuffColorFilter(color, state.mTintMode); 198 colorFilter = mTintFilter; 199 } 200 201 state.mVPathRenderer.setColorFilter(colorFilter); 202 invalidateSelf(); 203 } 204 205 @Override 206 public void setTint(ColorStateList tint, Mode tintMode) { 207 final VectorDrawableState state = mVectorState; 208 if (state.mTint != tint || state.mTintMode != tintMode) { 209 state.mTint = tint; 210 state.mTintMode = tintMode; 211 212 mTintFilter = updateTintFilter(mTintFilter, tint, tintMode); 213 mVectorState.mVPathRenderer.setColorFilter(mTintFilter); 214 invalidateSelf(); 215 } 216 } 217 218 @Override 219 protected boolean onStateChange(int[] stateSet) { 220 final VectorDrawableState state = mVectorState; 221 if (state.mTint != null && state.mTintMode != null) { 222 mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode); 223 return true; 224 } 225 return false; 226 } 227 228 @Override 229 public int getOpacity() { 230 return PixelFormat.TRANSLUCENT; 231 } 232 233 /** 234 * Sets padding for this shape, defined by a Rect object. Define the padding 235 * in the Rect object as: left, top, right, bottom. 236 */ 237 public void setPadding(Rect padding) { 238 setPadding(padding.left, padding.top, padding.right, padding.bottom); 239 } 240 241 /** 242 * Sets padding for the shape. 243 * 244 * @param left padding for the left side (in pixels) 245 * @param top padding for the top (in pixels) 246 * @param right padding for the right side (in pixels) 247 * @param bottom padding for the bottom (in pixels) 248 */ 249 public void setPadding(int left, int top, int right, int bottom) { 250 if ((left | top | right | bottom) == 0) { 251 mVectorState.mPadding = null; 252 } else { 253 if (mVectorState.mPadding == null) { 254 mVectorState.mPadding = new Rect(); 255 } 256 mVectorState.mPadding.set(left, top, right, bottom); 257 } 258 invalidateSelf(); 259 } 260 261 @Override 262 public int getIntrinsicWidth() { 263 return (int) mVectorState.mVPathRenderer.mBaseWidth; 264 } 265 266 @Override 267 public int getIntrinsicHeight() { 268 return (int) mVectorState.mVPathRenderer.mBaseHeight; 269 } 270 271 @Override 272 public boolean getPadding(Rect padding) { 273 if (mVectorState.mPadding != null) { 274 padding.set(mVectorState.mPadding); 275 return true; 276 } else { 277 return super.getPadding(padding); 278 } 279 } 280 281 @Override 282 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) 283 throws XmlPullParserException, IOException { 284 final VPathRenderer p = inflateInternal(res, parser, attrs, theme); 285 setPathRenderer(p); 286 } 287 288 @Override 289 public boolean canApplyTheme() { 290 return super.canApplyTheme() || mVectorState != null && mVectorState.canApplyTheme(); 291 } 292 293 @Override 294 public void applyTheme(Theme t) { 295 super.applyTheme(t); 296 297 final VectorDrawableState state = mVectorState; 298 final VPathRenderer path = state.mVPathRenderer; 299 if (path != null && path.canApplyTheme()) { 300 path.applyTheme(t); 301 } 302 } 303 304 /** @hide */ 305 public static VectorDrawable create(Resources resources, int rid) { 306 try { 307 final XmlPullParser xpp = resources.getXml(rid); 308 final AttributeSet attrs = Xml.asAttributeSet(xpp); 309 final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 310 factory.setNamespaceAware(true); 311 312 final VectorDrawable drawable = new VectorDrawable(); 313 drawable.inflate(resources, xpp, attrs); 314 315 return drawable; 316 } catch (XmlPullParserException e) { 317 Log.e(LOGTAG, "parser error", e); 318 } catch (IOException e) { 319 Log.e(LOGTAG, "parser error", e); 320 } 321 return null; 322 } 323 324 private static int applyAlpha(int color, float alpha) { 325 int alphaBytes = Color.alpha(color); 326 color &= 0x00FFFFFF; 327 color |= ((int) (alphaBytes * alpha)) << 24; 328 return color; 329 } 330 331 private VPathRenderer inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs, 332 Theme theme) throws XmlPullParserException, IOException { 333 final VPathRenderer pathRenderer = new VPathRenderer(); 334 335 boolean noSizeTag = true; 336 boolean noViewportTag = true; 337 boolean noGroupTag = true; 338 boolean noPathTag = true; 339 340 // Use a stack to help to build the group tree. 341 // The top of the stack is always the current group. 342 final Stack<VGroup> groupStack = new Stack<VGroup>(); 343 groupStack.push(pathRenderer.mRootGroup); 344 345 int eventType = parser.getEventType(); 346 while (eventType != XmlPullParser.END_DOCUMENT) { 347 if (eventType == XmlPullParser.START_TAG) { 348 final String tagName = parser.getName(); 349 final VGroup currentGroup = groupStack.peek(); 350 351 if (SHAPE_PATH.equals(tagName)) { 352 final VPath path = new VPath(); 353 path.inflate(res, attrs, theme); 354 currentGroup.add(path); 355 if (path.getPathName() != null) { 356 mVGTargetsMap.put(path.getPathName(), path); 357 } 358 noPathTag = false; 359 } else if (SHAPE_SIZE.equals(tagName)) { 360 pathRenderer.parseSize(res, attrs); 361 noSizeTag = false; 362 } else if (SHAPE_VIEWPORT.equals(tagName)) { 363 pathRenderer.parseViewport(res, attrs); 364 noViewportTag = false; 365 } else if (SHAPE_GROUP.equals(tagName)) { 366 VGroup newChildGroup = new VGroup(); 367 newChildGroup.inflate(res, attrs, theme); 368 currentGroup.mChildGroupList.add(newChildGroup); 369 groupStack.push(newChildGroup); 370 if (newChildGroup.getGroupName() != null) { 371 mVGTargetsMap.put(newChildGroup.getGroupName(), newChildGroup); 372 } 373 noGroupTag = false; 374 } 375 } else if (eventType == XmlPullParser.END_TAG) { 376 final String tagName = parser.getName(); 377 if (SHAPE_GROUP.equals(tagName)) { 378 groupStack.pop(); 379 } 380 } 381 eventType = parser.next(); 382 } 383 384 // Print the tree out for debug. 385 if (DBG_VECTOR_DRAWABLE) { 386 printGroupTree(pathRenderer.mRootGroup, 0); 387 } 388 389 if (noSizeTag || noViewportTag || noPathTag) { 390 final StringBuffer tag = new StringBuffer(); 391 392 if (noSizeTag) { 393 tag.append(SHAPE_SIZE); 394 } 395 396 if (noViewportTag) { 397 if (tag.length() > 0) { 398 tag.append(" & "); 399 } 400 tag.append(SHAPE_SIZE); 401 } 402 403 if (noPathTag) { 404 if (tag.length() > 0) { 405 tag.append(" or "); 406 } 407 tag.append(SHAPE_PATH); 408 } 409 410 throw new XmlPullParserException("no " + tag + " defined"); 411 } 412 413 return pathRenderer; 414 } 415 416 private void printGroupTree(VGroup currentGroup, int level) { 417 String indent = ""; 418 for (int i = 0 ; i < level ; i++) { 419 indent += " "; 420 } 421 // Print the current node 422 Log.v(LOGTAG, indent + "current group is :" + currentGroup.getGroupName() 423 + " rotation is " + currentGroup.mRotate); 424 Log.v(LOGTAG, indent + "matrix is :" + currentGroup.getLocalMatrix().toString()); 425 // Then print all the children 426 for (int i = 0 ; i < currentGroup.mChildGroupList.size(); i++) { 427 printGroupTree(currentGroup.mChildGroupList.get(i), level + 1); 428 } 429 } 430 431 private void setPathRenderer(VPathRenderer pathRenderer) { 432 mVectorState.mVPathRenderer = pathRenderer; 433 } 434 435 private static class VectorDrawableState extends ConstantState { 436 int mChangingConfigurations; 437 VPathRenderer mVPathRenderer; 438 Rect mPadding; 439 ColorStateList mTint; 440 Mode mTintMode; 441 442 public VectorDrawableState(VectorDrawableState copy) { 443 if (copy != null) { 444 mChangingConfigurations = copy.mChangingConfigurations; 445 // TODO: Make sure the constant state are handled correctly. 446 mVPathRenderer = new VPathRenderer(copy.mVPathRenderer); 447 mPadding = new Rect(copy.mPadding); 448 mTint = copy.mTint; 449 mTintMode = copy.mTintMode; 450 } 451 } 452 453 @Override 454 public Drawable newDrawable() { 455 return new VectorDrawable(this, null, null); 456 } 457 458 @Override 459 public Drawable newDrawable(Resources res) { 460 return new VectorDrawable(this, res, null); 461 } 462 463 @Override 464 public Drawable newDrawable(Resources res, Theme theme) { 465 return new VectorDrawable(this, res, theme); 466 } 467 468 @Override 469 public int getChangingConfigurations() { 470 return mChangingConfigurations; 471 } 472 } 473 474 private static class VPathRenderer { 475 /* Right now the internal data structure is organized as a tree. 476 * Each node can be a group node, or a path. 477 * A group node can have groups or paths as children, but a path node has 478 * no children. 479 * One example can be: 480 * Root Group 481 * / | \ 482 * Group Path Group 483 * / \ | 484 * Path Path Path 485 * 486 */ 487 private final VGroup mRootGroup; 488 489 private final Path mPath = new Path(); 490 private final Path mRenderPath = new Path(); 491 private static final Matrix IDENTITY_MATRIX = new Matrix(); 492 493 private Paint mStrokePaint; 494 private Paint mFillPaint; 495 private ColorFilter mColorFilter; 496 private PathMeasure mPathMeasure; 497 498 private float mBaseWidth = 0; 499 private float mBaseHeight = 0; 500 private float mViewportWidth = 0; 501 private float mViewportHeight = 0; 502 private int mRootAlpha = 0xFF; 503 504 private final Matrix mFinalPathMatrix = new Matrix(); 505 506 public VPathRenderer() { 507 mRootGroup = new VGroup(); 508 } 509 510 public void setRootAlpha(int alpha) { 511 mRootAlpha = alpha; 512 } 513 514 public int getRootAlpha() { 515 return mRootAlpha; 516 } 517 518 public VPathRenderer(VPathRenderer copy) { 519 mRootGroup = copy.mRootGroup; 520 mBaseWidth = copy.mBaseWidth; 521 mBaseHeight = copy.mBaseHeight; 522 mViewportWidth = copy.mViewportHeight; 523 mViewportHeight = copy.mViewportHeight; 524 } 525 526 public boolean canApplyTheme() { 527 // If one of the paths can apply theme, then return true; 528 return recursiveCanApplyTheme(mRootGroup); 529 } 530 531 private boolean recursiveCanApplyTheme(VGroup currentGroup) { 532 // We can do a tree traverse here, if there is one path return true, 533 // then we return true for the whole tree. 534 final ArrayList<VPath> paths = currentGroup.mPathList; 535 for (int j = paths.size() - 1; j >= 0; j--) { 536 final VPath path = paths.get(j); 537 if (path.canApplyTheme()) { 538 return true; 539 } 540 } 541 542 final ArrayList<VGroup> childGroups = currentGroup.mChildGroupList; 543 544 for (int i = 0; i < childGroups.size(); i++) { 545 VGroup childGroup = childGroups.get(i); 546 if (childGroup.canApplyTheme() 547 || recursiveCanApplyTheme(childGroup)) { 548 return true; 549 } 550 } 551 return false; 552 } 553 554 public void applyTheme(Theme t) { 555 // Apply theme to every path of the tree. 556 recursiveApplyTheme(mRootGroup, t); 557 } 558 559 private void recursiveApplyTheme(VGroup currentGroup, Theme t) { 560 // We can do a tree traverse here, apply theme to all paths which 561 // can apply theme. 562 final ArrayList<VPath> paths = currentGroup.mPathList; 563 for (int j = paths.size() - 1; j >= 0; j--) { 564 final VPath path = paths.get(j); 565 if (path.canApplyTheme()) { 566 path.applyTheme(t); 567 } 568 } 569 570 final ArrayList<VGroup> childGroups = currentGroup.mChildGroupList; 571 572 for (int i = 0; i < childGroups.size(); i++) { 573 VGroup childGroup = childGroups.get(i); 574 if (childGroup.canApplyTheme()) { 575 childGroup.applyTheme(t); 576 } 577 recursiveApplyTheme(childGroup, t); 578 } 579 580 } 581 582 public void setColorFilter(ColorFilter colorFilter) { 583 mColorFilter = colorFilter; 584 585 if (mFillPaint != null) { 586 mFillPaint.setColorFilter(colorFilter); 587 } 588 589 if (mStrokePaint != null) { 590 mStrokePaint.setColorFilter(colorFilter); 591 } 592 593 } 594 595 private void drawGroupTree(VGroup currentGroup, Matrix currentMatrix, 596 float currentAlpha, Canvas canvas, int w, int h) { 597 // Calculate current group's matrix by preConcat the parent's and 598 // and the current one on the top of the stack. 599 // Basically the Mfinal = Mviewport * M0 * M1 * M2; 600 // Mi the local matrix at level i of the group tree. 601 currentGroup.mStackedMatrix.set(currentMatrix); 602 603 currentGroup.mStackedMatrix.preConcat(currentGroup.mLocalMatrix); 604 605 float stackedAlpha = currentAlpha * currentGroup.mGroupAlpha; 606 drawPath(currentGroup, stackedAlpha, canvas, w, h); 607 // Draw the group tree in post order. 608 for (int i = 0 ; i < currentGroup.mChildGroupList.size(); i++) { 609 drawGroupTree(currentGroup.mChildGroupList.get(i), 610 currentGroup.mStackedMatrix, stackedAlpha, canvas, w, h); 611 } 612 } 613 614 public void draw(Canvas canvas, int w, int h) { 615 // Travese the tree in pre-order to draw. 616 drawGroupTree(mRootGroup, IDENTITY_MATRIX, ((float) mRootAlpha) / 0xFF, canvas, w, h); 617 } 618 619 private void drawPath(VGroup vGroup, float stackedAlpha, Canvas canvas, int w, int h) { 620 final float scale = Math.min(h / mViewportHeight, w / mViewportWidth); 621 622 mFinalPathMatrix.set(vGroup.mStackedMatrix); 623 mFinalPathMatrix.postScale(scale, scale, mViewportWidth / 2f, mViewportHeight / 2f); 624 mFinalPathMatrix.postTranslate(w / 2f - mViewportWidth / 2f, h / 2f - mViewportHeight / 2f); 625 626 ArrayList<VPath> paths = vGroup.getPaths(); 627 for (int i = 0; i < paths.size(); i++) { 628 VPath vPath = paths.get(i); 629 vPath.toPath(mPath); 630 final Path path = mPath; 631 632 if (vPath.mTrimPathStart != 0.0f || vPath.mTrimPathEnd != 1.0f) { 633 float start = (vPath.mTrimPathStart + vPath.mTrimPathOffset) % 1.0f; 634 float end = (vPath.mTrimPathEnd + vPath.mTrimPathOffset) % 1.0f; 635 636 if (mPathMeasure == null) { 637 mPathMeasure = new PathMeasure(); 638 } 639 mPathMeasure.setPath(mPath, false); 640 641 float len = mPathMeasure.getLength(); 642 start = start * len; 643 end = end * len; 644 path.reset(); 645 if (start > end) { 646 mPathMeasure.getSegment(start, len, path, true); 647 mPathMeasure.getSegment(0f, end, path, true); 648 } else { 649 mPathMeasure.getSegment(start, end, path, true); 650 } 651 path.rLineTo(0, 0); // fix bug in measure 652 } 653 654 mRenderPath.reset(); 655 656 mRenderPath.addPath(path, mFinalPathMatrix); 657 658 if (vPath.mClip) { 659 canvas.clipPath(mRenderPath, Region.Op.REPLACE); 660 } else { 661 if (vPath.mFillColor != 0) { 662 if (mFillPaint == null) { 663 mFillPaint = new Paint(); 664 mFillPaint.setColorFilter(mColorFilter); 665 mFillPaint.setStyle(Paint.Style.FILL); 666 mFillPaint.setAntiAlias(true); 667 } 668 mFillPaint.setColor(applyAlpha(vPath.mFillColor, stackedAlpha)); 669 canvas.drawPath(mRenderPath, mFillPaint); 670 } 671 672 if (vPath.mStrokeColor != 0) { 673 if (mStrokePaint == null) { 674 mStrokePaint = new Paint(); 675 mStrokePaint.setColorFilter(mColorFilter); 676 mStrokePaint.setStyle(Paint.Style.STROKE); 677 mStrokePaint.setAntiAlias(true); 678 } 679 680 final Paint strokePaint = mStrokePaint; 681 if (vPath.mStrokeLineJoin != null) { 682 strokePaint.setStrokeJoin(vPath.mStrokeLineJoin); 683 } 684 685 if (vPath.mStrokeLineCap != null) { 686 strokePaint.setStrokeCap(vPath.mStrokeLineCap); 687 } 688 689 strokePaint.setStrokeMiter(vPath.mStrokeMiterlimit * scale); 690 691 strokePaint.setColor(applyAlpha(vPath.mStrokeColor, stackedAlpha)); 692 strokePaint.setStrokeWidth(vPath.mStrokeWidth * scale); 693 canvas.drawPath(mRenderPath, strokePaint); 694 } 695 } 696 } 697 } 698 699 private void parseViewport(Resources r, AttributeSet attrs) 700 throws XmlPullParserException { 701 final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableViewport); 702 mViewportWidth = a.getFloat(R.styleable.VectorDrawableViewport_viewportWidth, mViewportWidth); 703 mViewportHeight = a.getFloat(R.styleable.VectorDrawableViewport_viewportHeight, mViewportHeight); 704 705 if (mViewportWidth <= 0) { 706 throw new XmlPullParserException(a.getPositionDescription() + 707 "<viewport> tag requires viewportWidth > 0"); 708 } else if (mViewportHeight <= 0) { 709 throw new XmlPullParserException(a.getPositionDescription() + 710 "<viewport> tag requires viewportHeight > 0"); 711 } 712 713 a.recycle(); 714 } 715 716 private void parseSize(Resources r, AttributeSet attrs) 717 throws XmlPullParserException { 718 final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableSize); 719 mBaseWidth = a.getDimension(R.styleable.VectorDrawableSize_width, mBaseWidth); 720 mBaseHeight = a.getDimension(R.styleable.VectorDrawableSize_height, mBaseHeight); 721 722 if (mBaseWidth <= 0) { 723 throw new XmlPullParserException(a.getPositionDescription() + 724 "<size> tag requires width > 0"); 725 } else if (mBaseHeight <= 0) { 726 throw new XmlPullParserException(a.getPositionDescription() + 727 "<size> tag requires height > 0"); 728 } 729 730 a.recycle(); 731 } 732 733 } 734 735 static class VGroup { 736 private final ArrayList<VPath> mPathList = new ArrayList<VPath>(); 737 private final ArrayList<VGroup> mChildGroupList = new ArrayList<VGroup>(); 738 739 private float mRotate = 0; 740 private float mPivotX = 0; 741 private float mPivotY = 0; 742 private float mScaleX = 1; 743 private float mScaleY = 1; 744 private float mTranslateX = 0; 745 private float mTranslateY = 0; 746 private float mGroupAlpha = 1; 747 748 // mLocalMatrix is parsed from the XML. 749 private final Matrix mLocalMatrix = new Matrix(); 750 // mStackedMatrix is only used when drawing, it combines all the 751 // parents' local matrices with the current one. 752 private final Matrix mStackedMatrix = new Matrix(); 753 754 private int[] mThemeAttrs; 755 756 private String mGroupName = null; 757 758 /* Getter and Setter */ 759 public float getRotation() { 760 return mRotate; 761 } 762 763 public void setRotation(float rotation) { 764 if (rotation != mRotate) { 765 mRotate = rotation; 766 updateLocalMatrix(); 767 } 768 } 769 770 public float getPivotX() { 771 return mPivotX; 772 } 773 774 public void setPivotX(float pivotX) { 775 if (pivotX != mPivotX) { 776 mPivotX = pivotX; 777 updateLocalMatrix(); 778 } 779 } 780 781 public float getPivotY() { 782 return mPivotY; 783 } 784 785 public void setPivotY(float pivotY) { 786 if (pivotY != mPivotY) { 787 mPivotY = pivotY; 788 updateLocalMatrix(); 789 } 790 } 791 792 public float getScaleX() { 793 return mScaleX; 794 } 795 796 public void setScaleX(float scaleX) { 797 if (scaleX != mScaleX) { 798 mScaleX = scaleX; 799 updateLocalMatrix(); 800 } 801 } 802 803 public float getScaleY() { 804 return mScaleY; 805 } 806 807 public void setScaleY(float scaleY) { 808 if (scaleY != mScaleY) { 809 mScaleY = scaleY; 810 updateLocalMatrix(); 811 } 812 } 813 814 public float getTranslateX() { 815 return mTranslateX; 816 } 817 818 public void setTranslateX(float translateX) { 819 if (translateX != mTranslateX) { 820 mTranslateX = translateX; 821 updateLocalMatrix(); 822 } 823 } 824 825 public float getTranslateY() { 826 return mTranslateY; 827 } 828 829 public void setTranslateY(float translateY) { 830 if (translateY != mTranslateY) { 831 mTranslateY = translateY; 832 updateLocalMatrix(); 833 } 834 } 835 836 public float getAlpha() { 837 return mGroupAlpha; 838 } 839 840 public void setAlpha(float groupAlpha) { 841 if (groupAlpha != mGroupAlpha) { 842 mGroupAlpha = groupAlpha; 843 } 844 } 845 846 public String getGroupName() { 847 return mGroupName; 848 } 849 850 public Matrix getLocalMatrix() { 851 return mLocalMatrix; 852 } 853 854 public void add(VPath path) { 855 mPathList.add(path); 856 } 857 858 public boolean canApplyTheme() { 859 return mThemeAttrs != null; 860 } 861 862 public void applyTheme(Theme t) { 863 if (mThemeAttrs == null) { 864 return; 865 } 866 867 final TypedArray a = t.resolveAttributes( 868 mThemeAttrs, R.styleable.VectorDrawablePath); 869 870 mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate); 871 mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX); 872 mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY); 873 mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX); 874 mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY); 875 mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX); 876 mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY); 877 mGroupAlpha = a.getFloat(R.styleable.VectorDrawableGroup_alpha, mGroupAlpha); 878 updateLocalMatrix(); 879 if (a.hasValue(R.styleable.VectorDrawableGroup_name)) { 880 mGroupName = a.getString(R.styleable.VectorDrawableGroup_name); 881 } 882 a.recycle(); 883 } 884 885 public void inflate(Resources res, AttributeSet attrs, Theme theme) { 886 final TypedArray a = obtainAttributes(res, theme, attrs, R.styleable.VectorDrawableGroup); 887 final int[] themeAttrs = a.extractThemeAttrs(); 888 889 mThemeAttrs = themeAttrs; 890 // NOTE: The set of attributes loaded here MUST match the 891 // set of attributes loaded in applyTheme. 892 893 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_rotation] == 0) { 894 mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate); 895 } 896 897 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_pivotX] == 0) { 898 mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX); 899 } 900 901 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_pivotY] == 0) { 902 mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY); 903 } 904 905 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_scaleX] == 0) { 906 mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX); 907 } 908 909 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_scaleY] == 0) { 910 mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY); 911 } 912 913 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_translateX] == 0) { 914 mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX); 915 } 916 917 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_translateY] == 0) { 918 mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY); 919 } 920 921 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_name] == 0) { 922 mGroupName = a.getString(R.styleable.VectorDrawableGroup_name); 923 } 924 925 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawableGroup_alpha] == 0) { 926 mGroupAlpha = a.getFloat(R.styleable.VectorDrawableGroup_alpha, mGroupAlpha); 927 } 928 929 updateLocalMatrix(); 930 a.recycle(); 931 } 932 933 private void updateLocalMatrix() { 934 // The order we apply is the same as the 935 // RenderNode.cpp::applyViewPropertyTransforms(). 936 mLocalMatrix.reset(); 937 mLocalMatrix.postTranslate(-mPivotX, -mPivotY); 938 mLocalMatrix.postScale(mScaleX, mScaleY); 939 mLocalMatrix.postRotate(mRotate, 0, 0); 940 mLocalMatrix.postTranslate(mTranslateX + mPivotX, mTranslateY + mPivotY); 941 } 942 943 /** 944 * Must return in order of adding 945 * @return ordered list of paths 946 */ 947 public ArrayList<VPath> getPaths() { 948 return mPathList; 949 } 950 951 } 952 953 static class VPath { 954 private int[] mThemeAttrs; 955 956 int mStrokeColor = 0; 957 float mStrokeWidth = 0; 958 float mStrokeOpacity = Float.NaN; 959 int mFillColor = Color.BLACK; 960 int mFillRule; 961 float mFillOpacity = Float.NaN; 962 float mTrimPathStart = 0; 963 float mTrimPathEnd = 1; 964 float mTrimPathOffset = 0; 965 966 boolean mClip = false; 967 Paint.Cap mStrokeLineCap = Paint.Cap.BUTT; 968 Paint.Join mStrokeLineJoin = Paint.Join.MITER; 969 float mStrokeMiterlimit = 4; 970 971 private VNode[] mNode = null; 972 private String mPathName; 973 974 public VPath() { 975 // Empty constructor. 976 } 977 978 public void toPath(Path path) { 979 path.reset(); 980 if (mNode != null) { 981 VNode.createPath(mNode, path); 982 } 983 } 984 985 public String getPathName() { 986 return mPathName; 987 } 988 989 private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) { 990 switch (id) { 991 case LINECAP_BUTT: 992 return Paint.Cap.BUTT; 993 case LINECAP_ROUND: 994 return Paint.Cap.ROUND; 995 case LINECAP_SQUARE: 996 return Paint.Cap.SQUARE; 997 default: 998 return defValue; 999 } 1000 } 1001 1002 private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) { 1003 switch (id) { 1004 case LINEJOIN_MITER: 1005 return Paint.Join.MITER; 1006 case LINEJOIN_ROUND: 1007 return Paint.Join.ROUND; 1008 case LINEJOIN_BEVEL: 1009 return Paint.Join.BEVEL; 1010 default: 1011 return defValue; 1012 } 1013 } 1014 1015 /* Setters and Getters */ 1016 int getStroke() { 1017 return mStrokeColor; 1018 } 1019 1020 void setStroke(int strokeColor) { 1021 mStrokeColor = strokeColor; 1022 } 1023 1024 float getStrokeWidth() { 1025 return mStrokeWidth; 1026 } 1027 1028 void setStrokeWidth(float strokeWidth) { 1029 mStrokeWidth = strokeWidth; 1030 } 1031 1032 float getStrokeOpacity() { 1033 return mStrokeOpacity; 1034 } 1035 1036 void setStrokeOpacity(float strokeOpacity) { 1037 mStrokeOpacity = strokeOpacity; 1038 } 1039 1040 int getFill() { 1041 return mFillColor; 1042 } 1043 1044 void setFill(int fillColor) { 1045 mFillColor = fillColor; 1046 } 1047 1048 float getFillOpacity() { 1049 return mFillOpacity; 1050 } 1051 1052 void setFillOpacity(float fillOpacity) { 1053 mFillOpacity = fillOpacity; 1054 } 1055 1056 float getTrimPathStart() { 1057 return mTrimPathStart; 1058 } 1059 1060 void setTrimPathStart(float trimPathStart) { 1061 mTrimPathStart = trimPathStart; 1062 } 1063 1064 float getTrimPathEnd() { 1065 return mTrimPathEnd; 1066 } 1067 1068 void setTrimPathEnd(float trimPathEnd) { 1069 mTrimPathEnd = trimPathEnd; 1070 } 1071 1072 float getTrimPathOffset() { 1073 return mTrimPathOffset; 1074 } 1075 1076 void setTrimPathOffset(float trimPathOffset) { 1077 mTrimPathOffset = trimPathOffset; 1078 } 1079 1080 public void inflate(Resources r, AttributeSet attrs, Theme theme) { 1081 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawablePath); 1082 final int[] themeAttrs = a.extractThemeAttrs(); 1083 mThemeAttrs = themeAttrs; 1084 1085 // NOTE: The set of attributes loaded here MUST match the 1086 // set of attributes loaded in applyTheme. 1087 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_clipToPath] == 0) { 1088 mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip); 1089 } 1090 1091 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_name] == 0) { 1092 mPathName = a.getString(R.styleable.VectorDrawablePath_name); 1093 } 1094 1095 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pathData] == 0) { 1096 mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData)); 1097 } 1098 1099 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fill] == 0) { 1100 mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor); 1101 } 1102 1103 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fillOpacity] == 0) { 1104 mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity); 1105 } 1106 1107 if (themeAttrs == null 1108 || themeAttrs[R.styleable.VectorDrawablePath_strokeLineCap] == 0) { 1109 mStrokeLineCap = getStrokeLineCap( 1110 a.getInt(R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap); 1111 } 1112 1113 if (themeAttrs == null 1114 || themeAttrs[R.styleable.VectorDrawablePath_strokeLineJoin] == 0) { 1115 mStrokeLineJoin = getStrokeLineJoin( 1116 a.getInt(R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin); 1117 } 1118 1119 if (themeAttrs == null 1120 || themeAttrs[R.styleable.VectorDrawablePath_strokeMiterLimit] == 0) { 1121 mStrokeMiterlimit = a.getFloat( 1122 R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit); 1123 } 1124 1125 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_stroke] == 0) { 1126 mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor); 1127 } 1128 1129 if (themeAttrs == null 1130 || themeAttrs[R.styleable.VectorDrawablePath_strokeOpacity] == 0) { 1131 mStrokeOpacity = a.getFloat( 1132 R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity); 1133 } 1134 1135 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_strokeWidth] == 0) { 1136 mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth); 1137 } 1138 1139 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_trimPathEnd] == 0) { 1140 mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd); 1141 } 1142 1143 if (themeAttrs == null 1144 || themeAttrs[R.styleable.VectorDrawablePath_trimPathOffset] == 0) { 1145 mTrimPathOffset = a.getFloat( 1146 R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset); 1147 } 1148 1149 if (themeAttrs == null 1150 || themeAttrs[R.styleable.VectorDrawablePath_trimPathStart] == 0) { 1151 mTrimPathStart = a.getFloat( 1152 R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart); 1153 } 1154 1155 updateColorAlphas(); 1156 1157 a.recycle(); 1158 } 1159 1160 public boolean canApplyTheme() { 1161 return mThemeAttrs != null; 1162 } 1163 1164 public void applyTheme(Theme t) { 1165 if (mThemeAttrs == null) { 1166 return; 1167 } 1168 1169 final TypedArray a = t.resolveAttributes( 1170 mThemeAttrs, R.styleable.VectorDrawablePath); 1171 1172 mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip); 1173 1174 if (a.hasValue(R.styleable.VectorDrawablePath_name)) { 1175 mPathName = a.getString(R.styleable.VectorDrawablePath_name); 1176 } 1177 1178 if (a.hasValue(R.styleable.VectorDrawablePath_pathData)) { 1179 mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData)); 1180 } 1181 1182 mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor); 1183 mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity); 1184 1185 mStrokeLineCap = getStrokeLineCap(a.getInt( 1186 R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap); 1187 mStrokeLineJoin = getStrokeLineJoin(a.getInt( 1188 R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin); 1189 mStrokeMiterlimit = a.getFloat( 1190 R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit); 1191 mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor); 1192 mStrokeOpacity = a.getFloat( 1193 R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity); 1194 mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth); 1195 1196 mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd); 1197 mTrimPathOffset = a.getFloat( 1198 R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset); 1199 mTrimPathStart = a.getFloat( 1200 R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart); 1201 1202 updateColorAlphas(); 1203 a.recycle(); 1204 } 1205 1206 private void updateColorAlphas() { 1207 if (!Float.isNaN(mFillOpacity)) { 1208 mFillColor = applyAlpha(mFillColor, mFillOpacity); 1209 } 1210 1211 if (!Float.isNaN(mStrokeOpacity)) { 1212 mStrokeColor = applyAlpha(mStrokeColor, mStrokeOpacity); 1213 } 1214 } 1215 1216 private static int nextStart(String s, int end) { 1217 char c; 1218 1219 while (end < s.length()) { 1220 c = s.charAt(end); 1221 if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) { 1222 return end; 1223 } 1224 end++; 1225 } 1226 return end; 1227 } 1228 1229 private void addNode(ArrayList<VectorDrawable.VNode> list, char cmd, float[] val) { 1230 list.add(new VectorDrawable.VNode(cmd, val)); 1231 } 1232 1233 /** 1234 * parse the floats in the string 1235 * this is an optimized version of 1236 * parseFloat(s.split(",|\\s")); 1237 * 1238 * @param s the string containing a command and list of floats 1239 * @return array of floats 1240 */ 1241 private static float[] getFloats(String s) { 1242 if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') { 1243 return new float[0]; 1244 } 1245 try { 1246 float[] tmp = new float[s.length()]; 1247 int count = 0; 1248 int pos = 1, end; 1249 while ((end = extract(s, pos)) >= 0) { 1250 if (pos < end) { 1251 tmp[count++] = Float.parseFloat(s.substring(pos, end)); 1252 } 1253 pos = end + 1; 1254 } 1255 // handle the final float if there is one 1256 if (pos < s.length()) { 1257 tmp[count++] = Float.parseFloat(s.substring(pos, s.length())); 1258 } 1259 return Arrays.copyOf(tmp, count); 1260 } catch (NumberFormatException e){ 1261 Log.e(LOGTAG,"error in parsing \""+s+"\""); 1262 throw e; 1263 } 1264 } 1265 1266 /** 1267 * calculate the position of the next comma or space 1268 * @param s the string to search 1269 * @param start the position to start searching 1270 * @return the position of the next comma or space or -1 if none found 1271 */ 1272 private static int extract(String s, int start) { 1273 int space = s.indexOf(' ', start); 1274 int comma = s.indexOf(',', start); 1275 if (space == -1) { 1276 return comma; 1277 } 1278 if (comma == -1) { 1279 return space; 1280 } 1281 return (comma > space) ? space : comma; 1282 } 1283 1284 private VectorDrawable.VNode[] parsePath(String value) { 1285 int start = 0; 1286 int end = 1; 1287 1288 ArrayList<VectorDrawable.VNode> list = new ArrayList<VectorDrawable.VNode>(); 1289 while (end < value.length()) { 1290 end = nextStart(value, end); 1291 String s = value.substring(start, end); 1292 float[] val = getFloats(s); 1293 addNode(list, s.charAt(0), val); 1294 1295 start = end; 1296 end++; 1297 } 1298 if ((end - start) == 1 && start < value.length()) { 1299 1300 addNode(list, value.charAt(start), new float[0]); 1301 } 1302 return list.toArray(new VectorDrawable.VNode[list.size()]); 1303 } 1304 } 1305 1306 private static class VNode { 1307 private char mType; 1308 private float[] mParams; 1309 1310 public VNode(char type, float[] params) { 1311 mType = type; 1312 mParams = params; 1313 } 1314 1315 public VNode(VNode n) { 1316 mType = n.mType; 1317 mParams = Arrays.copyOf(n.mParams, n.mParams.length); 1318 } 1319 1320 public static void createPath(VNode[] node, Path path) { 1321 float[] current = new float[4]; 1322 char previousCommand = 'm'; 1323 for (int i = 0; i < node.length; i++) { 1324 addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); 1325 previousCommand = node[i].mType; 1326 } 1327 } 1328 1329 private static void addCommand(Path path, float[] current, 1330 char previousCmd, char cmd, float[] val) { 1331 1332 int incr = 2; 1333 float currentX = current[0]; 1334 float currentY = current[1]; 1335 float ctrlPointX = current[2]; 1336 float ctrlPointY = current[3]; 1337 float reflectiveCtrlPointX; 1338 float reflectiveCtrlPointY; 1339 1340 switch (cmd) { 1341 case 'z': 1342 case 'Z': 1343 path.close(); 1344 return; 1345 case 'm': 1346 case 'M': 1347 case 'l': 1348 case 'L': 1349 case 't': 1350 case 'T': 1351 incr = 2; 1352 break; 1353 case 'h': 1354 case 'H': 1355 case 'v': 1356 case 'V': 1357 incr = 1; 1358 break; 1359 case 'c': 1360 case 'C': 1361 incr = 6; 1362 break; 1363 case 's': 1364 case 'S': 1365 case 'q': 1366 case 'Q': 1367 incr = 4; 1368 break; 1369 case 'a': 1370 case 'A': 1371 incr = 7; 1372 break; 1373 } 1374 for (int k = 0; k < val.length; k += incr) { 1375 switch (cmd) { 1376 case 'm': // moveto - Start a new sub-path (relative) 1377 path.rMoveTo(val[k + 0], val[k + 1]); 1378 currentX += val[k + 0]; 1379 currentY += val[k + 1]; 1380 break; 1381 case 'M': // moveto - Start a new sub-path 1382 path.moveTo(val[k + 0], val[k + 1]); 1383 currentX = val[k + 0]; 1384 currentY = val[k + 1]; 1385 break; 1386 case 'l': // lineto - Draw a line from the current point (relative) 1387 path.rLineTo(val[k + 0], val[k + 1]); 1388 currentX += val[k + 0]; 1389 currentY += val[k + 1]; 1390 break; 1391 case 'L': // lineto - Draw a line from the current point 1392 path.lineTo(val[k + 0], val[k + 1]); 1393 currentX = val[k + 0]; 1394 currentY = val[k + 1]; 1395 break; 1396 case 'z': // closepath - Close the current subpath 1397 case 'Z': // closepath - Close the current subpath 1398 path.close(); 1399 break; 1400 case 'h': // horizontal lineto - Draws a horizontal line (relative) 1401 path.rLineTo(val[k + 0], 0); 1402 currentX += val[k + 0]; 1403 break; 1404 case 'H': // horizontal lineto - Draws a horizontal line 1405 path.lineTo(val[k + 0], currentY); 1406 currentX = val[k + 0]; 1407 break; 1408 case 'v': // vertical lineto - Draws a vertical line from the current point (r) 1409 path.rLineTo(0, val[k + 0]); 1410 currentY += val[k + 0]; 1411 break; 1412 case 'V': // vertical lineto - Draws a vertical line from the current point 1413 path.lineTo(currentX, val[k + 0]); 1414 currentY = val[k + 0]; 1415 break; 1416 case 'c': // curveto - Draws a cubic Bézier curve (relative) 1417 path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], 1418 val[k + 4], val[k + 5]); 1419 1420 ctrlPointX = currentX + val[k + 2]; 1421 ctrlPointY = currentY + val[k + 3]; 1422 currentX += val[k + 4]; 1423 currentY += val[k + 5]; 1424 1425 break; 1426 case 'C': // curveto - Draws a cubic Bézier curve 1427 path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], 1428 val[k + 4], val[k + 5]); 1429 currentX = val[k + 4]; 1430 currentY = val[k + 5]; 1431 ctrlPointX = val[k + 2]; 1432 ctrlPointY = val[k + 3]; 1433 break; 1434 case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) 1435 reflectiveCtrlPointX = 0; 1436 reflectiveCtrlPointY = 0; 1437 if (previousCmd == 'c' || previousCmd == 's' 1438 || previousCmd == 'C' || previousCmd == 'S') { 1439 reflectiveCtrlPointX = currentX - ctrlPointX; 1440 reflectiveCtrlPointY = currentY - ctrlPointY; 1441 } 1442 path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 1443 val[k + 0], val[k + 1], 1444 val[k + 2], val[k + 3]); 1445 1446 ctrlPointX = currentX + val[k + 0]; 1447 ctrlPointY = currentY + val[k + 1]; 1448 currentX += val[k + 2]; 1449 currentY += val[k + 3]; 1450 break; 1451 case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) 1452 reflectiveCtrlPointX = currentX; 1453 reflectiveCtrlPointY = currentY; 1454 if (previousCmd == 'c' || previousCmd == 's' 1455 || previousCmd == 'C' || previousCmd == 'S') { 1456 reflectiveCtrlPointX = 2 * currentX - ctrlPointX; 1457 reflectiveCtrlPointY = 2 * currentY - ctrlPointY; 1458 } 1459 path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 1460 val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 1461 ctrlPointX = val[k + 0]; 1462 ctrlPointY = val[k + 1]; 1463 currentX = val[k + 2]; 1464 currentY = val[k + 3]; 1465 break; 1466 case 'q': // Draws a quadratic Bézier (relative) 1467 path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 1468 ctrlPointX = currentX + val[k + 0]; 1469 ctrlPointY = currentY + val[k + 1]; 1470 currentX += val[k + 2]; 1471 currentY += val[k + 3]; 1472 break; 1473 case 'Q': // Draws a quadratic Bézier 1474 path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 1475 ctrlPointX = val[k + 0]; 1476 ctrlPointY = val[k + 1]; 1477 currentX = val[k + 2]; 1478 currentY = val[k + 3]; 1479 break; 1480 case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) 1481 reflectiveCtrlPointX = 0; 1482 reflectiveCtrlPointY = 0; 1483 if (previousCmd == 'q' || previousCmd == 't' 1484 || previousCmd == 'Q' || previousCmd == 'T') { 1485 reflectiveCtrlPointX = currentX - ctrlPointX; 1486 reflectiveCtrlPointY = currentY - ctrlPointY; 1487 } 1488 path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 1489 val[k + 0], val[k + 1]); 1490 ctrlPointX = currentX + reflectiveCtrlPointX; 1491 ctrlPointY = currentY + reflectiveCtrlPointY; 1492 currentX += val[k + 0]; 1493 currentY += val[k + 1]; 1494 break; 1495 case 'T': // Draws a quadratic Bézier curve (reflective control point) 1496 reflectiveCtrlPointX = currentX; 1497 reflectiveCtrlPointY = currentY; 1498 if (previousCmd == 'q' || previousCmd == 't' 1499 || previousCmd == 'Q' || previousCmd == 'T') { 1500 reflectiveCtrlPointX = 2 * currentX - ctrlPointX; 1501 reflectiveCtrlPointY = 2 * currentY - ctrlPointY; 1502 } 1503 path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 1504 val[k + 0], val[k + 1]); 1505 ctrlPointX = reflectiveCtrlPointX; 1506 ctrlPointY = reflectiveCtrlPointY; 1507 currentX = val[k + 0]; 1508 currentY = val[k + 1]; 1509 break; 1510 case 'a': // Draws an elliptical arc 1511 // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) 1512 drawArc(path, 1513 currentX, 1514 currentY, 1515 val[k + 5] + currentX, 1516 val[k + 6] + currentY, 1517 val[k + 0], 1518 val[k + 1], 1519 val[k + 2], 1520 val[k + 3] != 0, 1521 val[k + 4] != 0); 1522 currentX += val[k + 5]; 1523 currentY += val[k + 6]; 1524 ctrlPointX = currentX; 1525 ctrlPointY = currentY; 1526 break; 1527 case 'A': // Draws an elliptical arc 1528 drawArc(path, 1529 currentX, 1530 currentY, 1531 val[k + 5], 1532 val[k + 6], 1533 val[k + 0], 1534 val[k + 1], 1535 val[k + 2], 1536 val[k + 3] != 0, 1537 val[k + 4] != 0); 1538 currentX = val[k + 5]; 1539 currentY = val[k + 6]; 1540 ctrlPointX = currentX; 1541 ctrlPointY = currentY; 1542 break; 1543 } 1544 previousCmd = cmd; 1545 } 1546 current[0] = currentX; 1547 current[1] = currentY; 1548 current[2] = ctrlPointX; 1549 current[3] = ctrlPointY; 1550 } 1551 1552 private static void drawArc(Path p, 1553 float x0, 1554 float y0, 1555 float x1, 1556 float y1, 1557 float a, 1558 float b, 1559 float theta, 1560 boolean isMoreThanHalf, 1561 boolean isPositiveArc) { 1562 1563 /* Convert rotation angle from degrees to radians */ 1564 double thetaD = Math.toRadians(theta); 1565 /* Pre-compute rotation matrix entries */ 1566 double cosTheta = Math.cos(thetaD); 1567 double sinTheta = Math.sin(thetaD); 1568 /* Transform (x0, y0) and (x1, y1) into unit space */ 1569 /* using (inverse) rotation, followed by (inverse) scale */ 1570 double x0p = (x0 * cosTheta + y0 * sinTheta) / a; 1571 double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; 1572 double x1p = (x1 * cosTheta + y1 * sinTheta) / a; 1573 double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; 1574 1575 /* Compute differences and averages */ 1576 double dx = x0p - x1p; 1577 double dy = y0p - y1p; 1578 double xm = (x0p + x1p) / 2; 1579 double ym = (y0p + y1p) / 2; 1580 /* Solve for intersecting unit circles */ 1581 double dsq = dx * dx + dy * dy; 1582 if (dsq == 0.0) { 1583 Log.w(LOGTAG, " Points are coincident"); 1584 return; /* Points are coincident */ 1585 } 1586 double disc = 1.0 / dsq - 1.0 / 4.0; 1587 if (disc < 0.0) { 1588 Log.w(LOGTAG, "Points are too far apart " + dsq); 1589 float adjust = (float) (Math.sqrt(dsq) / 1.99999); 1590 drawArc(p, x0, y0, x1, y1, a * adjust, 1591 b * adjust, theta, isMoreThanHalf, isPositiveArc); 1592 return; /* Points are too far apart */ 1593 } 1594 double s = Math.sqrt(disc); 1595 double sdx = s * dx; 1596 double sdy = s * dy; 1597 double cx; 1598 double cy; 1599 if (isMoreThanHalf == isPositiveArc) { 1600 cx = xm - sdy; 1601 cy = ym + sdx; 1602 } else { 1603 cx = xm + sdy; 1604 cy = ym - sdx; 1605 } 1606 1607 double eta0 = Math.atan2((y0p - cy), (x0p - cx)); 1608 1609 double eta1 = Math.atan2((y1p - cy), (x1p - cx)); 1610 1611 double sweep = (eta1 - eta0); 1612 if (isPositiveArc != (sweep >= 0)) { 1613 if (sweep > 0) { 1614 sweep -= 2 * Math.PI; 1615 } else { 1616 sweep += 2 * Math.PI; 1617 } 1618 } 1619 1620 cx *= a; 1621 cy *= b; 1622 double tcx = cx; 1623 cx = cx * cosTheta - cy * sinTheta; 1624 cy = tcx * sinTheta + cy * cosTheta; 1625 1626 arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); 1627 } 1628 1629 /** 1630 * Converts an arc to cubic Bezier segments and records them in p. 1631 * 1632 * @param p The target for the cubic Bezier segments 1633 * @param cx The x coordinate center of the ellipse 1634 * @param cy The y coordinate center of the ellipse 1635 * @param a The radius of the ellipse in the horizontal direction 1636 * @param b The radius of the ellipse in the vertical direction 1637 * @param e1x E(eta1) x coordinate of the starting point of the arc 1638 * @param e1y E(eta2) y coordinate of the starting point of the arc 1639 * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane 1640 * @param start The start angle of the arc on the ellipse 1641 * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse 1642 */ 1643 private static void arcToBezier(Path p, 1644 double cx, 1645 double cy, 1646 double a, 1647 double b, 1648 double e1x, 1649 double e1y, 1650 double theta, 1651 double start, 1652 double sweep) { 1653 // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html 1654 // and http://www.spaceroots.org/documents/ellipse/node22.html 1655 1656 // Maximum of 45 degrees per cubic Bezier segment 1657 int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); 1658 1659 double eta1 = start; 1660 double cosTheta = Math.cos(theta); 1661 double sinTheta = Math.sin(theta); 1662 double cosEta1 = Math.cos(eta1); 1663 double sinEta1 = Math.sin(eta1); 1664 double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); 1665 double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); 1666 1667 double anglePerSegment = sweep / numSegments; 1668 for (int i = 0; i < numSegments; i++) { 1669 double eta2 = eta1 + anglePerSegment; 1670 double sinEta2 = Math.sin(eta2); 1671 double cosEta2 = Math.cos(eta2); 1672 double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); 1673 double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); 1674 double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; 1675 double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; 1676 double tanDiff2 = Math.tan((eta2 - eta1) / 2); 1677 double alpha = 1678 Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; 1679 double q1x = e1x + alpha * ep1x; 1680 double q1y = e1y + alpha * ep1y; 1681 double q2x = e2x - alpha * ep2x; 1682 double q2y = e2y - alpha * ep2y; 1683 1684 p.cubicTo((float) q1x, 1685 (float) q1y, 1686 (float) q2x, 1687 (float) q2y, 1688 (float) e2x, 1689 (float) e2y); 1690 eta1 = eta2; 1691 e1x = e2x; 1692 e1y = e2y; 1693 ep1x = ep2x; 1694 ep1y = ep2y; 1695 } 1696 } 1697 1698 } 1699} 1700