AnimatorInflater.java revision 9032fa5808d7808f54d31d646049d0b1f4a9491b
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package android.animation; 17 18import android.annotation.AnimatorRes; 19import android.content.Context; 20import android.content.res.ConfigurationBoundResourceCache; 21import android.content.res.ConstantState; 22import android.content.res.Resources; 23import android.content.res.Resources.NotFoundException; 24import android.content.res.Resources.Theme; 25import android.content.res.TypedArray; 26import android.content.res.XmlResourceParser; 27import android.graphics.Path; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.util.PathParser; 31import android.util.StateSet; 32import android.util.TypedValue; 33import android.util.Xml; 34import android.view.InflateException; 35import android.view.animation.AnimationUtils; 36import android.view.animation.BaseInterpolator; 37import android.view.animation.Interpolator; 38 39import com.android.internal.R; 40 41import org.xmlpull.v1.XmlPullParser; 42import org.xmlpull.v1.XmlPullParserException; 43 44import java.io.IOException; 45import java.util.ArrayList; 46 47/** 48 * This class is used to instantiate animator XML files into Animator objects. 49 * <p> 50 * For performance reasons, inflation relies heavily on pre-processing of 51 * XML files that is done at build time. Therefore, it is not currently possible 52 * to use this inflater with an XmlPullParser over a plain XML file at runtime; 53 * it only works with an XmlPullParser returned from a compiled resource (R. 54 * <em>something</em> file.) 55 */ 56public class AnimatorInflater { 57 private static final String TAG = "AnimatorInflater"; 58 /** 59 * These flags are used when parsing AnimatorSet objects 60 */ 61 private static final int TOGETHER = 0; 62 private static final int SEQUENTIALLY = 1; 63 64 /** 65 * Enum values used in XML attributes to indicate the value for mValueType 66 */ 67 private static final int VALUE_TYPE_FLOAT = 0; 68 private static final int VALUE_TYPE_INT = 1; 69 private static final int VALUE_TYPE_PATH = 2; 70 private static final int VALUE_TYPE_COLOR = 3; 71 private static final int VALUE_TYPE_UNDEFINED = 4; 72 73 private static final boolean DBG_ANIMATOR_INFLATER = false; 74 75 // used to calculate changing configs for resource references 76 private static final TypedValue sTmpTypedValue = new TypedValue(); 77 78 /** 79 * Loads an {@link Animator} object from a resource 80 * 81 * @param context Application context used to access resources 82 * @param id The resource id of the animation to load 83 * @return The animator object reference by the specified id 84 * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded 85 */ 86 public static Animator loadAnimator(Context context, @AnimatorRes int id) 87 throws NotFoundException { 88 return loadAnimator(context.getResources(), context.getTheme(), id); 89 } 90 91 /** 92 * Loads an {@link Animator} object from a resource 93 * 94 * @param resources The resources 95 * @param theme The theme 96 * @param id The resource id of the animation to load 97 * @return The animator object reference by the specified id 98 * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded 99 * @hide 100 */ 101 public static Animator loadAnimator(Resources resources, Theme theme, int id) 102 throws NotFoundException { 103 return loadAnimator(resources, theme, id, 1); 104 } 105 106 /** @hide */ 107 public static Animator loadAnimator(Resources resources, Theme theme, int id, 108 float pathErrorScale) throws NotFoundException { 109 final ConfigurationBoundResourceCache<Animator> animatorCache = resources 110 .getAnimatorCache(); 111 Animator animator = animatorCache.get(id, theme); 112 if (animator != null) { 113 if (DBG_ANIMATOR_INFLATER) { 114 Log.d(TAG, "loaded animator from cache, " + resources.getResourceName(id)); 115 } 116 return animator; 117 } else if (DBG_ANIMATOR_INFLATER) { 118 Log.d(TAG, "cache miss for animator " + resources.getResourceName(id)); 119 } 120 XmlResourceParser parser = null; 121 try { 122 parser = resources.getAnimation(id); 123 animator = createAnimatorFromXml(resources, theme, parser, pathErrorScale); 124 if (animator != null) { 125 animator.appendChangingConfigurations(getChangingConfigs(resources, id)); 126 final ConstantState<Animator> constantState = animator.createConstantState(); 127 if (constantState != null) { 128 if (DBG_ANIMATOR_INFLATER) { 129 Log.d(TAG, "caching animator for res " + resources.getResourceName(id)); 130 } 131 animatorCache.put(id, theme, constantState); 132 // create a new animator so that cached version is never used by the user 133 animator = constantState.newInstance(resources, theme); 134 } 135 } 136 return animator; 137 } catch (XmlPullParserException ex) { 138 Resources.NotFoundException rnf = 139 new Resources.NotFoundException("Can't load animation resource ID #0x" + 140 Integer.toHexString(id)); 141 rnf.initCause(ex); 142 throw rnf; 143 } catch (IOException ex) { 144 Resources.NotFoundException rnf = 145 new Resources.NotFoundException("Can't load animation resource ID #0x" + 146 Integer.toHexString(id)); 147 rnf.initCause(ex); 148 throw rnf; 149 } finally { 150 if (parser != null) parser.close(); 151 } 152 } 153 154 public static StateListAnimator loadStateListAnimator(Context context, int id) 155 throws NotFoundException { 156 final Resources resources = context.getResources(); 157 final ConfigurationBoundResourceCache<StateListAnimator> cache = resources 158 .getStateListAnimatorCache(); 159 final Theme theme = context.getTheme(); 160 StateListAnimator animator = cache.get(id, theme); 161 if (animator != null) { 162 return animator; 163 } 164 XmlResourceParser parser = null; 165 try { 166 parser = resources.getAnimation(id); 167 animator = createStateListAnimatorFromXml(context, parser, Xml.asAttributeSet(parser)); 168 if (animator != null) { 169 animator.appendChangingConfigurations(getChangingConfigs(resources, id)); 170 final ConstantState<StateListAnimator> constantState = animator 171 .createConstantState(); 172 if (constantState != null) { 173 cache.put(id, theme, constantState); 174 // return a clone so that the animator in constant state is never used. 175 animator = constantState.newInstance(resources, theme); 176 } 177 } 178 return animator; 179 } catch (XmlPullParserException ex) { 180 Resources.NotFoundException rnf = 181 new Resources.NotFoundException( 182 "Can't load state list animator resource ID #0x" + 183 Integer.toHexString(id) 184 ); 185 rnf.initCause(ex); 186 throw rnf; 187 } catch (IOException ex) { 188 Resources.NotFoundException rnf = 189 new Resources.NotFoundException( 190 "Can't load state list animator resource ID #0x" + 191 Integer.toHexString(id) 192 ); 193 rnf.initCause(ex); 194 throw rnf; 195 } finally { 196 if (parser != null) { 197 parser.close(); 198 } 199 } 200 } 201 202 private static StateListAnimator createStateListAnimatorFromXml(Context context, 203 XmlPullParser parser, AttributeSet attributeSet) 204 throws IOException, XmlPullParserException { 205 int type; 206 StateListAnimator stateListAnimator = new StateListAnimator(); 207 208 while (true) { 209 type = parser.next(); 210 switch (type) { 211 case XmlPullParser.END_DOCUMENT: 212 case XmlPullParser.END_TAG: 213 return stateListAnimator; 214 215 case XmlPullParser.START_TAG: 216 // parse item 217 Animator animator = null; 218 if ("item".equals(parser.getName())) { 219 int attributeCount = parser.getAttributeCount(); 220 int[] states = new int[attributeCount]; 221 int stateIndex = 0; 222 for (int i = 0; i < attributeCount; i++) { 223 int attrName = attributeSet.getAttributeNameResource(i); 224 if (attrName == R.attr.animation) { 225 final int animId = attributeSet.getAttributeResourceValue(i, 0); 226 animator = loadAnimator(context, animId); 227 } else { 228 states[stateIndex++] = 229 attributeSet.getAttributeBooleanValue(i, false) ? 230 attrName : -attrName; 231 } 232 } 233 if (animator == null) { 234 animator = createAnimatorFromXml(context.getResources(), 235 context.getTheme(), parser, 1f); 236 } 237 238 if (animator == null) { 239 throw new Resources.NotFoundException( 240 "animation state item must have a valid animation"); 241 } 242 stateListAnimator 243 .addState(StateSet.trimStateSet(states, stateIndex), animator); 244 } 245 break; 246 } 247 } 248 } 249 250 /** 251 * PathDataEvaluator is used to interpolate between two paths which are 252 * represented in the same format but different control points' values. 253 * The path is represented as an array of PathDataNode here, which is 254 * fundamentally an array of floating point numbers. 255 */ 256 private static class PathDataEvaluator implements TypeEvaluator<PathParser.PathDataNode[]> { 257 private PathParser.PathDataNode[] mNodeArray; 258 259 /** 260 * Create a PathParser.PathDataNode[] that does not reuse the animated value. 261 * Care must be taken when using this option because on every evaluation 262 * a new <code>PathParser.PathDataNode[]</code> will be allocated. 263 */ 264 private PathDataEvaluator() {} 265 266 /** 267 * Create a PathDataEvaluator that reuses <code>nodeArray</code> for every evaluate() call. 268 * Caution must be taken to ensure that the value returned from 269 * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or 270 * used across threads. The value will be modified on each <code>evaluate()</code> call. 271 * 272 * @param nodeArray The array to modify and return from <code>evaluate</code>. 273 */ 274 public PathDataEvaluator(PathParser.PathDataNode[] nodeArray) { 275 mNodeArray = nodeArray; 276 } 277 278 @Override 279 public PathParser.PathDataNode[] evaluate(float fraction, 280 PathParser.PathDataNode[] startPathData, 281 PathParser.PathDataNode[] endPathData) { 282 if (!PathParser.canMorph(startPathData, endPathData)) { 283 throw new IllegalArgumentException("Can't interpolate between" 284 + " two incompatible pathData"); 285 } 286 287 if (mNodeArray == null || !PathParser.canMorph(mNodeArray, startPathData)) { 288 mNodeArray = PathParser.deepCopyNodes(startPathData); 289 } 290 291 for (int i = 0; i < startPathData.length; i++) { 292 mNodeArray[i].interpolatePathDataNode(startPathData[i], 293 endPathData[i], fraction); 294 } 295 296 return mNodeArray; 297 } 298 } 299 300 private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType, 301 int valueFromId, int valueToId, String propertyName) { 302 303 TypedValue tvFrom = styledAttributes.peekValue(valueFromId); 304 boolean hasFrom = (tvFrom != null); 305 int fromType = hasFrom ? tvFrom.type : 0; 306 TypedValue tvTo = styledAttributes.peekValue(valueToId); 307 boolean hasTo = (tvTo != null); 308 int toType = hasTo ? tvTo.type : 0; 309 310 if (valueType == VALUE_TYPE_UNDEFINED) { 311 // Check whether it's color type. If not, fall back to default type (i.e. float type) 312 if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) { 313 valueType = VALUE_TYPE_COLOR; 314 } else { 315 valueType = VALUE_TYPE_FLOAT; 316 } 317 } 318 319 boolean getFloats = (valueType == VALUE_TYPE_FLOAT); 320 321 PropertyValuesHolder returnValue = null; 322 323 if (valueType == VALUE_TYPE_PATH) { 324 String fromString = styledAttributes.getString(valueFromId); 325 String toString = styledAttributes.getString(valueToId); 326 PathParser.PathDataNode[] nodesFrom = PathParser.createNodesFromPathData(fromString); 327 PathParser.PathDataNode[] nodesTo = PathParser.createNodesFromPathData(toString); 328 329 if (nodesFrom != null || nodesTo != null) { 330 if (nodesFrom != null) { 331 TypeEvaluator evaluator = 332 new PathDataEvaluator(PathParser.deepCopyNodes(nodesFrom)); 333 if (nodesTo != null) { 334 if (!PathParser.canMorph(nodesFrom, nodesTo)) { 335 throw new InflateException(" Can't morph from " + fromString + " to " + 336 toString); 337 } 338 returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, 339 nodesFrom, nodesTo); 340 } else { 341 returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, 342 (Object) nodesFrom); 343 } 344 } else if (nodesTo != null) { 345 TypeEvaluator evaluator = 346 new PathDataEvaluator(PathParser.deepCopyNodes(nodesTo)); 347 returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, 348 (Object) nodesTo); 349 } 350 } 351 } else { 352 TypeEvaluator evaluator = null; 353 // Integer and float value types are handled here. 354 if (valueType == VALUE_TYPE_COLOR) { 355 // special case for colors: ignore valueType and get ints 356 evaluator = ArgbEvaluator.getInstance(); 357 } 358 if (getFloats) { 359 float valueFrom; 360 float valueTo; 361 if (hasFrom) { 362 if (fromType == TypedValue.TYPE_DIMENSION) { 363 valueFrom = styledAttributes.getDimension(valueFromId, 0f); 364 } else { 365 valueFrom = styledAttributes.getFloat(valueFromId, 0f); 366 } 367 if (hasTo) { 368 if (toType == TypedValue.TYPE_DIMENSION) { 369 valueTo = styledAttributes.getDimension(valueToId, 0f); 370 } else { 371 valueTo = styledAttributes.getFloat(valueToId, 0f); 372 } 373 returnValue = PropertyValuesHolder.ofFloat(propertyName, 374 valueFrom, valueTo); 375 } else { 376 returnValue = PropertyValuesHolder.ofFloat(propertyName, valueFrom); 377 } 378 } else { 379 if (toType == TypedValue.TYPE_DIMENSION) { 380 valueTo = styledAttributes.getDimension(valueToId, 0f); 381 } else { 382 valueTo = styledAttributes.getFloat(valueToId, 0f); 383 } 384 returnValue = PropertyValuesHolder.ofFloat(propertyName, valueTo); 385 } 386 } else { 387 int valueFrom; 388 int valueTo; 389 if (hasFrom) { 390 if (fromType == TypedValue.TYPE_DIMENSION) { 391 valueFrom = (int) styledAttributes.getDimension(valueFromId, 0f); 392 } else if (isColorType(fromType)) { 393 valueFrom = styledAttributes.getColor(valueFromId, 0); 394 } else { 395 valueFrom = styledAttributes.getInt(valueFromId, 0); 396 } 397 if (hasTo) { 398 if (toType == TypedValue.TYPE_DIMENSION) { 399 valueTo = (int) styledAttributes.getDimension(valueToId, 0f); 400 } else if (isColorType(toType)) { 401 valueTo = styledAttributes.getColor(valueToId, 0); 402 } else { 403 valueTo = styledAttributes.getInt(valueToId, 0); 404 } 405 returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom, valueTo); 406 } else { 407 returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom); 408 } 409 } else { 410 if (hasTo) { 411 if (toType == TypedValue.TYPE_DIMENSION) { 412 valueTo = (int) styledAttributes.getDimension(valueToId, 0f); 413 } else if (isColorType(toType)) { 414 valueTo = styledAttributes.getColor(valueToId, 0); 415 } else { 416 valueTo = styledAttributes.getInt(valueToId, 0); 417 } 418 returnValue = PropertyValuesHolder.ofInt(propertyName, valueTo); 419 } 420 } 421 } 422 if (returnValue != null && evaluator != null) { 423 returnValue.setEvaluator(evaluator); 424 } 425 } 426 427 return returnValue; 428 } 429 430 /** 431 * @param anim The animator, must not be null 432 * @param arrayAnimator Incoming typed array for Animator's attributes. 433 * @param arrayObjectAnimator Incoming typed array for Object Animator's 434 * attributes. 435 * @param pixelSize The relative pixel size, used to calculate the 436 * maximum error for path animations. 437 */ 438 private static void parseAnimatorFromTypeArray(ValueAnimator anim, 439 TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) { 440 long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300); 441 442 long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0); 443 444 int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, VALUE_TYPE_FLOAT); 445 446 PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType, 447 R.styleable.Animator_valueFrom, R.styleable.Animator_valueTo, ""); 448 if (pvh != null) { 449 anim.setValues(pvh); 450 } 451 452 anim.setDuration(duration); 453 anim.setStartDelay(startDelay); 454 455 if (arrayAnimator.hasValue(R.styleable.Animator_repeatCount)) { 456 anim.setRepeatCount( 457 arrayAnimator.getInt(R.styleable.Animator_repeatCount, 0)); 458 } 459 if (arrayAnimator.hasValue(R.styleable.Animator_repeatMode)) { 460 anim.setRepeatMode( 461 arrayAnimator.getInt(R.styleable.Animator_repeatMode, 462 ValueAnimator.RESTART)); 463 } 464 465 if (arrayObjectAnimator != null) { 466 setupObjectAnimator(anim, arrayObjectAnimator, valueType == VALUE_TYPE_FLOAT, 467 pixelSize); 468 } 469 } 470 471 /** 472 * Setup the Animator to achieve path morphing. 473 * 474 * @param anim The target Animator which will be updated. 475 * @param arrayAnimator TypedArray for the ValueAnimator. 476 * @return the PathDataEvaluator. 477 */ 478 private static TypeEvaluator setupAnimatorForPath(ValueAnimator anim, 479 TypedArray arrayAnimator) { 480 TypeEvaluator evaluator = null; 481 String fromString = arrayAnimator.getString(R.styleable.Animator_valueFrom); 482 String toString = arrayAnimator.getString(R.styleable.Animator_valueTo); 483 PathParser.PathDataNode[] nodesFrom = PathParser.createNodesFromPathData(fromString); 484 PathParser.PathDataNode[] nodesTo = PathParser.createNodesFromPathData(toString); 485 486 if (nodesFrom != null) { 487 if (nodesTo != null) { 488 anim.setObjectValues(nodesFrom, nodesTo); 489 if (!PathParser.canMorph(nodesFrom, nodesTo)) { 490 throw new InflateException(arrayAnimator.getPositionDescription() 491 + " Can't morph from " + fromString + " to " + toString); 492 } 493 } else { 494 anim.setObjectValues((Object)nodesFrom); 495 } 496 evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesFrom)); 497 } else if (nodesTo != null) { 498 anim.setObjectValues((Object)nodesTo); 499 evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesTo)); 500 } 501 502 if (DBG_ANIMATOR_INFLATER && evaluator != null) { 503 Log.v(TAG, "create a new PathDataEvaluator here"); 504 } 505 506 return evaluator; 507 } 508 509 /** 510 * Setup ObjectAnimator's property or values from pathData. 511 * 512 * @param anim The target Animator which will be updated. 513 * @param arrayObjectAnimator TypedArray for the ObjectAnimator. 514 * @param getFloats True if the value type is float. 515 * @param pixelSize The relative pixel size, used to calculate the 516 * maximum error for path animations. 517 */ 518 private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator, 519 boolean getFloats, float pixelSize) { 520 ObjectAnimator oa = (ObjectAnimator) anim; 521 String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData); 522 523 // Note that if there is a pathData defined in the Object Animator, 524 // valueFrom / valueTo will be ignored. 525 if (pathData != null) { 526 String propertyXName = 527 arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyXName); 528 String propertyYName = 529 arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyYName); 530 531 if (propertyXName == null && propertyYName == null) { 532 throw new InflateException(arrayObjectAnimator.getPositionDescription() 533 + " propertyXName or propertyYName is needed for PathData"); 534 } else { 535 Path path = PathParser.createPathFromPathData(pathData); 536 float error = 0.5f * pixelSize; // max half a pixel error 537 PathKeyframes keyframeSet = KeyframeSet.ofPath(path, error); 538 Keyframes xKeyframes; 539 Keyframes yKeyframes; 540 if (getFloats) { 541 xKeyframes = keyframeSet.createXFloatKeyframes(); 542 yKeyframes = keyframeSet.createYFloatKeyframes(); 543 } else { 544 xKeyframes = keyframeSet.createXIntKeyframes(); 545 yKeyframes = keyframeSet.createYIntKeyframes(); 546 } 547 PropertyValuesHolder x = null; 548 PropertyValuesHolder y = null; 549 if (propertyXName != null) { 550 x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes); 551 } 552 if (propertyYName != null) { 553 y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes); 554 } 555 if (x == null) { 556 oa.setValues(y); 557 } else if (y == null) { 558 oa.setValues(x); 559 } else { 560 oa.setValues(x, y); 561 } 562 } 563 } else { 564 String propertyName = 565 arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyName); 566 oa.setPropertyName(propertyName); 567 } 568 } 569 570 /** 571 * Setup ValueAnimator's values. 572 * This will handle all of the integer, float and color types. 573 * 574 * @param anim The target Animator which will be updated. 575 * @param arrayAnimator TypedArray for the ValueAnimator. 576 * @param getFloats True if the value type is float. 577 * @param hasFrom True if "valueFrom" exists. 578 * @param fromType The type of "valueFrom". 579 * @param hasTo True if "valueTo" exists. 580 * @param toType The type of "valueTo". 581 */ 582 private static void setupValues(ValueAnimator anim, TypedArray arrayAnimator, 583 boolean getFloats, boolean hasFrom, int fromType, boolean hasTo, int toType) { 584 int valueFromIndex = R.styleable.Animator_valueFrom; 585 int valueToIndex = R.styleable.Animator_valueTo; 586 if (getFloats) { 587 float valueFrom; 588 float valueTo; 589 if (hasFrom) { 590 if (fromType == TypedValue.TYPE_DIMENSION) { 591 valueFrom = arrayAnimator.getDimension(valueFromIndex, 0f); 592 } else { 593 valueFrom = arrayAnimator.getFloat(valueFromIndex, 0f); 594 } 595 if (hasTo) { 596 if (toType == TypedValue.TYPE_DIMENSION) { 597 valueTo = arrayAnimator.getDimension(valueToIndex, 0f); 598 } else { 599 valueTo = arrayAnimator.getFloat(valueToIndex, 0f); 600 } 601 anim.setFloatValues(valueFrom, valueTo); 602 } else { 603 anim.setFloatValues(valueFrom); 604 } 605 } else { 606 if (toType == TypedValue.TYPE_DIMENSION) { 607 valueTo = arrayAnimator.getDimension(valueToIndex, 0f); 608 } else { 609 valueTo = arrayAnimator.getFloat(valueToIndex, 0f); 610 } 611 anim.setFloatValues(valueTo); 612 } 613 } else { 614 int valueFrom; 615 int valueTo; 616 if (hasFrom) { 617 if (fromType == TypedValue.TYPE_DIMENSION) { 618 valueFrom = (int) arrayAnimator.getDimension(valueFromIndex, 0f); 619 } else if (isColorType(fromType)) { 620 valueFrom = arrayAnimator.getColor(valueFromIndex, 0); 621 } else { 622 valueFrom = arrayAnimator.getInt(valueFromIndex, 0); 623 } 624 if (hasTo) { 625 if (toType == TypedValue.TYPE_DIMENSION) { 626 valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f); 627 } else if (isColorType(toType)) { 628 valueTo = arrayAnimator.getColor(valueToIndex, 0); 629 } else { 630 valueTo = arrayAnimator.getInt(valueToIndex, 0); 631 } 632 anim.setIntValues(valueFrom, valueTo); 633 } else { 634 anim.setIntValues(valueFrom); 635 } 636 } else { 637 if (hasTo) { 638 if (toType == TypedValue.TYPE_DIMENSION) { 639 valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f); 640 } else if (isColorType(toType)) { 641 valueTo = arrayAnimator.getColor(valueToIndex, 0); 642 } else { 643 valueTo = arrayAnimator.getInt(valueToIndex, 0); 644 } 645 anim.setIntValues(valueTo); 646 } 647 } 648 } 649 } 650 651 private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, 652 float pixelSize) 653 throws XmlPullParserException, IOException { 654 return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0, 655 pixelSize); 656 } 657 658 private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, 659 AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize) 660 throws XmlPullParserException, IOException { 661 Animator anim = null; 662 ArrayList<Animator> childAnims = null; 663 664 // Make sure we are on a start tag. 665 int type; 666 int depth = parser.getDepth(); 667 668 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 669 && type != XmlPullParser.END_DOCUMENT) { 670 671 if (type != XmlPullParser.START_TAG) { 672 continue; 673 } 674 675 String name = parser.getName(); 676 boolean gotValues = false; 677 678 if (name.equals("objectAnimator")) { 679 anim = loadObjectAnimator(res, theme, attrs, pixelSize); 680 } else if (name.equals("animator")) { 681 anim = loadAnimator(res, theme, attrs, null, pixelSize); 682 } else if (name.equals("set")) { 683 anim = new AnimatorSet(); 684 TypedArray a; 685 if (theme != null) { 686 a = theme.obtainStyledAttributes(attrs, R.styleable.AnimatorSet, 0, 0); 687 } else { 688 a = res.obtainAttributes(attrs, R.styleable.AnimatorSet); 689 } 690 anim.appendChangingConfigurations(a.getChangingConfigurations()); 691 int ordering = a.getInt(R.styleable.AnimatorSet_ordering, TOGETHER); 692 createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering, 693 pixelSize); 694 a.recycle(); 695 } else if (name.equals("propertyValuesHolder")) { 696 PropertyValuesHolder[] values = loadValues(res, theme, parser, 697 Xml.asAttributeSet(parser)); 698 if (values != null && anim != null && (anim instanceof ValueAnimator)) { 699 ((ValueAnimator) anim).setValues(values); 700 } 701 gotValues = true; 702 } else { 703 throw new RuntimeException("Unknown animator name: " + parser.getName()); 704 } 705 706 if (parent != null && !gotValues) { 707 if (childAnims == null) { 708 childAnims = new ArrayList<Animator>(); 709 } 710 childAnims.add(anim); 711 } 712 } 713 if (parent != null && childAnims != null) { 714 Animator[] animsArray = new Animator[childAnims.size()]; 715 int index = 0; 716 for (Animator a : childAnims) { 717 animsArray[index++] = a; 718 } 719 if (sequenceOrdering == TOGETHER) { 720 parent.playTogether(animsArray); 721 } else { 722 parent.playSequentially(animsArray); 723 } 724 } 725 return anim; 726 } 727 728 private static PropertyValuesHolder[] loadValues(Resources res, Theme theme, 729 XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { 730 ArrayList<PropertyValuesHolder> values = null; 731 732 int type; 733 while ((type = parser.getEventType()) != XmlPullParser.END_TAG && 734 type != XmlPullParser.END_DOCUMENT) { 735 736 if (type != XmlPullParser.START_TAG) { 737 parser.next(); 738 continue; 739 } 740 741 String name = parser.getName(); 742 743 if (name.equals("propertyValuesHolder")) { 744 TypedArray a; 745 if (theme != null) { 746 a = theme.obtainStyledAttributes(attrs, R.styleable.PropertyValuesHolder, 0, 0); 747 } else { 748 a = res.obtainAttributes(attrs, R.styleable.PropertyValuesHolder); 749 } 750 String propertyName = a.getString(R.styleable.PropertyValuesHolder_propertyName); 751 int valueType = a.getInt(R.styleable.PropertyValuesHolder_valueType, 752 VALUE_TYPE_UNDEFINED); 753 754 PropertyValuesHolder pvh = loadPvh(res, theme, parser, propertyName, valueType); 755 if (pvh == null) { 756 pvh = getPVH(a, valueType, 757 R.styleable.PropertyValuesHolder_valueFrom, 758 R.styleable.PropertyValuesHolder_valueTo, propertyName); 759 } 760 if (pvh != null) { 761 if (values == null) { 762 values = new ArrayList<PropertyValuesHolder>(); 763 } 764 values.add(pvh); 765 } 766 a.recycle(); 767 } 768 769 parser.next(); 770 } 771 772 PropertyValuesHolder[] valuesArray = null; 773 if (values != null) { 774 int count = values.size(); 775 valuesArray = new PropertyValuesHolder[count]; 776 for (int i = 0; i < count; ++i) { 777 valuesArray[i] = values.get(i); 778 } 779 } 780 return valuesArray; 781 } 782 783 private static void dumpKeyframes(Object[] keyframes, String header) { 784 if (keyframes == null || keyframes.length == 0) { 785 return; 786 } 787 Log.d(TAG, header); 788 int count = keyframes.length; 789 for (int i = 0; i < count; ++i) { 790 Keyframe keyframe = (Keyframe) keyframes[i]; 791 Log.d(TAG, "Keyframe " + i + ": fraction " + 792 (keyframe.getFraction() < 0 ? "null" : keyframe.getFraction()) + ", " + 793 ", value : " + ((keyframe.hasValue()) ? keyframe.getValue() : "null")); 794 } 795 } 796 797 // Load property values holder if there are keyframes defined in it. Otherwise return null. 798 private static PropertyValuesHolder loadPvh(Resources res, Theme theme, XmlPullParser parser, 799 String propertyName, int valueType) 800 throws XmlPullParserException, IOException { 801 802 PropertyValuesHolder value = null; 803 ArrayList<Keyframe> keyframes = null; 804 805 int type; 806 while ((type = parser.next()) != XmlPullParser.END_TAG && 807 type != XmlPullParser.END_DOCUMENT) { 808 String name = parser.getName(); 809 if (name.equals("keyframe")) { 810 Keyframe keyframe = loadKeyframe(res, theme, Xml.asAttributeSet(parser), valueType); 811 if (keyframe != null) { 812 if (keyframes == null) { 813 keyframes = new ArrayList<Keyframe>(); 814 } 815 keyframes.add(keyframe); 816 } 817 parser.next(); 818 } 819 } 820 821 int count; 822 if (keyframes != null && (count = keyframes.size()) > 0) { 823 // make sure we have keyframes at 0 and 1 824 // If we have keyframes with set fractions, add keyframes at start/end 825 // appropriately. If start/end have no set fractions: 826 // if there's only one keyframe, set its fraction to 1 and add one at 0 827 // if >1 keyframe, set the last fraction to 1, the first fraction to 0 828 Keyframe firstKeyframe = keyframes.get(0); 829 Keyframe lastKeyframe = keyframes.get(count - 1); 830 float endFraction = lastKeyframe.getFraction(); 831 if (endFraction < 1) { 832 if (endFraction < 0) { 833 lastKeyframe.setFraction(1); 834 } else { 835 keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1)); 836 ++count; 837 } 838 } 839 float startFraction = firstKeyframe.getFraction(); 840 if (startFraction != 0) { 841 if (startFraction < 0) { 842 firstKeyframe.setFraction(0); 843 } else { 844 keyframes.add(0, createNewKeyframe(firstKeyframe, 0)); 845 ++count; 846 } 847 } 848 Keyframe[] keyframeArray = new Keyframe[count]; 849 keyframes.toArray(keyframeArray); 850 for (int i = 0; i < count; ++i) { 851 Keyframe keyframe = keyframeArray[i]; 852 if (keyframe.getFraction() < 0) { 853 if (i == 0) { 854 keyframe.setFraction(0); 855 } else if (i == count - 1) { 856 keyframe.setFraction(1); 857 } else { 858 // figure out the start/end parameters of the current gap 859 // in fractions and distribute the gap among those keyframes 860 int startIndex = i; 861 int endIndex = i; 862 for (int j = startIndex + 1; j < count - 1; ++j) { 863 if (keyframeArray[j].getFraction() >= 0) { 864 break; 865 } 866 endIndex = j; 867 } 868 float gap = keyframeArray[endIndex + 1].getFraction() - 869 keyframeArray[startIndex - 1].getFraction(); 870 distributeKeyframes(keyframeArray, gap, startIndex, endIndex); 871 } 872 } 873 } 874 value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray); 875 if (valueType == VALUE_TYPE_COLOR) { 876 value.setEvaluator(ArgbEvaluator.getInstance()); 877 } 878 } 879 880 return value; 881 } 882 883 private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) { 884 return sampleKeyframe.getType() == float.class ? 885 Keyframe.ofFloat(fraction) : 886 (sampleKeyframe.getType() == int.class) ? 887 Keyframe.ofInt(fraction) : 888 Keyframe.ofObject(fraction); 889 } 890 891 /** 892 * Utility function to set fractions on keyframes to cover a gap in which the 893 * fractions are not currently set. Keyframe fractions will be distributed evenly 894 * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap 895 * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the 896 * keyframe before startIndex. 897 * Assumptions: 898 * - First and last keyframe fractions (bounding this spread) are already set. So, 899 * for example, if no fractions are set, we will already set first and last keyframe 900 * fraction values to 0 and 1. 901 * - startIndex must be >0 (which follows from first assumption). 902 * - endIndex must be >= startIndex. 903 * 904 * @param keyframes the array of keyframes 905 * @param gap The total gap we need to distribute 906 * @param startIndex The index of the first keyframe whose fraction must be set 907 * @param endIndex The index of the last keyframe whose fraction must be set 908 */ 909 private static void distributeKeyframes(Keyframe[] keyframes, float gap, 910 int startIndex, int endIndex) { 911 int count = endIndex - startIndex + 2; 912 float increment = gap / count; 913 for (int i = startIndex; i <= endIndex; ++i) { 914 keyframes[i].setFraction(keyframes[i-1].getFraction() + increment); 915 } 916 } 917 918 private static Keyframe loadKeyframe(Resources res, Theme theme, AttributeSet attrs, 919 int valueType) 920 throws XmlPullParserException, IOException { 921 922 TypedArray a; 923 if (theme != null) { 924 a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0); 925 } else { 926 a = res.obtainAttributes(attrs, R.styleable.Keyframe); 927 } 928 929 Keyframe keyframe = null; 930 931 float fraction = a.getFloat(R.styleable.Keyframe_fraction, -1); 932 933 TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value); 934 boolean hasValue = (keyframeValue != null); 935 if (valueType == VALUE_TYPE_UNDEFINED) { 936 // When no value type is provided, check whether it's a color type first. 937 // If not, fall back to default value type (i.e. float type). 938 if (hasValue && isColorType(keyframeValue.type)) { 939 valueType = VALUE_TYPE_COLOR; 940 } else { 941 valueType = VALUE_TYPE_FLOAT; 942 } 943 } 944 945 if (hasValue) { 946 switch (valueType) { 947 case VALUE_TYPE_FLOAT: 948 float value = a.getFloat(R.styleable.Keyframe_value, 0); 949 keyframe = Keyframe.ofFloat(fraction, value); 950 break; 951 case VALUE_TYPE_COLOR: 952 case VALUE_TYPE_INT: 953 int intValue = a.getInt(R.styleable.Keyframe_value, 0); 954 keyframe = Keyframe.ofInt(fraction, intValue); 955 break; 956 } 957 } else { 958 keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) : 959 Keyframe.ofInt(fraction); 960 } 961 962 final int resID = a.getResourceId(R.styleable.Keyframe_interpolator, 0); 963 if (resID > 0) { 964 final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID); 965 keyframe.setInterpolator(interpolator); 966 } 967 a.recycle(); 968 969 return keyframe; 970 } 971 972 private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs, 973 float pathErrorScale) throws NotFoundException { 974 ObjectAnimator anim = new ObjectAnimator(); 975 976 loadAnimator(res, theme, attrs, anim, pathErrorScale); 977 978 return anim; 979 } 980 981 /** 982 * Creates a new animation whose parameters come from the specified context 983 * and attributes set. 984 * 985 * @param res The resources 986 * @param attrs The set of attributes holding the animation parameters 987 * @param anim Null if this is a ValueAnimator, otherwise this is an 988 * ObjectAnimator 989 */ 990 private static ValueAnimator loadAnimator(Resources res, Theme theme, 991 AttributeSet attrs, ValueAnimator anim, float pathErrorScale) 992 throws NotFoundException { 993 TypedArray arrayAnimator = null; 994 TypedArray arrayObjectAnimator = null; 995 996 if (theme != null) { 997 arrayAnimator = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0); 998 } else { 999 arrayAnimator = res.obtainAttributes(attrs, R.styleable.Animator); 1000 } 1001 1002 // If anim is not null, then it is an object animator. 1003 if (anim != null) { 1004 if (theme != null) { 1005 arrayObjectAnimator = theme.obtainStyledAttributes(attrs, 1006 R.styleable.PropertyAnimator, 0, 0); 1007 } else { 1008 arrayObjectAnimator = res.obtainAttributes(attrs, R.styleable.PropertyAnimator); 1009 } 1010 anim.appendChangingConfigurations(arrayObjectAnimator.getChangingConfigurations()); 1011 } 1012 1013 if (anim == null) { 1014 anim = new ValueAnimator(); 1015 } 1016 anim.appendChangingConfigurations(arrayAnimator.getChangingConfigurations()); 1017 1018 parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale); 1019 1020 final int resID = arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0); 1021 if (resID > 0) { 1022 final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID); 1023 if (interpolator instanceof BaseInterpolator) { 1024 anim.appendChangingConfigurations( 1025 ((BaseInterpolator) interpolator).getChangingConfiguration()); 1026 } 1027 anim.setInterpolator(interpolator); 1028 } 1029 1030 arrayAnimator.recycle(); 1031 if (arrayObjectAnimator != null) { 1032 arrayObjectAnimator.recycle(); 1033 } 1034 return anim; 1035 } 1036 1037 private static int getChangingConfigs(Resources resources, int id) { 1038 synchronized (sTmpTypedValue) { 1039 resources.getValue(id, sTmpTypedValue, true); 1040 return sTmpTypedValue.changingConfigurations; 1041 } 1042 } 1043 1044 private static boolean isColorType(int type) { 1045 return (type >= TypedValue.TYPE_FIRST_COLOR_INT) && (type <= TypedValue.TYPE_LAST_COLOR_INT); 1046 } 1047} 1048