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