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