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