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