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