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