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