VectorDrawable.java revision 498213a265e05134b4a4fbf93dce69de1a47973c
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.animation.ObjectAnimator; 18import android.animation.ValueAnimator; 19import android.content.res.Resources; 20import android.content.res.Resources.Theme; 21import android.content.res.TypedArray; 22import android.graphics.Canvas; 23import android.graphics.ColorFilter; 24import android.graphics.Matrix; 25import android.graphics.Paint; 26import android.graphics.Path; 27import android.graphics.PathMeasure; 28import android.graphics.PixelFormat; 29import android.graphics.Rect; 30import android.graphics.Region; 31import android.util.AttributeSet; 32import android.util.Log; 33import android.util.Xml; 34import android.view.animation.AccelerateDecelerateInterpolator; 35import android.view.animation.Interpolator; 36import android.view.animation.LinearInterpolator; 37 38import com.android.internal.R; 39 40import org.xmlpull.v1.XmlPullParser; 41import org.xmlpull.v1.XmlPullParserException; 42import org.xmlpull.v1.XmlPullParserFactory; 43 44import java.io.IOException; 45import java.util.ArrayList; 46import java.util.Arrays; 47import java.util.Collection; 48import java.util.HashMap; 49import java.util.HashSet; 50 51/** 52 * This lets you create a drawable based on an XML vector graphic It can be 53 * defined in an XML file with the <code><vector></code> element. 54 * <p/> 55 * The vector drawable has 6 elements: 56 * <p/> 57 * <dl> 58 * <dt><code><vector></code></dt> 59 * <dd>The attribute <code>android:trigger</code> defines a state change that 60 * will drive the animation</dd> 61 * <dd>The attribute <code>android:versionCode</code> defines the version of 62 * VectorDrawable</dd> 63 * <dt><code><size></code></dt> 64 * <dd>Used to defined the intrinsic Width Height size of the drawable using 65 * <code>android:width</code> and <code>android:height</code></dd> 66 * <dt><code><viewport></code></dt> 67 * <dd>Used to defined the size of the virtual canvas the paths are drawn on. 68 * The size is defined using the attributes <code>android:viewportHeight</code> 69 * <code>android:viewportWidth</code></dd> 70 * <dt><code><group></code></dt> 71 * <dd>Defines a "key frame" in the animation if there is only one group the 72 * drawable is static 2D image that has no animation.</dd> 73 * <dt><code><path></code></dt> 74 * <dd>Defines paths to be drawn. The path elements must be within a group 75 * <dl> 76 * <dt><code>android:name</code> 77 * <dd>Defines the name of the path.</dd></dt> 78 * <dt><code>android:pathData</code> 79 * <dd>Defines path string.</dd></dt> 80 * <dt><code>android:fill</code> 81 * <dd>Defines the color to fill the path (none if not present).</dd></dt> 82 * <dt><code>android:stroke</code> 83 * <dd>Defines the color to draw the path outline (none if not present).</dd> 84 * </dt> 85 * <dt><code>android:strokeWidth</code> 86 * <dd>The width a path stroke</dd></dt> 87 * <dt><code>android:strokeOpacity</code> 88 * <dd>The opacity of a path stroke</dd></dt> 89 * <dt><code>android:rotation</code> 90 * <dd>The amount to rotation the path stroke.</dd></dt> 91 * <dt><code>android:pivotX</code> 92 * <dd>The X coordinate of the center of rotation of a path</dd></dt> 93 * <dt><code>android:pivotY</code> 94 * <dd>The Y coordinate of the center of rotation of a path</dd></dt> 95 * <dt><code>android:fillOpacity</code> 96 * <dd>The opacity to fill the path with</dd></dt> 97 * <dt><code>android:trimPathStart</code> 98 * <dd>The fraction of the path to trim from the start from 0 to 1</dd></dt> 99 * <dt><code>android:trimPathEnd</code> 100 * <dd>The fraction of the path to trim from the end from 0 to 1</dd></dt> 101 * <dt><code>android:trimPathOffset</code> 102 * <dd>Shift trim region (allows showed region to include the start and end) 103 * from 0 to 1</dd></dt> 104 * <dt><code>android:clipToPath</code> 105 * <dd>Path will set the clip path</dd></dt> 106 * <dt><code>android:strokeLineCap</code> 107 * <dd>Sets the linecap for a stroked path: butt, round, square</dd></dt> 108 * <dt><code>android:strokeLineJoin</code> 109 * <dd>Sets the lineJoin for a stroked path: miter,round,bevel</dd></dt> 110 * <dt><code>android:strokeMiterLimit</code> 111 * <dd>Sets the Miter limit for a stroked path</dd></dt> 112 * <dt><code>android:state_pressed</code> 113 * <dd>Sets a condition to be met to draw path</dd></dt> 114 * <dt><code>android:state_focused</code> 115 * <dd>Sets a condition to be met to draw path</dd></dt> 116 * <dt><code>android:state_selected</code> 117 * <dd>Sets a condition to be met to draw path</dd></dt> 118 * <dt><code>android:state_window_focused</code> 119 * <dd>Sets a condition to be met to draw path</dd></dt> 120 * <dt><code>android:state_enabled</code> 121 * <dd>Sets a condition to be met to draw path</dd></dt> 122 * <dt><code>android:state_activated</code> 123 * <dd>Sets a condition to be met to draw path</dd></dt> 124 * <dt><code>android:state_accelerated</code> 125 * <dd>Sets a condition to be met to draw path</dd></dt> 126 * <dt><code>android:state_hovered</code> 127 * <dd>Sets a condition to be met to draw path</dd></dt> 128 * <dt><code>android:state_checked</code> 129 * <dd>Sets a condition to be met to draw path</dd></dt> 130 * <dt><code>android:state_checkable</code> 131 * <dd>Sets a condition to be met to draw path</dd></dt> 132 * </dl> 133 * </dd> 134 * <dt><code><animation></code></dt> 135 * <dd>Used to customize the transition between two paths 136 * <dl> 137 * <dt><code>android:sequence</code> 138 * <dd>Configures this animation sequence between the named paths.</dd></dt> 139 * <dt><code>android:limitTo</code> 140 * <dd>Limits an animation to only interpolate the selected variable unlimited, 141 * path, rotation, trimPathStart, trimPathEnd, trimPathOffset</dd></dt> 142 * <dt><code>android:repeatCount</code> 143 * <dd>Number of times to loop this aspect of the animation</dd></dt> 144 * <dt><code>android:durations</code> 145 * <dd>The duration of each step in the animation in milliseconds Must contain 146 * the number of named paths - 1</dd></dt> 147 * <dt><code>android:startDelay</code> 148 * <dd></dd></dt> 149 * <dt><code>android:repeatStyle</code> 150 * <dd>when repeating how does it repeat back and forth or a to b: forward, 151 * inAndOut</dd></dt> 152 * <dt><code>android:animate</code> 153 * <dd>linear, accelerate, decelerate, easing</dd></dt> 154 * </dl> 155 * </dd> 156 */ 157public class VectorDrawable extends Drawable { 158 private static final String LOGTAG = VectorDrawable.class.getSimpleName(); 159 160 private static final String SHAPE_SIZE = "size"; 161 private static final String SHAPE_VIEWPORT = "viewport"; 162 private static final String SHAPE_GROUP = "group"; 163 private static final String SHAPE_PATH = "path"; 164 private static final String SHAPE_TRANSITION = "transition"; 165 private static final String SHAPE_ANIMATION = "animation"; 166 private static final String SHAPE_VECTOR = "vector"; 167 168 private static final int LINECAP_BUTT = 0; 169 private static final int LINECAP_ROUND = 1; 170 private static final int LINECAP_SQUARE = 2; 171 172 private static final int LINEJOIN_MITER = 0; 173 private static final int LINEJOIN_ROUND = 1; 174 private static final int LINEJOIN_BEVEL = 2; 175 176 private static final int DEFAULT_DURATION = 1000; 177 private static final long DEFAULT_INFINITE_DURATION = 60 * 60 * 1000; 178 179 private final VectorDrawableState mVectorState; 180 181 private int mAlpha = 0xFF; 182 183 public VectorDrawable() { 184 mVectorState = new VectorDrawableState(null); 185 } 186 187 private VectorDrawable(VectorDrawableState state, Resources res, Theme theme) { 188 mVectorState = new VectorDrawableState(state); 189 190 if (theme != null && canApplyTheme()) { 191 applyTheme(theme); 192 } 193 } 194 195 @Override 196 public ConstantState getConstantState() { 197 return mVectorState; 198 } 199 200 @Override 201 public void draw(Canvas canvas) { 202 final int saveCount = canvas.save(); 203 final Rect bounds = getBounds(); 204 canvas.translate(bounds.left, bounds.top); 205 mVectorState.mVAnimatedPath.draw(canvas, bounds.width(), bounds.height()); 206 canvas.restoreToCount(saveCount); 207 } 208 209 @Override 210 public void setAlpha(int alpha) { 211 // TODO correct handling of transparent 212 if (mAlpha != alpha) { 213 mAlpha = alpha; 214 invalidateSelf(); 215 } 216 } 217 218 @Override 219 public void setColorFilter(ColorFilter colorFilter) { 220 // TODO: support color filter 221 } 222 223 @Override 224 public int getOpacity() { 225 return PixelFormat.TRANSLUCENT; 226 } 227 228 /** 229 * Sets padding for this shape, defined by a Rect object. Define the padding 230 * in the Rect object as: left, top, right, bottom. 231 */ 232 public void setPadding(Rect padding) { 233 setPadding(padding.left, padding.top, padding.right, padding.bottom); 234 } 235 236 /** 237 * Sets padding for the shape. 238 * 239 * @param left padding for the left side (in pixels) 240 * @param top padding for the top (in pixels) 241 * @param right padding for the right side (in pixels) 242 * @param bottom padding for the bottom (in pixels) 243 */ 244 public void setPadding(int left, int top, int right, int bottom) { 245 if ((left | top | right | bottom) == 0) { 246 mVectorState.mPadding = null; 247 } else { 248 if (mVectorState.mPadding == null) { 249 mVectorState.mPadding = new Rect(); 250 } 251 mVectorState.mPadding.set(left, top, right, bottom); 252 } 253 invalidateSelf(); 254 } 255 256 @Override 257 public int getIntrinsicWidth() { 258 return (int) mVectorState.mVAnimatedPath.mBaseWidth; 259 } 260 261 @Override 262 public int getIntrinsicHeight() { 263 return (int) mVectorState.mVAnimatedPath.mBaseHeight; 264 } 265 266 @Override 267 public boolean getPadding(Rect padding) { 268 if (mVectorState.mPadding != null) { 269 padding.set(mVectorState.mPadding); 270 return true; 271 } else { 272 return super.getPadding(padding); 273 } 274 } 275 276 @Override 277 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) 278 throws XmlPullParserException, IOException { 279 final VAnimatedPath p = inflateInternal(res, parser, attrs, theme); 280 setAnimatedPath(p); 281 } 282 283 @Override 284 public boolean canApplyTheme() { 285 return super.canApplyTheme() || mVectorState != null && mVectorState.canApplyTheme(); 286 } 287 288 @Override 289 public void applyTheme(Theme t) { 290 super.applyTheme(t); 291 292 final VectorDrawableState state = mVectorState; 293 final VAnimatedPath path = state.mVAnimatedPath; 294 if (path != null && path.canApplyTheme()) { 295 path.applyTheme(t); 296 } 297 } 298 299 /** @hide */ 300 public static VectorDrawable create(Resources resources, int rid) { 301 try { 302 final XmlPullParser xpp = resources.getXml(rid); 303 final AttributeSet attrs = Xml.asAttributeSet(xpp); 304 final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 305 factory.setNamespaceAware(true); 306 307 final VectorDrawable drawable = new VectorDrawable(); 308 drawable.inflate(resources, xpp, attrs); 309 310 return drawable; 311 } catch (XmlPullParserException e) { 312 Log.e(LOGTAG, "parser error", e); 313 } catch (IOException e) { 314 Log.e(LOGTAG, "parser error", e); 315 } 316 return null; 317 } 318 319 private VAnimatedPath inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs, 320 Theme theme) throws XmlPullParserException, IOException { 321 final VAnimatedPath animatedPath = new VAnimatedPath(); 322 323 boolean noSizeTag = true; 324 boolean noViewportTag = true; 325 boolean noGroupTag = true; 326 boolean noPathTag = true; 327 328 VGroup currentGroup = null; 329 330 int eventType = parser.getEventType(); 331 while (eventType != XmlPullParser.END_DOCUMENT) { 332 if (eventType == XmlPullParser.START_TAG) { 333 final String tagName = parser.getName(); 334 if (SHAPE_PATH.equals(tagName)) { 335 final VPath path = new VPath(); 336 path.inflate(res, attrs, theme); 337 currentGroup.add(path); 338 noPathTag = false; 339 } else if (SHAPE_ANIMATION.equals(tagName)) { 340 final VAnimation anim = new VAnimation(); 341 anim.inflate(animatedPath.mGroupList, res, attrs, theme); 342 animatedPath.addAnimation(anim); 343 } else if (SHAPE_SIZE.equals(tagName)) { 344 animatedPath.parseSize(res, attrs); 345 noSizeTag = false; 346 } else if (SHAPE_VIEWPORT.equals(tagName)) { 347 animatedPath.parseViewport(res, attrs); 348 noViewportTag = false; 349 } else if (SHAPE_GROUP.equals(tagName)) { 350 currentGroup = new VGroup(); 351 animatedPath.mGroupList.add(currentGroup); 352 noGroupTag = false; 353 } else if (SHAPE_VECTOR.equals(tagName)) { 354 final TypedArray a = res.obtainAttributes(attrs, R.styleable.VectorDrawable); 355 animatedPath.setTrigger(a.getInteger(R.styleable.VectorDrawable_trigger, 0)); 356 357 // Parsing the version information. 358 // Right now, we only support version "1". 359 // If the xml didn't specify the version number, the default 360 // version is "1". 361 final int versionCode = a.getInt(R.styleable.VectorDrawable_versionCode, 1); 362 if (versionCode != 1) { 363 throw new IllegalArgumentException( 364 "So far, VectorDrawable only support version 1"); 365 } 366 367 a.recycle(); 368 } 369 } 370 371 eventType = parser.next(); 372 } 373 374 if (noSizeTag || noViewportTag || noGroupTag || noPathTag) { 375 final StringBuffer tag = new StringBuffer(); 376 377 if (noSizeTag) { 378 tag.append(SHAPE_SIZE); 379 } 380 381 if (noViewportTag) { 382 if (tag.length() > 0) { 383 tag.append(" & "); 384 } 385 tag.append(SHAPE_SIZE); 386 } 387 388 if (noGroupTag) { 389 if (tag.length() > 0) { 390 tag.append(" & "); 391 } 392 tag.append(SHAPE_GROUP); 393 } 394 395 if (noPathTag) { 396 if (tag.length() > 0) { 397 tag.append(" or "); 398 } 399 tag.append(SHAPE_PATH); 400 } 401 402 throw new XmlPullParserException("no " + tag + " defined"); 403 } 404 405 // post parse cleanup 406 animatedPath.parseFinish(); 407 return animatedPath; 408 } 409 410 private void setAnimatedPath(VAnimatedPath animatedPath) { 411 mVectorState.mVAnimatedPath = animatedPath; 412 } 413 414 private static class VectorDrawableState extends ConstantState { 415 int mChangingConfigurations; 416 VAnimatedPath mVAnimatedPath; 417 Rect mPadding; 418 419 public VectorDrawableState(VectorDrawableState copy) { 420 if (copy != null) { 421 mChangingConfigurations = copy.mChangingConfigurations; 422 mVAnimatedPath = new VAnimatedPath(copy.mVAnimatedPath); 423 mPadding = new Rect(copy.mPadding); 424 } 425 } 426 427 @Override 428 public Drawable newDrawable() { 429 return new VectorDrawable(this, null, null); 430 } 431 432 @Override 433 public Drawable newDrawable(Resources res) { 434 return new VectorDrawable(this, res, null); 435 } 436 437 @Override 438 public Drawable newDrawable(Resources res, Theme theme) { 439 return new VectorDrawable(this, res, theme); 440 } 441 442 @Override 443 public int getChangingConfigurations() { 444 return mChangingConfigurations; 445 } 446 } 447 448 private static class VAnimatedPath { 449 private static final int [] TRIGGER_MAP = { 450 0, 451 R.attr.state_pressed, 452 R.attr.state_focused, 453 R.attr.state_hovered, 454 R.attr.state_selected, 455 R.attr.state_checkable, 456 R.attr.state_checked, 457 R.attr.state_activated, 458 R.attr.state_focused 459 }; 460 461 private final Path mPath = new Path(); 462 private final Path mRenderPath = new Path(); 463 private final Matrix mMatrix = new Matrix(); 464 465 private ArrayList<VAnimation> mCurrentAnimList; 466 private VPath[] mCurrentPaths; 467 private Paint mStrokePaint; 468 private Paint mFillPaint; 469 private PathMeasure mPathMeasure; 470 471 private int[] mCurrentState = new int[0]; 472 private float mAnimationValue; 473 private long mTotalDuration; 474 private int mTrigger; 475 private boolean mTriggerState; 476 477 final ArrayList<VGroup> mGroupList = new ArrayList<VGroup>(); 478 479 float mBaseWidth = 1; 480 float mBaseHeight = 1; 481 float mViewportWidth; 482 float mViewportHeight; 483 484 public VAnimatedPath() { 485 } 486 487 public VAnimatedPath(VAnimatedPath copy) { 488 mCurrentAnimList = new ArrayList<VAnimation>(copy.mCurrentAnimList); 489 mGroupList.addAll(copy.mGroupList); 490 if (copy.mCurrentPaths != null) { 491 mCurrentPaths = new VPath[copy.mCurrentPaths.length]; 492 for (int i = 0; i < mCurrentPaths.length; i++) { 493 mCurrentPaths[i] = new VPath(copy.mCurrentPaths[i]); 494 } 495 } 496 mAnimationValue = copy.mAnimationValue; // time goes from 0 to 1 497 498 mBaseWidth = copy.mBaseWidth; 499 mBaseHeight = copy.mBaseHeight; 500 mViewportWidth = copy.mViewportHeight; 501 mViewportHeight = copy.mViewportHeight; 502 mTotalDuration = copy.mTotalDuration; 503 mTrigger = copy.mTrigger; 504 mCurrentState = new int[0]; 505 } 506 507 public boolean canApplyTheme() { 508 final ArrayList<VGroup> groups = mGroupList; 509 for (int i = groups.size() - 1; i >= 0; i--) { 510 final ArrayList<VPath> paths = groups.get(i).mVGList; 511 for (int j = paths.size() - 1; j >= 0; j--) { 512 final VPath path = paths.get(j); 513 if (path.canApplyTheme()) { 514 return true; 515 } 516 } 517 } 518 519 final ArrayList<VAnimation> anims = mCurrentAnimList; 520 for (int i = anims.size() - 1; i >= 0; i--) { 521 final VAnimation anim = anims.get(i); 522 if (anim.canApplyTheme()) { 523 return true; 524 } 525 } 526 527 return false; 528 } 529 530 public void applyTheme(Theme t) { 531 final ArrayList<VGroup> groups = mGroupList; 532 for (int i = groups.size() - 1; i >= 0; i--) { 533 final ArrayList<VPath> paths = groups.get(i).mVGList; 534 for (int j = paths.size() - 1; j >= 0; j--) { 535 final VPath path = paths.get(j); 536 if (path.canApplyTheme()) { 537 path.applyTheme(t); 538 } 539 } 540 } 541 542 final ArrayList<VAnimation> anims = mCurrentAnimList; 543 for (int i = anims.size() - 1; i >= 0; i--) { 544 final VAnimation anim = anims.get(i); 545 if (anim.canApplyTheme()) { 546 anim.applyTheme(t); 547 } 548 } 549 } 550 551 public void setTrigger(int trigger){ 552 mTrigger = VAnimatedPath.getStateForTrigger(trigger); 553 } 554 555 public long getTotalAnimationDuration() { 556 mTotalDuration = 0; 557 int size = mCurrentAnimList.size(); 558 for (int i = 0; i < size; i++) { 559 VAnimation vAnimation = mCurrentAnimList.get(i); 560 long t = vAnimation.getTotalDuration(); 561 if (t == -1) { 562 mTotalDuration = -1; 563 return -1; 564 } 565 mTotalDuration = Math.max(mTotalDuration, t); 566 } 567 568 return mTotalDuration; 569 } 570 571 public float getValue() { 572 return mAnimationValue; 573 } 574 575 /** 576 * @param value the point along the animations to show typically between 0.0f and 1.0f 577 * @return true if you need to keep repeating 578 */ 579 public boolean setAnimationFraction(float value) { 580 getTotalAnimationDuration(); 581 582 long animationTime = (long) (value * mTotalDuration); 583 584 final int len = mCurrentPaths.length; 585 for (int i = 0; i < len; i++) { 586 animationTime = 587 (long) ((mTotalDuration == -1) ? value * 1000 : mTotalDuration * value); 588 589 final VPath path = mCurrentPaths[i]; 590 final int size = mCurrentAnimList.size(); 591 for (int j = 0; j < size; j++) { 592 final VAnimation vAnimation = mCurrentAnimList.get(j); 593 if (vAnimation.doesAdjustPath(path)) { 594 mCurrentPaths[i] = vAnimation.getPathAtTime(animationTime, path); 595 } 596 } 597 } 598 599 mAnimationValue = value; 600 601 if (mTotalDuration == -1) { 602 return true; 603 } else { 604 return animationTime < mTotalDuration; 605 } 606 } 607 608 public void draw(Canvas canvas, int w, int h) { 609 if (mCurrentPaths == null) { 610 Log.e(LOGTAG,"mCurrentPaths == null"); 611 return; 612 } 613 614 for (int i = 0; i < mCurrentPaths.length; i++) { 615 if (mCurrentPaths[i] != null && mCurrentPaths[i].isVisible(mCurrentState)) { 616 drawPath(mCurrentPaths[i], canvas, w, h); 617 } 618 } 619 } 620 621 private void drawPath(VPath vPath, Canvas canvas, int w, int h) { 622 final float scale = Math.min(h / mViewportHeight, w / mViewportWidth); 623 624 vPath.toPath(mPath); 625 final Path path = mPath; 626 627 if (vPath.mTrimPathStart != 0.0f || vPath.mTrimPathEnd != 1.0f) { 628 float start = (vPath.mTrimPathStart + vPath.mTrimPathOffset) % 1.0f; 629 float end = (vPath.mTrimPathEnd + vPath.mTrimPathOffset) % 1.0f; 630 631 if (mPathMeasure == null) { 632 mPathMeasure = new PathMeasure(); 633 } 634 mPathMeasure.setPath(mPath, false); 635 636 float len = mPathMeasure.getLength(); 637 start = start * len; 638 end = end * len; 639 path.reset(); 640 if (start > end) { 641 mPathMeasure.getSegment(start, len, path, true); 642 mPathMeasure.getSegment(0f, end, path, true); 643 } else { 644 mPathMeasure.getSegment(start, end, path, true); 645 } 646 path.rLineTo(0, 0); // fix bug in measure 647 } 648 649 mRenderPath.reset(); 650 mMatrix.reset(); 651 652 mMatrix.postRotate(vPath.mRotate, vPath.mPivotX, vPath.mPivotY); 653 mMatrix.postScale(scale, scale, mViewportWidth / 2f, mViewportHeight / 2f); 654 mMatrix.postTranslate(w / 2f - mViewportWidth / 2f, h / 2f - mViewportHeight / 2f); 655 656 mRenderPath.addPath(path, mMatrix); 657 658 if (vPath.mClip) { 659 canvas.clipPath(mRenderPath, Region.Op.REPLACE); 660 } 661 662 if (vPath.mFillColor != 0) { 663 if (mFillPaint == null) { 664 mFillPaint = new Paint(); 665 mFillPaint.setStyle(Paint.Style.FILL); 666 mFillPaint.setAntiAlias(true); 667 } 668 669 mFillPaint.setColor(vPath.mFillColor); 670 canvas.drawPath(mRenderPath, mFillPaint); 671 } 672 673 if (vPath.mStrokeColor != 0) { 674 if (mStrokePaint == null) { 675 mStrokePaint = new Paint(); 676 mStrokePaint.setStyle(Paint.Style.STROKE); 677 mStrokePaint.setAntiAlias(true); 678 } 679 680 final Paint strokePaint = mStrokePaint; 681 if (vPath.mStrokeLineJoin != null) { 682 strokePaint.setStrokeJoin(vPath.mStrokeLineJoin); 683 } 684 685 if (vPath.mStrokeLineCap != null) { 686 strokePaint.setStrokeCap(vPath.mStrokeLineCap); 687 } 688 689 strokePaint.setStrokeMiter(vPath.mStrokeMiterlimit * scale); 690 strokePaint.setColor(vPath.mStrokeColor); 691 strokePaint.setStrokeWidth(vPath.mStrokeWidth * scale); 692 canvas.drawPath(mRenderPath, strokePaint); 693 } 694 } 695 696 /** 697 * Ensure there is at least one animation for every path in group (linking them by names) 698 * Build the "current" path based on the first group 699 * TODO: improve memory use & performance or move to C++ 700 */ 701 public void parseFinish() { 702 final HashMap<String, VAnimation> newAnimations = new HashMap<String, VAnimation>(); 703 for (VGroup group : mGroupList) { 704 for (VPath vPath : group.getPaths()) { 705 if (!vPath.mAnimated) { 706 VAnimation ap = null; 707 708 if (!newAnimations.containsKey(vPath.getID())) { 709 newAnimations.put(vPath.getID(), ap = new VAnimation()); 710 } else { 711 ap = newAnimations.get(vPath.getID()); 712 } 713 714 ap.addPath(vPath); 715 vPath.mAnimated = true; 716 } 717 } 718 } 719 720 if (mCurrentAnimList == null) { 721 mCurrentAnimList = new ArrayList<VectorDrawable.VAnimation>(); 722 } 723 mCurrentAnimList.addAll(newAnimations.values()); 724 725 final Collection<VPath> paths = mGroupList.get(0).getPaths(); 726 mCurrentPaths = paths.toArray(new VPath[paths.size()]); 727 for (int i = 0; i < mCurrentPaths.length; i++) { 728 mCurrentPaths[i] = new VPath(mCurrentPaths[i]); 729 } 730 } 731 732 public void setState(int[] state) { 733 mCurrentState = Arrays.copyOf(state, state.length); 734 } 735 736 int getTrigger(int []state){ 737 if (mTrigger == 0) return 0; 738 for (int i = 0; i < state.length; i++) { 739 if (state[i] == mTrigger){ 740 if (mTriggerState) 741 return 0; 742 mTriggerState = true; 743 return 1; 744 } 745 } 746 if (mTriggerState) { 747 mTriggerState = false; 748 return -1; 749 } 750 return 0; 751 } 752 753 public void addAnimation(VAnimation anim) { 754 if (mCurrentAnimList == null) { 755 mCurrentAnimList = new ArrayList<VectorDrawable.VAnimation>(); 756 } 757 mCurrentAnimList.add(anim); 758 } 759 760 private void parseViewport(Resources r, AttributeSet attrs) 761 throws XmlPullParserException { 762 final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableViewport); 763 mViewportWidth = a.getFloat(R.styleable.VectorDrawableViewport_viewportWidth, 0); 764 mViewportHeight = a.getFloat(R.styleable.VectorDrawableViewport_viewportHeight, 0); 765 if (mViewportWidth == 0 || mViewportHeight == 0) { 766 throw new XmlPullParserException(a.getPositionDescription()+ 767 "<viewport> tag requires viewportWidth & viewportHeight to be set"); 768 } 769 a.recycle(); 770 } 771 772 private void parseSize(Resources r, AttributeSet attrs) 773 throws XmlPullParserException { 774 final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableSize); 775 mBaseWidth = a.getDimension(R.styleable.VectorDrawableSize_width, 0); 776 mBaseHeight = a.getDimension(R.styleable.VectorDrawableSize_height, 0); 777 if (mBaseWidth == 0 || mBaseHeight == 0) { 778 throw new XmlPullParserException(a.getPositionDescription()+ 779 "<size> tag requires width & height to be set"); 780 } 781 a.recycle(); 782 } 783 784 private static final int getStateForTrigger(int trigger) { 785 return TRIGGER_MAP[trigger]; 786 } 787 } 788 789 private static class VAnimation { 790 private static final String SEPARATOR = ","; 791 792 private static final int DIRECTION_FORWARD = 0; 793 private static final int DIRECTION_IN_AND_OUT = 1; 794 795 public enum Style { 796 INTERPOLATE, CROSSFADE, WIPE 797 } 798 799 private final HashSet<String> mSeqMap = new HashSet<String>(); 800 801 private Interpolator mAnimInterpolator = new AccelerateDecelerateInterpolator(); 802 private VPath[] mPaths = new VPath[0]; 803 private long[] mDuration = { DEFAULT_DURATION }; 804 805 private int[] mThemeAttrs; 806 private Style mStyle; 807 private int mLimitProperty = 0; 808 private long mStartOffset; 809 private long mRepeat = 1; 810 private long mWipeDirection; 811 private int mMode = DIRECTION_FORWARD; 812 private int mInterpolatorType; 813 private String mId; 814 815 public VAnimation() { 816 // Empty constructor. 817 } 818 819 public void inflate(ArrayList<VGroup> groups, Resources r, AttributeSet attrs, Theme theme) 820 throws XmlPullParserException { 821 String value; 822 String[] sp; 823 int name; 824 825 final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableAnimation); 826 final int[] themeAttrs = a.extractThemeAttrs(); 827 mThemeAttrs = themeAttrs; 828 829 value = a.getString(R.styleable.VectorDrawableAnimation_sequence); 830 if (value != null) { 831 sp = value.split(SEPARATOR); 832 final VectorDrawable.VPath[] paths = new VectorDrawable.VPath[sp.length]; 833 834 for (int j = 0; j < sp.length; j++) { 835 mSeqMap.add(sp[j].trim()); 836 837 final VectorDrawable.VPath path = groups.get(j).get(sp[j]); 838 if (path == null) { 839 throw new XmlPullParserException(a.getPositionDescription() 840 + " missing path with name: " + sp[j]); 841 } 842 843 path.mAnimated = true; 844 paths[j] = path; 845 } 846 847 setPaths(paths); 848 } 849 850 name = R.styleable.VectorDrawableAnimation_durations; 851 value = a.getString(name); 852 if (value != null) { 853 long totalDuration = 0; 854 sp = value.split(SEPARATOR); 855 856 final long[] dur = new long[sp.length]; 857 for (int j = 0; j < dur.length; j++) { 858 dur[j] = Long.parseLong(sp[j]); 859 totalDuration += dur[j]; 860 } 861 862 if (totalDuration == 0){ 863 throw new XmlPullParserException(a.getPositionDescription() 864 + " total duration must not be zero"); 865 } 866 867 setDuration(dur); 868 } 869 870 setLimitProperty(a.getInt(R.styleable.VectorDrawableAnimation_limitTo, 0)); 871 setRepeat(a.getInt(R.styleable.VectorDrawableAnimation_repeatCount, 1)); 872 setStartOffset(a.getInt(R.styleable.VectorDrawableAnimation_startDelay, 0)); 873 setMode(a.getInt(R.styleable.VectorDrawableAnimation_repeatStyle, 0)); 874 875 fixMissingParameters(); 876 877 a.recycle(); 878 } 879 880 public boolean canApplyTheme() { 881 return mThemeAttrs != null; 882 } 883 884 public void applyTheme(Theme t) { 885 // TODO: Apply theme. 886 } 887 888 public boolean doesAdjustPath(VPath path) { 889 return mSeqMap.contains(path.getID()); 890 } 891 892 public String getId() { 893 if (mId == null) { 894 mId = mPaths[0].getID(); 895 for (int i = 1; i < mPaths.length; i++) { 896 mId += mPaths[i].getID(); 897 } 898 } 899 return mId; 900 } 901 902 public String getPathName() { 903 return mPaths[0].getID(); 904 } 905 906 public Style getStyle() { 907 return mStyle; 908 } 909 910 public void setStyle(Style style) { 911 mStyle = style; 912 } 913 914 public int getLimitProperty() { 915 return mLimitProperty; 916 } 917 918 public void setLimitProperty(int limitProperty) { 919 mLimitProperty = limitProperty; 920 } 921 922 public long[] getDuration() { 923 return mDuration; 924 } 925 926 public void setDuration(long[] duration) { 927 mDuration = duration; 928 } 929 930 public long getRepeat() { 931 return mRepeat; 932 } 933 934 public void setRepeat(long repeat) { 935 mRepeat = repeat; 936 } 937 938 public long getStartOffset() { 939 return mStartOffset; 940 } 941 942 public void setStartOffset(long startOffset) { 943 mStartOffset = startOffset; 944 } 945 946 public long getWipeDirection() { 947 return mWipeDirection; 948 } 949 950 public void setWipeDirection(long wipeDirection) { 951 mWipeDirection = wipeDirection; 952 } 953 954 public int getMode() { 955 return mMode; 956 } 957 958 public void setMode(int mode) { 959 mMode = mode; 960 } 961 962 public int getInterpolator() { 963 return mInterpolatorType; 964 } 965 966 public void setInterpolator(int interpolator) { 967 mInterpolatorType = interpolator; 968 } 969 970 /** 971 * compute the total time in milliseconds 972 * 973 * @return the total time in milliseconds the animation will take 974 */ 975 public long getTotalDuration() { 976 long total = mStartOffset; 977 if (getRepeat() == -1) { 978 return -1; 979 } 980 for (int i = 0; i < mDuration.length; i++) { 981 if (mRepeat > 1) { 982 total += mDuration[i] * mRepeat; 983 } else { 984 total += mDuration[i]; 985 } 986 } 987 return total; 988 } 989 990 public void setPaths(VPath[] paths) { 991 mPaths = paths; 992 } 993 994 public void addPath(VPath path) { 995 mPaths = Arrays.copyOf(mPaths, mPaths.length + 1); 996 mPaths[mPaths.length - 1] = path; 997 } 998 999 public boolean containsPath(String pathid) { 1000 for (int i = 0; i < mPaths.length; i++) { 1001 if (mPaths[i].getID().equals(pathid)) { 1002 return true; 1003 } 1004 } 1005 return false; 1006 } 1007 1008 public void interpolate(VPath p1, VPath p2, float time, VPath dest) { 1009 VPath.interpolate(time, p1, p2, dest, mLimitProperty); 1010 } 1011 1012 public VPath getPathAtTime(long milliseconds, VPath dest) { 1013 if (mPaths.length == 1) { 1014 dest.copyFrom(mPaths[0]); 1015 return dest; 1016 } 1017 long point = milliseconds - mStartOffset; 1018 if (point < 0) { 1019 point = 0; 1020 } 1021 float time = 0; 1022 long sum = mDuration[0]; 1023 for (int i = 1; i < mDuration.length; i++) { 1024 sum += mDuration[i]; 1025 } 1026 1027 if (mRepeat > 1) { 1028 time = point / (float) (sum * mRepeat); 1029 time = mAnimInterpolator.getInterpolation(time); 1030 1031 if (mMode == DIRECTION_IN_AND_OUT) { 1032 point = ((long) (time * sum * 2 * mRepeat)) % (sum * 2); 1033 if (point > sum) { 1034 point = sum * 2 - point; 1035 } 1036 } else { 1037 point = ((long) (time * sum * mRepeat)) % sum; 1038 } 1039 } else if (mRepeat == 1) { 1040 time = point / (float) (sum * mRepeat); 1041 time = mAnimInterpolator.getInterpolation(time); 1042 if (mMode == DIRECTION_IN_AND_OUT) { 1043 point = ((long) (time * sum * 2 * mRepeat)); 1044 if (point > sum) { 1045 point = sum * 2 - point; 1046 } 1047 } else { 1048 point = Math.min(((long) (time * sum * mRepeat)), sum); 1049 } 1050 1051 } else { // repeat = -1 1052 if (mMode == DIRECTION_IN_AND_OUT) { 1053 point = point % (sum * 2); 1054 if (point > sum) { 1055 point = sum * 2 - point; 1056 } 1057 time = point / (float) sum; 1058 } else { 1059 point = point % sum; 1060 time = point / (float) sum; 1061 } 1062 } 1063 1064 int transition = 0; 1065 while (point > mDuration[transition]) { 1066 point -= mDuration[transition++]; 1067 } 1068 if (mPaths.length > (transition + 1)) { 1069 if (mPaths[transition].getID() != dest.getID()) { 1070 dest.copyFrom(mPaths[transition]); 1071 } 1072 interpolate(mPaths[transition], mPaths[transition + 1], 1073 point / (float) mDuration[transition], dest); 1074 } else { 1075 interpolate(mPaths[transition], mPaths[transition], 0, dest); 1076 } 1077 return dest; 1078 } 1079 1080 void fixMissingParameters() { 1081 // fix missing points 1082 float rotation = Float.NaN; 1083 float rotationY = Float.NaN; 1084 float rotationX = Float.NaN; 1085 for (int i = 0; i < mPaths.length; i++) { 1086 if (mPaths[i].mPivotX > 0) { 1087 rotationX = mPaths[i].mPivotX; 1088 } 1089 if (mPaths[i].mPivotY > 0) { 1090 rotationY = mPaths[i].mPivotY; 1091 } 1092 if (mPaths[i].mRotate > 0) { 1093 rotation = mPaths[i].mRotate; 1094 } 1095 } 1096 if (rotation > 0) { 1097 for (int i = 0; i < mPaths.length; i++) { 1098 if (mPaths[i].mPivotX == 0) { 1099 mPaths[i].mPivotX = rotationX; 1100 } 1101 if (mPaths[i].mPivotY == 0) { 1102 mPaths[i].mPivotY = rotationY; 1103 } 1104 } 1105 } 1106 } 1107 } 1108 1109 private static class VGroup { 1110 private final HashMap<String, VPath> mVGPathMap = new HashMap<String, VPath>(); 1111 private final ArrayList<VPath> mVGList = new ArrayList<VPath>(); 1112 1113 public void add(VPath path) { 1114 String id = path.getID(); 1115 mVGPathMap.put(id, path); 1116 mVGList.add(path); 1117 } 1118 1119 public VPath get(String name) { 1120 return mVGPathMap.get(name); 1121 } 1122 1123 /** 1124 * Must return in order of adding 1125 * @return ordered list of paths 1126 */ 1127 public Collection<VPath> getPaths() { 1128 return mVGList; 1129 } 1130 1131 public int size() { 1132 return mVGPathMap.size(); 1133 } 1134 } 1135 1136 private static class VPath { 1137 private static final int LIMIT_ALL = 0; 1138 private static final int LIMIT_PATH = 1; 1139 private static final int LIMIT_ROTATE = 2; 1140 private static final int LIMIT_TRIM_PATH_START = 3; 1141 private static final int LIMIT_TRIM_PATH_OFFSET = 5; 1142 private static final int LIMIT_TRIM_PATH_END = 4; 1143 1144 private static final int STATE_UNDEFINED=0; 1145 private static final int STATE_TRUE=1; 1146 private static final int STATE_FALSE=2; 1147 1148 private static final int MAX_STATES = 10; 1149 1150 private int[] mThemeAttrs; 1151 1152 int mStrokeColor = 0; 1153 float mStrokeWidth = 0; 1154 float mStrokeOpacity = Float.NaN; 1155 1156 int mFillColor = 0; 1157 int mFillRule; 1158 float mFillOpacity = Float.NaN; 1159 1160 float mRotate = 0; 1161 float mPivotX = 0; 1162 float mPivotY = 0; 1163 1164 float mTrimPathStart = 0; 1165 float mTrimPathEnd = 1; 1166 float mTrimPathOffset = 0; 1167 1168 boolean mAnimated = false; 1169 boolean mClip = false; 1170 Paint.Cap mStrokeLineCap = Paint.Cap.BUTT; 1171 Paint.Join mStrokeLineJoin = Paint.Join.MITER; 1172 float mStrokeMiterlimit = 4; 1173 1174 private VNode[] mNode = null; 1175 private String mId; 1176 private int[] mCheckState = new int[MAX_STATES]; 1177 private boolean[] mCheckValue = new boolean[MAX_STATES]; 1178 private int mNumberOfStates = 0; 1179 private int mNumberOfTrue = 0; 1180 1181 public VPath() { 1182 // Empty constructor. 1183 } 1184 1185 public VPath(VPath p) { 1186 copyFrom(p); 1187 } 1188 1189 public void addStateFilter(int state, boolean condition) { 1190 int k = 0; 1191 while (k < mNumberOfStates) { 1192 if (mCheckState[mNumberOfStates] == state) 1193 break; 1194 k++; 1195 } 1196 mCheckState[k] = state; 1197 mCheckValue[k] = condition; 1198 if (k==mNumberOfStates){ 1199 mNumberOfStates++; 1200 } 1201 if (condition) { 1202 mNumberOfTrue++; 1203 } 1204 } 1205 1206 private int getState(int state){ 1207 for (int i = 0; i < mNumberOfStates; i++) { 1208 if (mCheckState[mNumberOfStates] == state){ 1209 return (mCheckValue[i])?STATE_TRUE:STATE_FALSE; 1210 } 1211 } 1212 return STATE_UNDEFINED; 1213 } 1214 /** 1215 * @return the name of the path 1216 */ 1217 public String getName() { 1218 return mId; 1219 } 1220 1221 public void toPath(Path path) { 1222 path.reset(); 1223 if (mNode != null) { 1224 VNode.createPath(mNode, path); 1225 } 1226 } 1227 1228 public String getID() { 1229 return mId; 1230 } 1231 1232 private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) { 1233 switch (id) { 1234 case LINECAP_BUTT: 1235 return Paint.Cap.BUTT; 1236 case LINECAP_ROUND: 1237 return Paint.Cap.ROUND; 1238 case LINECAP_SQUARE: 1239 return Paint.Cap.SQUARE; 1240 default: 1241 return defValue; 1242 } 1243 } 1244 1245 private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) { 1246 switch (id) { 1247 case LINEJOIN_MITER: 1248 return Paint.Join.MITER; 1249 case LINEJOIN_ROUND: 1250 return Paint.Join.ROUND; 1251 case LINEJOIN_BEVEL: 1252 return Paint.Join.BEVEL; 1253 default: 1254 return defValue; 1255 } 1256 } 1257 1258 public void inflate(Resources r, AttributeSet attrs, Theme theme) { 1259 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawablePath); 1260 final int[] themeAttrs = a.extractThemeAttrs(); 1261 mThemeAttrs = themeAttrs; 1262 1263 // NOTE: The set of attributes loaded here MUST match the 1264 // set of attributes loaded in applyTheme. 1265 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_clipToPath] == 0) { 1266 mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip); 1267 } 1268 1269 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_name] == 0) { 1270 mId = a.getString(R.styleable.VectorDrawablePath_name); 1271 } 1272 1273 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pathData] == 0) { 1274 mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData)); 1275 } 1276 1277 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fill] == 0) { 1278 mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor); 1279 } 1280 1281 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fillOpacity] == 0) { 1282 mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity); 1283 } 1284 1285 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_rotation] == 0) { 1286 mRotate = a.getFloat(R.styleable.VectorDrawablePath_rotation, mRotate); 1287 } 1288 1289 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pivotX] == 0) { 1290 mPivotX = a.getFloat(R.styleable.VectorDrawablePath_pivotX, mPivotX); 1291 } 1292 1293 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pivotY] == 0) { 1294 mPivotY = a.getFloat(R.styleable.VectorDrawablePath_pivotY, mPivotY); 1295 } 1296 1297 if (themeAttrs == null 1298 || themeAttrs[R.styleable.VectorDrawablePath_strokeLineCap] == 0) { 1299 mStrokeLineCap = getStrokeLineCap( 1300 a.getInt(R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap); 1301 } 1302 1303 if (themeAttrs == null 1304 || themeAttrs[R.styleable.VectorDrawablePath_strokeLineJoin] == 0) { 1305 mStrokeLineJoin = getStrokeLineJoin( 1306 a.getInt(R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin); 1307 } 1308 1309 if (themeAttrs == null 1310 || themeAttrs[R.styleable.VectorDrawablePath_strokeMiterLimit] == 0) { 1311 mStrokeMiterlimit = a.getFloat( 1312 R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit); 1313 } 1314 1315 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_stroke] == 0) { 1316 mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor); 1317 } 1318 1319 if (themeAttrs == null 1320 || themeAttrs[R.styleable.VectorDrawablePath_strokeOpacity] == 0) { 1321 mStrokeOpacity = a.getFloat( 1322 R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity); 1323 } 1324 1325 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_strokeWidth] == 0) { 1326 mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth); 1327 } 1328 1329 if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_trimPathEnd] == 0) { 1330 mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd); 1331 } 1332 1333 if (themeAttrs == null 1334 || themeAttrs[R.styleable.VectorDrawablePath_trimPathOffset] == 0) { 1335 mTrimPathOffset = a.getFloat( 1336 R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset); 1337 } 1338 1339 if (themeAttrs == null 1340 || themeAttrs[R.styleable.VectorDrawablePath_trimPathStart] == 0) { 1341 mTrimPathStart = a.getFloat( 1342 R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart); 1343 } 1344 1345 // TODO: Consider replacing this with existing state attributes. 1346 final int[] states = { 1347 R.styleable.VectorDrawablePath_state_activated, 1348 R.styleable.VectorDrawablePath_state_checkable, 1349 R.styleable.VectorDrawablePath_state_checked, 1350 R.styleable.VectorDrawablePath_state_enabled, 1351 R.styleable.VectorDrawablePath_state_focused, 1352 R.styleable.VectorDrawablePath_state_hovered, 1353 R.styleable.VectorDrawablePath_state_pressed, 1354 R.styleable.VectorDrawablePath_state_selected, 1355 R.styleable.VectorDrawablePath_state_window_focused 1356 }; 1357 1358 final int N = states.length; 1359 for (int i = 0; i < N; i++) { 1360 final int state = states[i]; 1361 if (a.hasValue(state)) { 1362 addStateFilter(state, a.getBoolean(state, false)); 1363 } 1364 } 1365 1366 updateColorAlphas(); 1367 1368 a.recycle(); 1369 } 1370 1371 public boolean canApplyTheme() { 1372 return mThemeAttrs != null; 1373 } 1374 1375 public void applyTheme(Theme t) { 1376 if (mThemeAttrs == null) { 1377 return; 1378 } 1379 1380 final TypedArray a = t.resolveAttributes( 1381 mThemeAttrs, R.styleable.VectorDrawablePath, 0, 0); 1382 1383 mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip); 1384 1385 if (a.hasValue(R.styleable.VectorDrawablePath_name)) { 1386 mId = a.getString(R.styleable.VectorDrawablePath_name); 1387 } 1388 1389 if (a.hasValue(R.styleable.VectorDrawablePath_pathData)) { 1390 mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData)); 1391 } 1392 1393 mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor); 1394 mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity); 1395 1396 mRotate = a.getFloat(R.styleable.VectorDrawablePath_rotation, mRotate); 1397 mPivotX = a.getFloat(R.styleable.VectorDrawablePath_pivotX, mPivotX); 1398 mPivotY = a.getFloat(R.styleable.VectorDrawablePath_pivotY, mPivotY); 1399 1400 mStrokeLineCap = getStrokeLineCap(a.getInt( 1401 R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap); 1402 mStrokeLineJoin = getStrokeLineJoin(a.getInt( 1403 R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin); 1404 mStrokeMiterlimit = a.getFloat( 1405 R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit); 1406 mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor); 1407 mStrokeOpacity = a.getFloat( 1408 R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity); 1409 mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth); 1410 1411 mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd); 1412 mTrimPathOffset = a.getFloat( 1413 R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset); 1414 mTrimPathStart = a.getFloat( 1415 R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart); 1416 1417 updateColorAlphas(); 1418 } 1419 1420 private void updateColorAlphas() { 1421 if (!Float.isNaN(mFillOpacity)) { 1422 mFillColor &= 0x00FFFFFF; 1423 mFillColor |= ((int) (0xFF * mFillOpacity)) << 24; 1424 } 1425 1426 if (!Float.isNaN(mStrokeOpacity)) { 1427 mStrokeColor &= 0x00FFFFFF; 1428 mStrokeColor |= ((int) (0xFF * mStrokeOpacity)) << 24; 1429 } 1430 } 1431 1432 private static int nextStart(String s, int end) { 1433 char c; 1434 1435 while (end < s.length()) { 1436 c = s.charAt(end); 1437 if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) { 1438 return end; 1439 } 1440 end++; 1441 } 1442 return end; 1443 } 1444 1445 private void addNode(ArrayList<VectorDrawable.VNode> list, char cmd, float[] val) { 1446 list.add(new VectorDrawable.VNode(cmd, val)); 1447 } 1448 1449 /** 1450 * parse the floats in the string 1451 * this is an optimized version of 1452 * parseFloat(s.split(",|\\s")); 1453 * 1454 * @param s the string containing a command and list of floats 1455 * @return array of floats 1456 */ 1457 private static float[] getFloats(String s) { 1458 if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') { 1459 return new float[0]; 1460 } 1461 try { 1462 float[] tmp = new float[s.length()]; 1463 int count = 0; 1464 int pos = 1, end; 1465 while ((end = extract(s, pos)) >= 0) { 1466 if (pos < end) { 1467 tmp[count++] = Float.parseFloat(s.substring(pos, end)); 1468 } 1469 pos = end + 1; 1470 } 1471 // handle the final float if there is one 1472 if (pos < s.length()) { 1473 tmp[count++] = Float.parseFloat(s.substring(pos, s.length())); 1474 } 1475 return Arrays.copyOf(tmp, count); 1476 } catch (NumberFormatException e){ 1477 Log.e(LOGTAG,"error in parsing \""+s+"\""); 1478 throw e; 1479 } 1480 } 1481 1482 /** 1483 * calculate the position of the next comma or space 1484 * @param s the string to search 1485 * @param start the position to start searching 1486 * @return the position of the next comma or space or -1 if none found 1487 */ 1488 private static int extract(String s, int start) { 1489 int space = s.indexOf(' ', start); 1490 int comma = s.indexOf(',', start); 1491 if (space == -1) { 1492 return comma; 1493 } 1494 if (comma == -1) { 1495 return space; 1496 } 1497 return (comma > space) ? space : comma; 1498 } 1499 1500 private VectorDrawable.VNode[] parsePath(String value) { 1501 int start = 0; 1502 int end = 1; 1503 1504 ArrayList<VectorDrawable.VNode> list = new ArrayList<VectorDrawable.VNode>(); 1505 while (end < value.length()) { 1506 end = nextStart(value, end); 1507 String s = value.substring(start, end); 1508 float[] val = getFloats(s); 1509 addNode(list, s.charAt(0), val); 1510 1511 start = end; 1512 end++; 1513 } 1514 if ((end - start) == 1 && start < value.length()) { 1515 1516 addNode(list, value.charAt(start), new float[0]); 1517 } 1518 return list.toArray(new VectorDrawable.VNode[list.size()]); 1519 } 1520 1521 public void copyFrom(VPath p1) { 1522 mNode = new VNode[p1.mNode.length]; 1523 for (int i = 0; i < mNode.length; i++) { 1524 mNode[i] = new VNode(p1.mNode[i]); 1525 } 1526 mId = p1.mId; 1527 mStrokeColor = p1.mStrokeColor; 1528 mFillColor = p1.mFillColor; 1529 mStrokeWidth = p1.mStrokeWidth; 1530 mRotate = p1.mRotate; 1531 mPivotX = p1.mPivotX; 1532 mPivotY = p1.mPivotY; 1533 mAnimated = p1.mAnimated; 1534 mTrimPathStart = p1.mTrimPathStart; 1535 mTrimPathEnd = p1.mTrimPathEnd; 1536 mTrimPathOffset = p1.mTrimPathOffset; 1537 mStrokeLineCap = p1.mStrokeLineCap; 1538 mStrokeLineJoin = p1.mStrokeLineJoin; 1539 mStrokeMiterlimit = p1.mStrokeMiterlimit; 1540 mNumberOfStates = p1.mNumberOfStates; 1541 for (int i = 0; i < mNumberOfStates; i++) { 1542 mCheckState[i] = p1.mCheckState[i]; 1543 mCheckValue[i] = p1.mCheckValue[i]; 1544 } 1545 1546 mFillRule = p1.mFillRule; 1547 } 1548 1549 public static VPath interpolate(float t, VPath p1, VPath p2, VPath returnPath, int limit) { 1550 if (limit == LIMIT_ALL || limit == LIMIT_PATH) { 1551 if (returnPath.mNode == null || returnPath.mNode.length != p1.mNode.length) { 1552 returnPath.mNode = new VNode[p1.mNode.length]; 1553 } 1554 for (int i = 0; i < returnPath.mNode.length; i++) { 1555 if (returnPath.mNode[i] == null) { 1556 returnPath.mNode[i] = new VNode(p1.mNode[i], p2.mNode[i], t); 1557 } else { 1558 returnPath.mNode[i].interpolate(p1.mNode[i], p2.mNode[i], t); 1559 } 1560 } 1561 } 1562 float t1 = 1 - t; 1563 switch (limit) { 1564 case LIMIT_ALL: 1565 returnPath.mRotate = t1 * p1.mRotate + t * p2.mRotate; 1566 returnPath.mPivotX = t1 * p1.mPivotX + t * p2.mPivotX; 1567 returnPath.mPivotY = t1 * p1.mPivotY + t * p2.mPivotY; 1568 returnPath.mClip = p1.mClip | p2.mClip; 1569 1570 returnPath.mTrimPathStart = t1 * p1.mTrimPathStart + t * p2.mTrimPathStart; 1571 returnPath.mTrimPathEnd = t1 * p1.mTrimPathEnd + t * p2.mTrimPathEnd; 1572 returnPath.mTrimPathOffset = t1 * p1.mTrimPathOffset + t * p2.mTrimPathOffset; 1573 returnPath.mStrokeMiterlimit = 1574 t1 * p1.mStrokeMiterlimit + t * p2.mStrokeMiterlimit; 1575 returnPath.mStrokeLineCap = p1.mStrokeLineCap; 1576 if (returnPath.mStrokeLineCap == null) { 1577 returnPath.mStrokeLineCap = p2.mStrokeLineCap; 1578 } 1579 returnPath.mStrokeLineJoin = p1.mStrokeLineJoin; 1580 if (returnPath.mStrokeLineJoin == null) { 1581 returnPath.mStrokeLineJoin = p2.mStrokeLineJoin; 1582 } 1583 returnPath.mFillRule = p1.mFillRule; 1584 1585 returnPath.mStrokeColor = rgbInterpolate(t, p1.mStrokeColor, p2.mStrokeColor); 1586 returnPath.mFillColor = rgbInterpolate(t, p1.mFillColor, p2.mFillColor); 1587 returnPath.mStrokeWidth = t1 * p1.mStrokeWidth + t * p2.mStrokeWidth; 1588 returnPath.mNumberOfStates = p1.mNumberOfStates; 1589 for (int i = 0; i < returnPath.mNumberOfStates; i++) { 1590 returnPath.mCheckState[i] = p1.mCheckState[i]; 1591 returnPath.mCheckValue[i] = p1.mCheckValue[i]; 1592 } 1593 for (int i = 0; i < p2.mNumberOfStates; i++) { 1594 returnPath.addStateFilter(p2.mCheckState[i], p2.mCheckValue[i]); 1595 } 1596 1597 int count = 0; 1598 for (int i = 0; i < returnPath.mNumberOfStates; i++) { 1599 if (returnPath.mCheckValue[i]) { 1600 count++; 1601 } 1602 } 1603 returnPath.mNumberOfTrue = count; 1604 break; 1605 case LIMIT_ROTATE: 1606 returnPath.mRotate = t1 * p1.mRotate + t * p2.mRotate; 1607 break; 1608 case LIMIT_TRIM_PATH_END: 1609 returnPath.mTrimPathEnd = t1 * p1.mTrimPathEnd + t * p2.mTrimPathEnd; 1610 break; 1611 case LIMIT_TRIM_PATH_OFFSET: 1612 returnPath.mTrimPathOffset = t1 * p1.mTrimPathOffset + t * p2.mTrimPathOffset; 1613 break; 1614 case LIMIT_TRIM_PATH_START: 1615 returnPath.mTrimPathStart = t1 * p1.mTrimPathStart + t * p2.mTrimPathStart; 1616 break; 1617 } 1618 return returnPath; 1619 } 1620 1621 private static int rgbInterpolate(float fraction, int startColor, int endColor) { 1622 if (startColor == endColor) { 1623 return startColor; 1624 } else if (startColor == 0) { 1625 return endColor; 1626 } else if (endColor == 0) { 1627 return startColor; 1628 } 1629 1630 final int startA = (startColor >> 24) & 0xff; 1631 final int startR = (startColor >> 16) & 0xff; 1632 final int startG = (startColor >> 8) & 0xff; 1633 final int startB = startColor & 0xff; 1634 1635 final int endA = (endColor >> 24) & 0xff; 1636 final int endR = (endColor >> 16) & 0xff; 1637 final int endG = (endColor >> 8) & 0xff; 1638 final int endB = endColor & 0xff; 1639 1640 return ((startA + (int)(fraction * (endA - startA))) << 24) | 1641 ((startR + (int)(fraction * (endR - startR))) << 16) | 1642 ((startG + (int)(fraction * (endG - startG))) << 8) | 1643 ((startB + (int)(fraction * (endB - startB)))); 1644 } 1645 1646 public boolean isVisible(int[] state) { 1647 int match = 0; 1648 for (int i = 0; i < state.length; i++) { 1649 int v = getState(state[i]); 1650 if (v != STATE_UNDEFINED) { 1651 if (v==STATE_TRUE) { 1652 match++; 1653 } else { 1654 return false; 1655 } 1656 } 1657 } 1658 return match == mNumberOfTrue; 1659 } 1660 } 1661 1662 private static class VNode { 1663 private char mType; 1664 private float[] mParams; 1665 1666 public VNode(char type, float[] params) { 1667 mType = type; 1668 mParams = params; 1669 } 1670 1671 public VNode(VNode n) { 1672 mType = n.mType; 1673 mParams = Arrays.copyOf(n.mParams, n.mParams.length); 1674 } 1675 1676 public VNode(VNode n1, VNode n2, float t) { 1677 mType = n1.mType; 1678 mParams = new float[n1.mParams.length]; 1679 interpolate(n1, n2, t); 1680 } 1681 1682 private boolean match(VNode n) { 1683 if (n.mType != mType) { 1684 return false; 1685 } 1686 return (mParams.length == n.mParams.length); 1687 } 1688 1689 public void interpolate(VNode n1, VNode n2, float t) { 1690 for (int i = 0; i < n1.mParams.length; i++) { 1691 mParams[i] = n1.mParams[i] * (1 - t) + n2.mParams[i] * t; 1692 } 1693 } 1694 1695 public static void createPath(VNode[] node, Path path) { 1696 float[] current = new float[4]; 1697 char previousCommand = 'm'; 1698 for (int i = 0; i < node.length; i++) { 1699 addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); 1700 previousCommand = node[i].mType; 1701 } 1702 } 1703 1704 private static void addCommand(Path path, float[] current, 1705 char previousCmd, char cmd, float[] val) { 1706 1707 int incr = 2; 1708 float currentX = current[0]; 1709 float currentY = current[1]; 1710 float ctrlPointX = current[2]; 1711 float ctrlPointY = current[3]; 1712 float reflectiveCtrlPointX; 1713 float reflectiveCtrlPointY; 1714 1715 switch (cmd) { 1716 case 'z': 1717 case 'Z': 1718 path.close(); 1719 return; 1720 case 'm': 1721 case 'M': 1722 case 'l': 1723 case 'L': 1724 case 't': 1725 case 'T': 1726 incr = 2; 1727 break; 1728 case 'h': 1729 case 'H': 1730 case 'v': 1731 case 'V': 1732 incr = 1; 1733 break; 1734 case 'c': 1735 case 'C': 1736 incr = 6; 1737 break; 1738 case 's': 1739 case 'S': 1740 case 'q': 1741 case 'Q': 1742 incr = 4; 1743 break; 1744 case 'a': 1745 case 'A': 1746 incr = 7; 1747 break; 1748 } 1749 for (int k = 0; k < val.length; k += incr) { 1750 // TODO: build test to prove all permutations work 1751 switch (cmd) { 1752 case 'm': // moveto - Start a new sub-path (relative) 1753 path.rMoveTo(val[k + 0], val[k + 1]); 1754 currentX += val[k + 0]; 1755 currentY += val[k + 1]; 1756 break; 1757 case 'M': // moveto - Start a new sub-path 1758 path.moveTo(val[k + 0], val[k + 1]); 1759 currentX = val[k + 0]; 1760 currentY = val[k + 1]; 1761 break; 1762 case 'l': // lineto - Draw a line from the current point (relative) 1763 path.rLineTo(val[k + 0], val[k + 1]); 1764 currentX += val[k + 0]; 1765 currentY += val[k + 1]; 1766 break; 1767 case 'L': // lineto - Draw a line from the current point 1768 path.lineTo(val[k + 0], val[k + 1]); 1769 currentX = val[k + 0]; 1770 currentY = val[k + 1]; 1771 break; 1772 case 'z': // closepath - Close the current subpath 1773 case 'Z': // closepath - Close the current subpath 1774 path.close(); 1775 break; 1776 case 'h': // horizontal lineto - Draws a horizontal line (relative) 1777 path.rLineTo(val[k + 0], 0); 1778 currentX += val[k + 0]; 1779 break; 1780 case 'H': // horizontal lineto - Draws a horizontal line 1781 path.lineTo(val[k + 0], currentY); 1782 currentX = val[k + 0]; 1783 break; 1784 case 'v': // vertical lineto - Draws a vertical line from the current point (r) 1785 path.rLineTo(0, val[k + 0]); 1786 currentY += val[k + 0]; 1787 break; 1788 case 'V': // vertical lineto - Draws a vertical line from the current point 1789 path.lineTo(currentX, val[k + 0]); 1790 currentY = val[k + 0]; 1791 break; 1792 case 'c': // curveto - Draws a cubic Bézier curve (relative) 1793 path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], 1794 val[k + 4], val[k + 5]); 1795 1796 ctrlPointX = currentX + val[k + 2]; 1797 ctrlPointY = currentY + val[k + 3]; 1798 currentX += val[k + 4]; 1799 currentY += val[k + 5]; 1800 1801 break; 1802 case 'C': // curveto - Draws a cubic Bézier curve 1803 path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], 1804 val[k + 4], val[k + 5]); 1805 currentX = val[k + 4]; 1806 currentY = val[k + 5]; 1807 ctrlPointX = val[k + 2]; 1808 ctrlPointY = val[k + 3]; 1809 break; 1810 case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) 1811 reflectiveCtrlPointX = 0; 1812 reflectiveCtrlPointY = 0; 1813 if (previousCmd == 'c' || previousCmd == 's' 1814 || previousCmd == 'C' || previousCmd == 'S') { 1815 reflectiveCtrlPointX = currentX - ctrlPointX; 1816 reflectiveCtrlPointY = currentY - ctrlPointY; 1817 } 1818 path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 1819 val[k + 0], val[k + 1], 1820 val[k + 2], val[k + 3]); 1821 1822 ctrlPointX = currentX + val[k + 0]; 1823 ctrlPointY = currentY + val[k + 1]; 1824 currentX += val[k + 2]; 1825 currentY += val[k + 3]; 1826 break; 1827 case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) 1828 reflectiveCtrlPointX = currentX; 1829 reflectiveCtrlPointY = currentY; 1830 if (previousCmd == 'c' || previousCmd == 's' 1831 || previousCmd == 'C' || previousCmd == 'S') { 1832 reflectiveCtrlPointX = 2 * currentX - ctrlPointX; 1833 reflectiveCtrlPointY = 2 * currentY - ctrlPointY; 1834 } 1835 path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 1836 val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 1837 ctrlPointX = val[k + 0]; 1838 ctrlPointY = val[k + 1]; 1839 currentX = val[k + 2]; 1840 currentY = val[k + 3]; 1841 break; 1842 case 'q': // Draws a quadratic Bézier (relative) 1843 path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 1844 ctrlPointX = currentX + val[k + 0]; 1845 ctrlPointY = currentY + val[k + 1]; 1846 currentX += val[k + 2]; 1847 currentY += val[k + 3]; 1848 break; 1849 case 'Q': // Draws a quadratic Bézier 1850 path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 1851 ctrlPointX = val[k + 0]; 1852 ctrlPointY = val[k + 1]; 1853 currentX = val[k + 2]; 1854 currentY = val[k + 3]; 1855 break; 1856 case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) 1857 reflectiveCtrlPointX = 0; 1858 reflectiveCtrlPointY = 0; 1859 if (previousCmd == 'q' || previousCmd == 't' 1860 || previousCmd == 'Q' || previousCmd == 'T') { 1861 reflectiveCtrlPointX = currentX - ctrlPointX; 1862 reflectiveCtrlPointY = currentY - ctrlPointY; 1863 } 1864 path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 1865 val[k + 0], val[k + 1]); 1866 ctrlPointX = currentX + reflectiveCtrlPointX; 1867 ctrlPointY = currentY + reflectiveCtrlPointY; 1868 currentX += val[k + 0]; 1869 currentY += val[k + 1]; 1870 break; 1871 case 'T': // Draws a quadratic Bézier curve (reflective control point) 1872 reflectiveCtrlPointX = currentX; 1873 reflectiveCtrlPointY = currentY; 1874 if (previousCmd == 'q' || previousCmd == 't' 1875 || previousCmd == 'Q' || previousCmd == 'T') { 1876 reflectiveCtrlPointX = 2 * currentX - ctrlPointX; 1877 reflectiveCtrlPointY = 2 * currentY - ctrlPointY; 1878 } 1879 path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 1880 val[k + 0], val[k + 1]); 1881 ctrlPointX = reflectiveCtrlPointX; 1882 ctrlPointY = reflectiveCtrlPointY; 1883 currentX = val[k + 0]; 1884 currentY = val[k + 1]; 1885 break; 1886 case 'a': // Draws an elliptical arc 1887 // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) 1888 drawArc(path, 1889 currentX, 1890 currentY, 1891 val[k + 5] + currentX, 1892 val[k + 6] + currentY, 1893 val[k + 0], 1894 val[k + 1], 1895 val[k + 2], 1896 val[k + 3] != 0, 1897 val[k + 4] != 0); 1898 currentX += val[k + 5]; 1899 currentY += val[k + 6]; 1900 ctrlPointX = currentX; 1901 ctrlPointY = currentY; 1902 break; 1903 case 'A': // Draws an elliptical arc 1904 drawArc(path, 1905 currentX, 1906 currentY, 1907 val[k + 5], 1908 val[k + 6], 1909 val[k + 0], 1910 val[k + 1], 1911 val[k + 2], 1912 val[k + 3] != 0, 1913 val[k + 4] != 0); 1914 currentX = val[k + 5]; 1915 currentY = val[k + 6]; 1916 ctrlPointX = currentX; 1917 ctrlPointY = currentY; 1918 break; 1919 } 1920 previousCmd = cmd; 1921 } 1922 current[0] = currentX; 1923 current[1] = currentY; 1924 current[2] = ctrlPointX; 1925 current[3] = ctrlPointY; 1926 } 1927 1928 private static void drawArc(Path p, 1929 float x0, 1930 float y0, 1931 float x1, 1932 float y1, 1933 float a, 1934 float b, 1935 float theta, 1936 boolean isMoreThanHalf, 1937 boolean isPositiveArc) { 1938 1939 /* Convert rotation angle from degrees to radians */ 1940 double thetaD = Math.toRadians(theta); 1941 /* Pre-compute rotation matrix entries */ 1942 double cosTheta = Math.cos(thetaD); 1943 double sinTheta = Math.sin(thetaD); 1944 /* Transform (x0, y0) and (x1, y1) into unit space */ 1945 /* using (inverse) rotation, followed by (inverse) scale */ 1946 double x0p = (x0 * cosTheta + y0 * sinTheta) / a; 1947 double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; 1948 double x1p = (x1 * cosTheta + y1 * sinTheta) / a; 1949 double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; 1950 1951 /* Compute differences and averages */ 1952 double dx = x0p - x1p; 1953 double dy = y0p - y1p; 1954 double xm = (x0p + x1p) / 2; 1955 double ym = (y0p + y1p) / 2; 1956 /* Solve for intersecting unit circles */ 1957 double dsq = dx * dx + dy * dy; 1958 if (dsq == 0.0) { 1959 Log.w(LOGTAG, " Points are coincident"); 1960 return; /* Points are coincident */ 1961 } 1962 double disc = 1.0 / dsq - 1.0 / 4.0; 1963 if (disc < 0.0) { 1964 Log.w(LOGTAG, "Points are too far apart " + dsq); 1965 float adjust = (float) (Math.sqrt(dsq) / 1.99999); 1966 drawArc(p, x0, y0, x1, y1, a * adjust, 1967 b * adjust, theta, isMoreThanHalf, isPositiveArc); 1968 return; /* Points are too far apart */ 1969 } 1970 double s = Math.sqrt(disc); 1971 double sdx = s * dx; 1972 double sdy = s * dy; 1973 double cx; 1974 double cy; 1975 if (isMoreThanHalf == isPositiveArc) { 1976 cx = xm - sdy; 1977 cy = ym + sdx; 1978 } else { 1979 cx = xm + sdy; 1980 cy = ym - sdx; 1981 } 1982 1983 double eta0 = Math.atan2((y0p - cy), (x0p - cx)); 1984 1985 double eta1 = Math.atan2((y1p - cy), (x1p - cx)); 1986 1987 double sweep = (eta1 - eta0); 1988 if (isPositiveArc != (sweep >= 0)) { 1989 if (sweep > 0) { 1990 sweep -= 2 * Math.PI; 1991 } else { 1992 sweep += 2 * Math.PI; 1993 } 1994 } 1995 1996 cx *= a; 1997 cy *= b; 1998 double tcx = cx; 1999 cx = cx * cosTheta - cy * sinTheta; 2000 cy = tcx * sinTheta + cy * cosTheta; 2001 2002 arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); 2003 } 2004 2005 /** 2006 * Converts an arc to cubic Bezier segments and records them in p. 2007 * 2008 * @param p The target for the cubic Bezier segments 2009 * @param cx The x coordinate center of the ellipse 2010 * @param cy The y coordinate center of the ellipse 2011 * @param a The radius of the ellipse in the horizontal direction 2012 * @param b The radius of the ellipse in the vertical direction 2013 * @param e1x E(eta1) x coordinate of the starting point of the arc 2014 * @param e1y E(eta2) y coordinate of the starting point of the arc 2015 * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane 2016 * @param start The start angle of the arc on the ellipse 2017 * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse 2018 */ 2019 private static void arcToBezier(Path p, 2020 double cx, 2021 double cy, 2022 double a, 2023 double b, 2024 double e1x, 2025 double e1y, 2026 double theta, 2027 double start, 2028 double sweep) { 2029 // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html 2030 // and http://www.spaceroots.org/documents/ellipse/node22.html 2031 2032 // Maximum of 45 degrees per cubic Bezier segment 2033 int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); 2034 2035 double eta1 = start; 2036 double cosTheta = Math.cos(theta); 2037 double sinTheta = Math.sin(theta); 2038 double cosEta1 = Math.cos(eta1); 2039 double sinEta1 = Math.sin(eta1); 2040 double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); 2041 double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); 2042 2043 double anglePerSegment = sweep / numSegments; 2044 for (int i = 0; i < numSegments; i++) { 2045 double eta2 = eta1 + anglePerSegment; 2046 double sinEta2 = Math.sin(eta2); 2047 double cosEta2 = Math.cos(eta2); 2048 double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); 2049 double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); 2050 double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; 2051 double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; 2052 double tanDiff2 = Math.tan((eta2 - eta1) / 2); 2053 double alpha = 2054 Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; 2055 double q1x = e1x + alpha * ep1x; 2056 double q1y = e1y + alpha * ep1y; 2057 double q2x = e2x - alpha * ep2x; 2058 double q2y = e2y - alpha * ep2y; 2059 2060 p.cubicTo((float) q1x, 2061 (float) q1y, 2062 (float) q2x, 2063 (float) q2y, 2064 (float) e2x, 2065 (float) e2y); 2066 eta1 = eta2; 2067 e1x = e2x; 2068 e1y = e2y; 2069 ep1x = ep2x; 2070 ep1y = ep2y; 2071 } 2072 } 2073 2074 } 2075} 2076