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