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