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