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