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