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