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 */ 16 package android.animation; 17 18 import android.annotation.AnimatorRes; 19 import android.annotation.AnyRes; 20 import android.annotation.NonNull; 21 import android.content.Context; 22 import android.content.pm.ActivityInfo.Config; 23 import android.content.res.ConfigurationBoundResourceCache; 24 import android.content.res.ConstantState; 25 import android.content.res.Resources; 26 import android.content.res.Resources.NotFoundException; 27 import android.content.res.Resources.Theme; 28 import android.content.res.TypedArray; 29 import android.content.res.XmlResourceParser; 30 import android.graphics.Path; 31 import android.util.AttributeSet; 32 import android.util.Log; 33 import android.util.PathParser; 34 import android.util.StateSet; 35 import android.util.TypedValue; 36 import android.util.Xml; 37 import android.view.InflateException; 38 import android.view.animation.AnimationUtils; 39 import android.view.animation.BaseInterpolator; 40 import android.view.animation.Interpolator; 41 42 import com.android.internal.R; 43 44 import org.xmlpull.v1.XmlPullParser; 45 import org.xmlpull.v1.XmlPullParserException; 46 47 import java.io.IOException; 48 import 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 */ 59 public 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 */ loadAnimator(Context context, @AnimatorRes int id)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 */ loadAnimator(Resources resources, Theme theme, int id)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 */ loadAnimator(Resources resources, Theme theme, int id, float pathErrorScale)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 loadStateListAnimator(Context context, int id)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 createStateListAnimatorFromXml(Context context, XmlPullParser parser, AttributeSet attributeSet)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 evaluate(float fraction, PathParser.PathData startPathData, PathParser.PathData endPathData)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 getPVH(TypedArray styledAttributes, int valueType, int valueFromId, int valueToId, String propertyName)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 */ parseAnimatorFromTypeArray(ValueAnimator anim, TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize)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, pixelSize); 443 } 444 } 445 446 /** 447 * Setup the Animator to achieve path morphing. 448 * 449 * @param anim The target Animator which will be updated. 450 * @param arrayAnimator TypedArray for the ValueAnimator. 451 * @return the PathDataEvaluator. 452 */ setupAnimatorForPath(ValueAnimator anim, TypedArray arrayAnimator)453 private static TypeEvaluator setupAnimatorForPath(ValueAnimator anim, 454 TypedArray arrayAnimator) { 455 TypeEvaluator evaluator = null; 456 String fromString = arrayAnimator.getString(R.styleable.Animator_valueFrom); 457 String toString = arrayAnimator.getString(R.styleable.Animator_valueTo); 458 PathParser.PathData pathDataFrom = fromString == null 459 ? null : new PathParser.PathData(fromString); 460 PathParser.PathData pathDataTo = toString == null 461 ? null : new PathParser.PathData(toString); 462 463 if (pathDataFrom != null) { 464 if (pathDataTo != null) { 465 anim.setObjectValues(pathDataFrom, pathDataTo); 466 if (!PathParser.canMorph(pathDataFrom, pathDataTo)) { 467 throw new InflateException(arrayAnimator.getPositionDescription() 468 + " Can't morph from " + fromString + " to " + toString); 469 } 470 } else { 471 anim.setObjectValues((Object)pathDataFrom); 472 } 473 evaluator = new PathDataEvaluator(); 474 } else if (pathDataTo != null) { 475 anim.setObjectValues((Object)pathDataTo); 476 evaluator = new PathDataEvaluator(); 477 } 478 479 if (DBG_ANIMATOR_INFLATER && evaluator != null) { 480 Log.v(TAG, "create a new PathDataEvaluator here"); 481 } 482 483 return evaluator; 484 } 485 486 /** 487 * Setup ObjectAnimator's property or values from pathData. 488 * 489 * @param anim The target Animator which will be updated. 490 * @param arrayObjectAnimator TypedArray for the ObjectAnimator. 491 * @param getFloats True if the value type is float. 492 * @param pixelSize The relative pixel size, used to calculate the 493 * maximum error for path animations. 494 */ setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator, int valueType, float pixelSize)495 private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator, 496 int valueType, float pixelSize) { 497 ObjectAnimator oa = (ObjectAnimator) anim; 498 String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData); 499 500 // Path can be involved in an ObjectAnimator in the following 3 ways: 501 // 1) Path morphing: the property to be animated is pathData, and valueFrom and valueTo 502 // are both of pathType. valueType = pathType needs to be explicitly defined. 503 // 2) A property in X or Y dimension can be animated along a path: the property needs to be 504 // defined in propertyXName or propertyYName attribute, the path will be defined in the 505 // pathData attribute. valueFrom and valueTo will not be necessary for this animation. 506 // 3) PathInterpolator can also define a path (in pathData) for its interpolation curve. 507 // Here we are dealing with case 2: 508 if (pathData != null) { 509 String propertyXName = 510 arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyXName); 511 String propertyYName = 512 arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyYName); 513 514 if (valueType == VALUE_TYPE_PATH || valueType == VALUE_TYPE_UNDEFINED) { 515 // When pathData is defined, we are in case #2 mentioned above. ValueType can only 516 // be float type, or int type. Otherwise we fallback to default type. 517 valueType = VALUE_TYPE_FLOAT; 518 } 519 if (propertyXName == null && propertyYName == null) { 520 throw new InflateException(arrayObjectAnimator.getPositionDescription() 521 + " propertyXName or propertyYName is needed for PathData"); 522 } else { 523 Path path = PathParser.createPathFromPathData(pathData); 524 float error = 0.5f * pixelSize; // max half a pixel error 525 PathKeyframes keyframeSet = KeyframeSet.ofPath(path, error); 526 Keyframes xKeyframes; 527 Keyframes yKeyframes; 528 if (valueType == VALUE_TYPE_FLOAT) { 529 xKeyframes = keyframeSet.createXFloatKeyframes(); 530 yKeyframes = keyframeSet.createYFloatKeyframes(); 531 } else { 532 xKeyframes = keyframeSet.createXIntKeyframes(); 533 yKeyframes = keyframeSet.createYIntKeyframes(); 534 } 535 PropertyValuesHolder x = null; 536 PropertyValuesHolder y = null; 537 if (propertyXName != null) { 538 x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes); 539 } 540 if (propertyYName != null) { 541 y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes); 542 } 543 if (x == null) { 544 oa.setValues(y); 545 } else if (y == null) { 546 oa.setValues(x); 547 } else { 548 oa.setValues(x, y); 549 } 550 } 551 } else { 552 String propertyName = 553 arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyName); 554 oa.setPropertyName(propertyName); 555 } 556 } 557 558 /** 559 * Setup ValueAnimator's values. 560 * This will handle all of the integer, float and color types. 561 * 562 * @param anim The target Animator which will be updated. 563 * @param arrayAnimator TypedArray for the ValueAnimator. 564 * @param getFloats True if the value type is float. 565 * @param hasFrom True if "valueFrom" exists. 566 * @param fromType The type of "valueFrom". 567 * @param hasTo True if "valueTo" exists. 568 * @param toType The type of "valueTo". 569 */ setupValues(ValueAnimator anim, TypedArray arrayAnimator, boolean getFloats, boolean hasFrom, int fromType, boolean hasTo, int toType)570 private static void setupValues(ValueAnimator anim, TypedArray arrayAnimator, 571 boolean getFloats, boolean hasFrom, int fromType, boolean hasTo, int toType) { 572 int valueFromIndex = R.styleable.Animator_valueFrom; 573 int valueToIndex = R.styleable.Animator_valueTo; 574 if (getFloats) { 575 float valueFrom; 576 float valueTo; 577 if (hasFrom) { 578 if (fromType == TypedValue.TYPE_DIMENSION) { 579 valueFrom = arrayAnimator.getDimension(valueFromIndex, 0f); 580 } else { 581 valueFrom = arrayAnimator.getFloat(valueFromIndex, 0f); 582 } 583 if (hasTo) { 584 if (toType == TypedValue.TYPE_DIMENSION) { 585 valueTo = arrayAnimator.getDimension(valueToIndex, 0f); 586 } else { 587 valueTo = arrayAnimator.getFloat(valueToIndex, 0f); 588 } 589 anim.setFloatValues(valueFrom, valueTo); 590 } else { 591 anim.setFloatValues(valueFrom); 592 } 593 } else { 594 if (toType == TypedValue.TYPE_DIMENSION) { 595 valueTo = arrayAnimator.getDimension(valueToIndex, 0f); 596 } else { 597 valueTo = arrayAnimator.getFloat(valueToIndex, 0f); 598 } 599 anim.setFloatValues(valueTo); 600 } 601 } else { 602 int valueFrom; 603 int valueTo; 604 if (hasFrom) { 605 if (fromType == TypedValue.TYPE_DIMENSION) { 606 valueFrom = (int) arrayAnimator.getDimension(valueFromIndex, 0f); 607 } else if (isColorType(fromType)) { 608 valueFrom = arrayAnimator.getColor(valueFromIndex, 0); 609 } else { 610 valueFrom = arrayAnimator.getInt(valueFromIndex, 0); 611 } 612 if (hasTo) { 613 if (toType == TypedValue.TYPE_DIMENSION) { 614 valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f); 615 } else if (isColorType(toType)) { 616 valueTo = arrayAnimator.getColor(valueToIndex, 0); 617 } else { 618 valueTo = arrayAnimator.getInt(valueToIndex, 0); 619 } 620 anim.setIntValues(valueFrom, valueTo); 621 } else { 622 anim.setIntValues(valueFrom); 623 } 624 } else { 625 if (hasTo) { 626 if (toType == TypedValue.TYPE_DIMENSION) { 627 valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f); 628 } else if (isColorType(toType)) { 629 valueTo = arrayAnimator.getColor(valueToIndex, 0); 630 } else { 631 valueTo = arrayAnimator.getInt(valueToIndex, 0); 632 } 633 anim.setIntValues(valueTo); 634 } 635 } 636 } 637 } 638 createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, float pixelSize)639 private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, 640 float pixelSize) 641 throws XmlPullParserException, IOException { 642 return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0, 643 pixelSize); 644 } 645 createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)646 private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, 647 AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize) 648 throws XmlPullParserException, IOException { 649 Animator anim = null; 650 ArrayList<Animator> childAnims = null; 651 652 // Make sure we are on a start tag. 653 int type; 654 int depth = parser.getDepth(); 655 656 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 657 && type != XmlPullParser.END_DOCUMENT) { 658 659 if (type != XmlPullParser.START_TAG) { 660 continue; 661 } 662 663 String name = parser.getName(); 664 boolean gotValues = false; 665 666 if (name.equals("objectAnimator")) { 667 anim = loadObjectAnimator(res, theme, attrs, pixelSize); 668 } else if (name.equals("animator")) { 669 anim = loadAnimator(res, theme, attrs, null, pixelSize); 670 } else if (name.equals("set")) { 671 anim = new AnimatorSet(); 672 TypedArray a; 673 if (theme != null) { 674 a = theme.obtainStyledAttributes(attrs, R.styleable.AnimatorSet, 0, 0); 675 } else { 676 a = res.obtainAttributes(attrs, R.styleable.AnimatorSet); 677 } 678 anim.appendChangingConfigurations(a.getChangingConfigurations()); 679 int ordering = a.getInt(R.styleable.AnimatorSet_ordering, TOGETHER); 680 createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering, 681 pixelSize); 682 a.recycle(); 683 } else if (name.equals("propertyValuesHolder")) { 684 PropertyValuesHolder[] values = loadValues(res, theme, parser, 685 Xml.asAttributeSet(parser)); 686 if (values != null && anim != null && (anim instanceof ValueAnimator)) { 687 ((ValueAnimator) anim).setValues(values); 688 } 689 gotValues = true; 690 } else { 691 throw new RuntimeException("Unknown animator name: " + parser.getName()); 692 } 693 694 if (parent != null && !gotValues) { 695 if (childAnims == null) { 696 childAnims = new ArrayList<Animator>(); 697 } 698 childAnims.add(anim); 699 } 700 } 701 if (parent != null && childAnims != null) { 702 Animator[] animsArray = new Animator[childAnims.size()]; 703 int index = 0; 704 for (Animator a : childAnims) { 705 animsArray[index++] = a; 706 } 707 if (sequenceOrdering == TOGETHER) { 708 parent.playTogether(animsArray); 709 } else { 710 parent.playSequentially(animsArray); 711 } 712 } 713 return anim; 714 } 715 loadValues(Resources res, Theme theme, XmlPullParser parser, AttributeSet attrs)716 private static PropertyValuesHolder[] loadValues(Resources res, Theme theme, 717 XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { 718 ArrayList<PropertyValuesHolder> values = null; 719 720 int type; 721 while ((type = parser.getEventType()) != XmlPullParser.END_TAG && 722 type != XmlPullParser.END_DOCUMENT) { 723 724 if (type != XmlPullParser.START_TAG) { 725 parser.next(); 726 continue; 727 } 728 729 String name = parser.getName(); 730 731 if (name.equals("propertyValuesHolder")) { 732 TypedArray a; 733 if (theme != null) { 734 a = theme.obtainStyledAttributes(attrs, R.styleable.PropertyValuesHolder, 0, 0); 735 } else { 736 a = res.obtainAttributes(attrs, R.styleable.PropertyValuesHolder); 737 } 738 String propertyName = a.getString(R.styleable.PropertyValuesHolder_propertyName); 739 int valueType = a.getInt(R.styleable.PropertyValuesHolder_valueType, 740 VALUE_TYPE_UNDEFINED); 741 742 PropertyValuesHolder pvh = loadPvh(res, theme, parser, propertyName, valueType); 743 if (pvh == null) { 744 pvh = getPVH(a, valueType, 745 R.styleable.PropertyValuesHolder_valueFrom, 746 R.styleable.PropertyValuesHolder_valueTo, propertyName); 747 } 748 if (pvh != null) { 749 if (values == null) { 750 values = new ArrayList<PropertyValuesHolder>(); 751 } 752 values.add(pvh); 753 } 754 a.recycle(); 755 } 756 757 parser.next(); 758 } 759 760 PropertyValuesHolder[] valuesArray = null; 761 if (values != null) { 762 int count = values.size(); 763 valuesArray = new PropertyValuesHolder[count]; 764 for (int i = 0; i < count; ++i) { 765 valuesArray[i] = values.get(i); 766 } 767 } 768 return valuesArray; 769 } 770 771 // When no value type is provided in keyframe, we need to infer the type from the value. i.e. 772 // if value is defined in the style of a color value, then the color type is returned. 773 // Otherwise, default float type is returned. inferValueTypeOfKeyframe(Resources res, Theme theme, AttributeSet attrs)774 private static int inferValueTypeOfKeyframe(Resources res, Theme theme, AttributeSet attrs) { 775 int valueType; 776 TypedArray a; 777 if (theme != null) { 778 a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0); 779 } else { 780 a = res.obtainAttributes(attrs, R.styleable.Keyframe); 781 } 782 783 TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value); 784 boolean hasValue = (keyframeValue != null); 785 // When no value type is provided, check whether it's a color type first. 786 // If not, fall back to default value type (i.e. float type). 787 if (hasValue && isColorType(keyframeValue.type)) { 788 valueType = VALUE_TYPE_COLOR; 789 } else { 790 valueType = VALUE_TYPE_FLOAT; 791 } 792 a.recycle(); 793 return valueType; 794 } 795 inferValueTypeFromValues(TypedArray styledAttributes, int valueFromId, int valueToId)796 private static int inferValueTypeFromValues(TypedArray styledAttributes, int valueFromId, 797 int valueToId) { 798 TypedValue tvFrom = styledAttributes.peekValue(valueFromId); 799 boolean hasFrom = (tvFrom != null); 800 int fromType = hasFrom ? tvFrom.type : 0; 801 TypedValue tvTo = styledAttributes.peekValue(valueToId); 802 boolean hasTo = (tvTo != null); 803 int toType = hasTo ? tvTo.type : 0; 804 805 int valueType; 806 // Check whether it's color type. If not, fall back to default type (i.e. float type) 807 if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) { 808 valueType = VALUE_TYPE_COLOR; 809 } else { 810 valueType = VALUE_TYPE_FLOAT; 811 } 812 return valueType; 813 } 814 dumpKeyframes(Object[] keyframes, String header)815 private static void dumpKeyframes(Object[] keyframes, String header) { 816 if (keyframes == null || keyframes.length == 0) { 817 return; 818 } 819 Log.d(TAG, header); 820 int count = keyframes.length; 821 for (int i = 0; i < count; ++i) { 822 Keyframe keyframe = (Keyframe) keyframes[i]; 823 Log.d(TAG, "Keyframe " + i + ": fraction " + 824 (keyframe.getFraction() < 0 ? "null" : keyframe.getFraction()) + ", " + 825 ", value : " + ((keyframe.hasValue()) ? keyframe.getValue() : "null")); 826 } 827 } 828 829 // Load property values holder if there are keyframes defined in it. Otherwise return null. loadPvh(Resources res, Theme theme, XmlPullParser parser, String propertyName, int valueType)830 private static PropertyValuesHolder loadPvh(Resources res, Theme theme, XmlPullParser parser, 831 String propertyName, int valueType) 832 throws XmlPullParserException, IOException { 833 834 PropertyValuesHolder value = null; 835 ArrayList<Keyframe> keyframes = null; 836 837 int type; 838 while ((type = parser.next()) != XmlPullParser.END_TAG && 839 type != XmlPullParser.END_DOCUMENT) { 840 String name = parser.getName(); 841 if (name.equals("keyframe")) { 842 if (valueType == VALUE_TYPE_UNDEFINED) { 843 valueType = inferValueTypeOfKeyframe(res, theme, Xml.asAttributeSet(parser)); 844 } 845 Keyframe keyframe = loadKeyframe(res, theme, Xml.asAttributeSet(parser), valueType); 846 if (keyframe != null) { 847 if (keyframes == null) { 848 keyframes = new ArrayList<Keyframe>(); 849 } 850 keyframes.add(keyframe); 851 } 852 parser.next(); 853 } 854 } 855 856 int count; 857 if (keyframes != null && (count = keyframes.size()) > 0) { 858 // make sure we have keyframes at 0 and 1 859 // If we have keyframes with set fractions, add keyframes at start/end 860 // appropriately. If start/end have no set fractions: 861 // if there's only one keyframe, set its fraction to 1 and add one at 0 862 // if >1 keyframe, set the last fraction to 1, the first fraction to 0 863 Keyframe firstKeyframe = keyframes.get(0); 864 Keyframe lastKeyframe = keyframes.get(count - 1); 865 float endFraction = lastKeyframe.getFraction(); 866 if (endFraction < 1) { 867 if (endFraction < 0) { 868 lastKeyframe.setFraction(1); 869 } else { 870 keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1)); 871 ++count; 872 } 873 } 874 float startFraction = firstKeyframe.getFraction(); 875 if (startFraction != 0) { 876 if (startFraction < 0) { 877 firstKeyframe.setFraction(0); 878 } else { 879 keyframes.add(0, createNewKeyframe(firstKeyframe, 0)); 880 ++count; 881 } 882 } 883 Keyframe[] keyframeArray = new Keyframe[count]; 884 keyframes.toArray(keyframeArray); 885 for (int i = 0; i < count; ++i) { 886 Keyframe keyframe = keyframeArray[i]; 887 if (keyframe.getFraction() < 0) { 888 if (i == 0) { 889 keyframe.setFraction(0); 890 } else if (i == count - 1) { 891 keyframe.setFraction(1); 892 } else { 893 // figure out the start/end parameters of the current gap 894 // in fractions and distribute the gap among those keyframes 895 int startIndex = i; 896 int endIndex = i; 897 for (int j = startIndex + 1; j < count - 1; ++j) { 898 if (keyframeArray[j].getFraction() >= 0) { 899 break; 900 } 901 endIndex = j; 902 } 903 float gap = keyframeArray[endIndex + 1].getFraction() - 904 keyframeArray[startIndex - 1].getFraction(); 905 distributeKeyframes(keyframeArray, gap, startIndex, endIndex); 906 } 907 } 908 } 909 value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray); 910 if (valueType == VALUE_TYPE_COLOR) { 911 value.setEvaluator(ArgbEvaluator.getInstance()); 912 } 913 } 914 915 return value; 916 } 917 createNewKeyframe(Keyframe sampleKeyframe, float fraction)918 private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) { 919 return sampleKeyframe.getType() == float.class ? 920 Keyframe.ofFloat(fraction) : 921 (sampleKeyframe.getType() == int.class) ? 922 Keyframe.ofInt(fraction) : 923 Keyframe.ofObject(fraction); 924 } 925 926 /** 927 * Utility function to set fractions on keyframes to cover a gap in which the 928 * fractions are not currently set. Keyframe fractions will be distributed evenly 929 * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap 930 * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the 931 * keyframe before startIndex. 932 * Assumptions: 933 * - First and last keyframe fractions (bounding this spread) are already set. So, 934 * for example, if no fractions are set, we will already set first and last keyframe 935 * fraction values to 0 and 1. 936 * - startIndex must be >0 (which follows from first assumption). 937 * - endIndex must be >= startIndex. 938 * 939 * @param keyframes the array of keyframes 940 * @param gap The total gap we need to distribute 941 * @param startIndex The index of the first keyframe whose fraction must be set 942 * @param endIndex The index of the last keyframe whose fraction must be set 943 */ distributeKeyframes(Keyframe[] keyframes, float gap, int startIndex, int endIndex)944 private static void distributeKeyframes(Keyframe[] keyframes, float gap, 945 int startIndex, int endIndex) { 946 int count = endIndex - startIndex + 2; 947 float increment = gap / count; 948 for (int i = startIndex; i <= endIndex; ++i) { 949 keyframes[i].setFraction(keyframes[i-1].getFraction() + increment); 950 } 951 } 952 loadKeyframe(Resources res, Theme theme, AttributeSet attrs, int valueType)953 private static Keyframe loadKeyframe(Resources res, Theme theme, AttributeSet attrs, 954 int valueType) 955 throws XmlPullParserException, IOException { 956 957 TypedArray a; 958 if (theme != null) { 959 a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0); 960 } else { 961 a = res.obtainAttributes(attrs, R.styleable.Keyframe); 962 } 963 964 Keyframe keyframe = null; 965 966 float fraction = a.getFloat(R.styleable.Keyframe_fraction, -1); 967 968 TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value); 969 boolean hasValue = (keyframeValue != null); 970 if (valueType == VALUE_TYPE_UNDEFINED) { 971 // When no value type is provided, check whether it's a color type first. 972 // If not, fall back to default value type (i.e. float type). 973 if (hasValue && isColorType(keyframeValue.type)) { 974 valueType = VALUE_TYPE_COLOR; 975 } else { 976 valueType = VALUE_TYPE_FLOAT; 977 } 978 } 979 980 if (hasValue) { 981 switch (valueType) { 982 case VALUE_TYPE_FLOAT: 983 float value = a.getFloat(R.styleable.Keyframe_value, 0); 984 keyframe = Keyframe.ofFloat(fraction, value); 985 break; 986 case VALUE_TYPE_COLOR: 987 case VALUE_TYPE_INT: 988 int intValue = a.getInt(R.styleable.Keyframe_value, 0); 989 keyframe = Keyframe.ofInt(fraction, intValue); 990 break; 991 } 992 } else { 993 keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) : 994 Keyframe.ofInt(fraction); 995 } 996 997 final int resID = a.getResourceId(R.styleable.Keyframe_interpolator, 0); 998 if (resID > 0) { 999 final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID); 1000 keyframe.setInterpolator(interpolator); 1001 } 1002 a.recycle(); 1003 1004 return keyframe; 1005 } 1006 loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs, float pathErrorScale)1007 private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs, 1008 float pathErrorScale) throws NotFoundException { 1009 ObjectAnimator anim = new ObjectAnimator(); 1010 1011 loadAnimator(res, theme, attrs, anim, pathErrorScale); 1012 1013 return anim; 1014 } 1015 1016 /** 1017 * Creates a new animation whose parameters come from the specified context 1018 * and attributes set. 1019 * 1020 * @param res The resources 1021 * @param attrs The set of attributes holding the animation parameters 1022 * @param anim Null if this is a ValueAnimator, otherwise this is an 1023 * ObjectAnimator 1024 */ loadAnimator(Resources res, Theme theme, AttributeSet attrs, ValueAnimator anim, float pathErrorScale)1025 private static ValueAnimator loadAnimator(Resources res, Theme theme, 1026 AttributeSet attrs, ValueAnimator anim, float pathErrorScale) 1027 throws NotFoundException { 1028 TypedArray arrayAnimator = null; 1029 TypedArray arrayObjectAnimator = null; 1030 1031 if (theme != null) { 1032 arrayAnimator = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0); 1033 } else { 1034 arrayAnimator = res.obtainAttributes(attrs, R.styleable.Animator); 1035 } 1036 1037 // If anim is not null, then it is an object animator. 1038 if (anim != null) { 1039 if (theme != null) { 1040 arrayObjectAnimator = theme.obtainStyledAttributes(attrs, 1041 R.styleable.PropertyAnimator, 0, 0); 1042 } else { 1043 arrayObjectAnimator = res.obtainAttributes(attrs, R.styleable.PropertyAnimator); 1044 } 1045 anim.appendChangingConfigurations(arrayObjectAnimator.getChangingConfigurations()); 1046 } 1047 1048 if (anim == null) { 1049 anim = new ValueAnimator(); 1050 } 1051 anim.appendChangingConfigurations(arrayAnimator.getChangingConfigurations()); 1052 1053 parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale); 1054 1055 final int resID = arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0); 1056 if (resID > 0) { 1057 final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID); 1058 if (interpolator instanceof BaseInterpolator) { 1059 anim.appendChangingConfigurations( 1060 ((BaseInterpolator) interpolator).getChangingConfiguration()); 1061 } 1062 anim.setInterpolator(interpolator); 1063 } 1064 1065 arrayAnimator.recycle(); 1066 if (arrayObjectAnimator != null) { 1067 arrayObjectAnimator.recycle(); 1068 } 1069 return anim; 1070 } 1071 getChangingConfigs(@onNull Resources resources, @AnyRes int id)1072 private static @Config int getChangingConfigs(@NonNull Resources resources, @AnyRes int id) { 1073 synchronized (sTmpTypedValue) { 1074 resources.getValue(id, sTmpTypedValue, true); 1075 return sTmpTypedValue.changingConfigurations; 1076 } 1077 } 1078 isColorType(int type)1079 private static boolean isColorType(int type) { 1080 return (type >= TypedValue.TYPE_FIRST_COLOR_INT) && (type <= TypedValue.TYPE_LAST_COLOR_INT); 1081 } 1082 } 1083