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