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