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