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