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