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