AnimatorInflater.java revision 984011f6850fd4b6ad4db6d6022bd475d7a2c712
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.content.Context; 19import android.content.res.Resources; 20import android.content.res.Resources.NotFoundException; 21import android.content.res.Resources.Theme; 22import android.content.res.TypedArray; 23import android.content.res.XmlResourceParser; 24import android.graphics.Path; 25import android.util.AttributeSet; 26import android.util.Log; 27import android.util.PathParser; 28import android.util.StateSet; 29import android.util.TypedValue; 30import android.util.Xml; 31import android.view.InflateException; 32import android.view.animation.AnimationUtils; 33 34import com.android.internal.R; 35 36import org.xmlpull.v1.XmlPullParser; 37import org.xmlpull.v1.XmlPullParserException; 38 39import java.io.IOException; 40import java.util.ArrayList; 41 42/** 43 * This class is used to instantiate animator XML files into Animator objects. 44 * <p> 45 * For performance reasons, inflation relies heavily on pre-processing of 46 * XML files that is done at build time. Therefore, it is not currently possible 47 * to use this inflater with an XmlPullParser over a plain XML file at runtime; 48 * it only works with an XmlPullParser returned from a compiled resource (R. 49 * <em>something</em> file.) 50 */ 51public class AnimatorInflater { 52 private static final String TAG = "AnimatorInflater"; 53 /** 54 * These flags are used when parsing AnimatorSet objects 55 */ 56 private static final int TOGETHER = 0; 57 private static final int SEQUENTIALLY = 1; 58 59 /** 60 * Enum values used in XML attributes to indicate the value for mValueType 61 */ 62 private static final int VALUE_TYPE_FLOAT = 0; 63 private static final int VALUE_TYPE_INT = 1; 64 private static final int VALUE_TYPE_PATH = 2; 65 private static final int VALUE_TYPE_COLOR = 4; 66 private static final int VALUE_TYPE_CUSTOM = 5; 67 68 private static final boolean DBG_ANIMATOR_INFLATER = false; 69 70 /** 71 * Loads an {@link Animator} object from a resource 72 * 73 * @param context Application context used to access resources 74 * @param id The resource id of the animation to load 75 * @return The animator object reference by the specified id 76 * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded 77 */ 78 public static Animator loadAnimator(Context context, int id) 79 throws NotFoundException { 80 return loadAnimator(context.getResources(), context.getTheme(), id); 81 } 82 83 /** 84 * Loads an {@link Animator} object from a resource 85 * 86 * @param resources The resources 87 * @param theme The theme 88 * @param id The resource id of the animation to load 89 * @return The animator object reference by the specified id 90 * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded 91 * @hide 92 */ 93 public static Animator loadAnimator(Resources resources, Theme theme, int id) 94 throws NotFoundException { 95 96 XmlResourceParser parser = null; 97 try { 98 parser = resources.getAnimation(id); 99 return createAnimatorFromXml(resources, theme, parser); 100 } catch (XmlPullParserException ex) { 101 Resources.NotFoundException rnf = 102 new Resources.NotFoundException("Can't load animation resource ID #0x" + 103 Integer.toHexString(id)); 104 rnf.initCause(ex); 105 throw rnf; 106 } catch (IOException ex) { 107 Resources.NotFoundException rnf = 108 new Resources.NotFoundException("Can't load animation resource ID #0x" + 109 Integer.toHexString(id)); 110 rnf.initCause(ex); 111 throw rnf; 112 } finally { 113 if (parser != null) parser.close(); 114 } 115 } 116 117 public static StateListAnimator loadStateListAnimator(Context context, int id) 118 throws NotFoundException { 119 XmlResourceParser parser = null; 120 try { 121 parser = context.getResources().getAnimation(id); 122 return createStateListAnimatorFromXml(context, parser, Xml.asAttributeSet(parser)); 123 } catch (XmlPullParserException ex) { 124 Resources.NotFoundException rnf = 125 new Resources.NotFoundException( 126 "Can't load state list animator resource ID #0x" + 127 Integer.toHexString(id) 128 ); 129 rnf.initCause(ex); 130 throw rnf; 131 } catch (IOException ex) { 132 Resources.NotFoundException rnf = 133 new Resources.NotFoundException( 134 "Can't load state list animator resource ID #0x" + 135 Integer.toHexString(id) 136 ); 137 rnf.initCause(ex); 138 throw rnf; 139 } finally { 140 if (parser != null) { 141 parser.close(); 142 } 143 } 144 } 145 146 private static StateListAnimator createStateListAnimatorFromXml(Context context, 147 XmlPullParser parser, AttributeSet attributeSet) 148 throws IOException, XmlPullParserException { 149 int type; 150 StateListAnimator stateListAnimator = new StateListAnimator(); 151 152 while (true) { 153 type = parser.next(); 154 switch (type) { 155 case XmlPullParser.END_DOCUMENT: 156 case XmlPullParser.END_TAG: 157 return stateListAnimator; 158 159 case XmlPullParser.START_TAG: 160 // parse item 161 Animator animator = null; 162 if ("item".equals(parser.getName())) { 163 int attributeCount = parser.getAttributeCount(); 164 int[] states = new int[attributeCount]; 165 int stateIndex = 0; 166 for (int i = 0; i < attributeCount; i++) { 167 int attrName = attributeSet.getAttributeNameResource(i); 168 if (attrName == R.attr.animation) { 169 animator = loadAnimator(context, 170 attributeSet.getAttributeResourceValue(i, 0)); 171 } else { 172 states[stateIndex++] = 173 attributeSet.getAttributeBooleanValue(i, false) ? 174 attrName : -attrName; 175 } 176 177 } 178 if (animator == null) { 179 animator = createAnimatorFromXml(context.getResources(), 180 context.getTheme(), parser); 181 } 182 183 if (animator == null) { 184 throw new Resources.NotFoundException( 185 "animation state item must have a valid animation"); 186 } 187 stateListAnimator 188 .addState(StateSet.trimStateSet(states, stateIndex), animator); 189 190 } 191 break; 192 } 193 } 194 } 195 196 /** 197 * PathDataEvaluator is used to interpolate between two paths which are 198 * represented in the same format but different control points' values. 199 * The path is represented as an array of PathDataNode here, which is 200 * fundamentally an array of floating point numbers. 201 */ 202 private static class PathDataEvaluator implements TypeEvaluator<PathParser.PathDataNode[]> { 203 private PathParser.PathDataNode[] mNodeArray; 204 205 /** 206 * Create a PathParser.PathDataNode[] that does not reuse the animated value. 207 * Care must be taken when using this option because on every evaluation 208 * a new <code>PathParser.PathDataNode[]</code> will be allocated. 209 */ 210 private PathDataEvaluator() {} 211 212 /** 213 * Create a PathDataEvaluator that reuses <code>nodeArray</code> for every evaluate() call. 214 * Caution must be taken to ensure that the value returned from 215 * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or 216 * used across threads. The value will be modified on each <code>evaluate()</code> call. 217 * 218 * @param nodeArray The array to modify and return from <code>evaluate</code>. 219 */ 220 public PathDataEvaluator(PathParser.PathDataNode[] nodeArray) { 221 mNodeArray = nodeArray; 222 } 223 224 @Override 225 public PathParser.PathDataNode[] evaluate(float fraction, 226 PathParser.PathDataNode[] startPathData, 227 PathParser.PathDataNode[] endPathData) { 228 if (!PathParser.canMorph(startPathData, endPathData)) { 229 throw new IllegalArgumentException("Can't interpolate between" 230 + " two incompatible pathData"); 231 } 232 233 if (mNodeArray == null || !PathParser.canMorph(mNodeArray, startPathData)) { 234 mNodeArray = PathParser.deepCopyNodes(startPathData); 235 } 236 237 for (int i = 0; i < startPathData.length; i++) { 238 mNodeArray[i].interpolatePathDataNode(startPathData[i], 239 endPathData[i], fraction); 240 } 241 242 return mNodeArray; 243 } 244 } 245 246 /** 247 * @param anim The animator, must not be null 248 * @param arrayAnimator Incoming typed array for Animator's attributes. 249 * @param arrayObjectAnimator Incoming typed array for Object Animator's 250 * attributes. 251 */ 252 private static void parseAnimatorFromTypeArray(ValueAnimator anim, 253 TypedArray arrayAnimator, TypedArray arrayObjectAnimator) { 254 long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300); 255 256 long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0); 257 258 int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, 259 VALUE_TYPE_FLOAT); 260 261 TypeEvaluator evaluator = null; 262 263 boolean getFloats = (valueType == VALUE_TYPE_FLOAT); 264 265 TypedValue tvFrom = arrayAnimator.peekValue(R.styleable.Animator_valueFrom); 266 boolean hasFrom = (tvFrom != null); 267 int fromType = hasFrom ? tvFrom.type : 0; 268 TypedValue tvTo = arrayAnimator.peekValue(R.styleable.Animator_valueTo); 269 boolean hasTo = (tvTo != null); 270 int toType = hasTo ? tvTo.type : 0; 271 272 // TODO: Further clean up this part of code into 4 types : path, color, 273 // integer and float. 274 if (valueType == VALUE_TYPE_PATH) { 275 evaluator = setupAnimatorForPath(anim, arrayAnimator); 276 } else { 277 // Integer and float value types are handled here. 278 if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && 279 (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) || 280 (hasTo && (toType >= TypedValue.TYPE_FIRST_COLOR_INT) && 281 (toType <= TypedValue.TYPE_LAST_COLOR_INT))) { 282 // special case for colors: ignore valueType and get ints 283 getFloats = false; 284 evaluator = ArgbEvaluator.getInstance(); 285 } 286 setupValues(anim, arrayAnimator, getFloats, hasFrom, fromType, hasTo, toType); 287 } 288 289 anim.setDuration(duration); 290 anim.setStartDelay(startDelay); 291 292 if (arrayAnimator.hasValue(R.styleable.Animator_repeatCount)) { 293 anim.setRepeatCount( 294 arrayAnimator.getInt(R.styleable.Animator_repeatCount, 0)); 295 } 296 if (arrayAnimator.hasValue(R.styleable.Animator_repeatMode)) { 297 anim.setRepeatMode( 298 arrayAnimator.getInt(R.styleable.Animator_repeatMode, 299 ValueAnimator.RESTART)); 300 } 301 if (evaluator != null) { 302 anim.setEvaluator(evaluator); 303 } 304 305 if (arrayObjectAnimator != null) { 306 setupObjectAnimator(anim, arrayObjectAnimator, getFloats); 307 } 308 } 309 310 /** 311 * Setup the Animator to achieve path morphing. 312 * 313 * @param anim The target Animator which will be updated. 314 * @param arrayAnimator TypedArray for the ValueAnimator. 315 * @return the PathDataEvaluator. 316 */ 317 private static TypeEvaluator setupAnimatorForPath(ValueAnimator anim, 318 TypedArray arrayAnimator) { 319 TypeEvaluator evaluator = null; 320 String fromString = arrayAnimator.getString(R.styleable.Animator_valueFrom); 321 String toString = arrayAnimator.getString(R.styleable.Animator_valueTo); 322 PathParser.PathDataNode[] nodesFrom = PathParser.createNodesFromPathData(fromString); 323 PathParser.PathDataNode[] nodesTo = PathParser.createNodesFromPathData(toString); 324 325 if (nodesFrom != null) { 326 if (nodesTo != null) { 327 anim.setObjectValues(nodesFrom, nodesTo); 328 if (!PathParser.canMorph(nodesFrom, nodesTo)) { 329 throw new InflateException(arrayAnimator.getPositionDescription() 330 + " Can't morph from " + fromString + " to " + toString); 331 } 332 } else { 333 anim.setObjectValues((Object)nodesFrom); 334 } 335 evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesFrom)); 336 } else if (nodesTo != null) { 337 anim.setObjectValues((Object)nodesTo); 338 evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesTo)); 339 } 340 341 if (DBG_ANIMATOR_INFLATER && evaluator != null) { 342 Log.v(TAG, "create a new PathDataEvaluator here"); 343 } 344 345 return evaluator; 346 } 347 348 /** 349 * Setup ObjectAnimator's property or values from pathData. 350 * 351 * @param anim The target Animator which will be updated. 352 * @param arrayObjectAnimator TypedArray for the ObjectAnimator. 353 * @param getFloats True if the value type is float. 354 */ 355 private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator, 356 boolean getFloats) { 357 ObjectAnimator oa = (ObjectAnimator) anim; 358 String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData); 359 360 // Note that if there is a pathData defined in the Object Animator, 361 // valueFrom / valueTo will be ignored. 362 if (pathData != null) { 363 String propertyXName = 364 arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyXName); 365 String propertyYName = 366 arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyYName); 367 368 if (propertyXName == null && propertyYName == null) { 369 throw new InflateException(arrayObjectAnimator.getPositionDescription() 370 + " propertyXName or propertyYName is needed for PathData"); 371 } else { 372 Path path = PathParser.createPathFromPathData(pathData); 373 PathKeyframes keyframeSet = KeyframeSet.ofPath(path); 374 Keyframes xKeyframes; 375 Keyframes yKeyframes; 376 if (getFloats) { 377 xKeyframes = keyframeSet.createXFloatKeyframes(); 378 yKeyframes = keyframeSet.createYFloatKeyframes(); 379 } else { 380 xKeyframes = keyframeSet.createXIntKeyframes(); 381 yKeyframes = keyframeSet.createYIntKeyframes(); 382 } 383 PropertyValuesHolder x = null; 384 PropertyValuesHolder y = null; 385 if (propertyXName != null) { 386 x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes); 387 } 388 if (propertyYName != null) { 389 y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes); 390 } 391 if (x == null) { 392 oa.setValues(y); 393 } else if (y == null) { 394 oa.setValues(x); 395 } else { 396 oa.setValues(x, y); 397 } 398 } 399 } else { 400 String propertyName = 401 arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyName); 402 oa.setPropertyName(propertyName); 403 } 404 } 405 406 /** 407 * Setup ValueAnimator's values. 408 * This will handle all of the integer, float and color types. 409 * 410 * @param anim The target Animator which will be updated. 411 * @param arrayAnimator TypedArray for the ValueAnimator. 412 * @param getFloats True if the value type is float. 413 * @param hasFrom True if "valueFrom" exists. 414 * @param fromType The type of "valueFrom". 415 * @param hasTo True if "valueTo" exists. 416 * @param toType The type of "valueTo". 417 */ 418 private static void setupValues(ValueAnimator anim, TypedArray arrayAnimator, 419 boolean getFloats, boolean hasFrom, int fromType, boolean hasTo, int toType) { 420 int valueFromIndex = R.styleable.Animator_valueFrom; 421 int valueToIndex = R.styleable.Animator_valueTo; 422 if (getFloats) { 423 float valueFrom; 424 float valueTo; 425 if (hasFrom) { 426 if (fromType == TypedValue.TYPE_DIMENSION) { 427 valueFrom = arrayAnimator.getDimension(valueFromIndex, 0f); 428 } else { 429 valueFrom = arrayAnimator.getFloat(valueFromIndex, 0f); 430 } 431 if (hasTo) { 432 if (toType == TypedValue.TYPE_DIMENSION) { 433 valueTo = arrayAnimator.getDimension(valueToIndex, 0f); 434 } else { 435 valueTo = arrayAnimator.getFloat(valueToIndex, 0f); 436 } 437 anim.setFloatValues(valueFrom, valueTo); 438 } else { 439 anim.setFloatValues(valueFrom); 440 } 441 } else { 442 if (toType == TypedValue.TYPE_DIMENSION) { 443 valueTo = arrayAnimator.getDimension(valueToIndex, 0f); 444 } else { 445 valueTo = arrayAnimator.getFloat(valueToIndex, 0f); 446 } 447 anim.setFloatValues(valueTo); 448 } 449 } else { 450 int valueFrom; 451 int valueTo; 452 if (hasFrom) { 453 if (fromType == TypedValue.TYPE_DIMENSION) { 454 valueFrom = (int) arrayAnimator.getDimension(valueFromIndex, 0f); 455 } else if ((fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && 456 (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) { 457 valueFrom = arrayAnimator.getColor(valueFromIndex, 0); 458 } else { 459 valueFrom = arrayAnimator.getInt(valueFromIndex, 0); 460 } 461 if (hasTo) { 462 if (toType == TypedValue.TYPE_DIMENSION) { 463 valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f); 464 } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && 465 (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { 466 valueTo = arrayAnimator.getColor(valueToIndex, 0); 467 } else { 468 valueTo = arrayAnimator.getInt(valueToIndex, 0); 469 } 470 anim.setIntValues(valueFrom, valueTo); 471 } else { 472 anim.setIntValues(valueFrom); 473 } 474 } else { 475 if (hasTo) { 476 if (toType == TypedValue.TYPE_DIMENSION) { 477 valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f); 478 } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && 479 (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { 480 valueTo = arrayAnimator.getColor(valueToIndex, 0); 481 } else { 482 valueTo = arrayAnimator.getInt(valueToIndex, 0); 483 } 484 anim.setIntValues(valueTo); 485 } 486 } 487 } 488 } 489 490 private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser) 491 throws XmlPullParserException, IOException { 492 return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0); 493 } 494 495 private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, 496 AttributeSet attrs, AnimatorSet parent, int sequenceOrdering) 497 throws XmlPullParserException, IOException { 498 499 Animator anim = null; 500 ArrayList<Animator> childAnims = null; 501 502 // Make sure we are on a start tag. 503 int type; 504 int depth = parser.getDepth(); 505 506 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 507 && type != XmlPullParser.END_DOCUMENT) { 508 509 if (type != XmlPullParser.START_TAG) { 510 continue; 511 } 512 513 String name = parser.getName(); 514 515 if (name.equals("objectAnimator")) { 516 anim = loadObjectAnimator(res, theme, attrs); 517 } else if (name.equals("animator")) { 518 anim = loadAnimator(res, theme, attrs, null); 519 } else if (name.equals("set")) { 520 anim = new AnimatorSet(); 521 TypedArray a; 522 if (theme != null) { 523 a = theme.obtainStyledAttributes(attrs, R.styleable.AnimatorSet, 0, 0); 524 } else { 525 a = res.obtainAttributes(attrs, R.styleable.AnimatorSet); 526 } 527 int ordering = a.getInt(R.styleable.AnimatorSet_ordering, 528 TOGETHER); 529 createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering); 530 a.recycle(); 531 } else { 532 throw new RuntimeException("Unknown animator name: " + parser.getName()); 533 } 534 535 if (parent != null) { 536 if (childAnims == null) { 537 childAnims = new ArrayList<Animator>(); 538 } 539 childAnims.add(anim); 540 } 541 } 542 if (parent != null && childAnims != null) { 543 Animator[] animsArray = new Animator[childAnims.size()]; 544 int index = 0; 545 for (Animator a : childAnims) { 546 animsArray[index++] = a; 547 } 548 if (sequenceOrdering == TOGETHER) { 549 parent.playTogether(animsArray); 550 } else { 551 parent.playSequentially(animsArray); 552 } 553 } 554 555 return anim; 556 557 } 558 559 private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs) 560 throws NotFoundException { 561 ObjectAnimator anim = new ObjectAnimator(); 562 563 loadAnimator(res, theme, attrs, anim); 564 565 return anim; 566 } 567 568 /** 569 * Creates a new animation whose parameters come from the specified context 570 * and attributes set. 571 * 572 * @param res The resources 573 * @param attrs The set of attributes holding the animation parameters 574 * @param anim Null if this is a ValueAnimator, otherwise this is an 575 * ObjectAnimator 576 */ 577 private static ValueAnimator loadAnimator(Resources res, Theme theme, 578 AttributeSet attrs, ValueAnimator anim) 579 throws NotFoundException { 580 581 TypedArray arrayAnimator = null; 582 TypedArray arrayObjectAnimator = null; 583 584 if (theme != null) { 585 arrayAnimator = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0); 586 } else { 587 arrayAnimator = res.obtainAttributes(attrs, R.styleable.Animator); 588 } 589 590 // If anim is not null, then it is an object animator. 591 if (anim != null) { 592 if (theme != null) { 593 arrayObjectAnimator = theme.obtainStyledAttributes(attrs, 594 R.styleable.PropertyAnimator, 0, 0); 595 } else { 596 arrayObjectAnimator = res.obtainAttributes(attrs, R.styleable.PropertyAnimator); 597 } 598 } 599 600 if (anim == null) { 601 anim = new ValueAnimator(); 602 } 603 604 parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator); 605 606 final int resID = 607 arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0); 608 if (resID > 0) { 609 anim.setInterpolator(AnimationUtils.loadInterpolator(res, theme, resID)); 610 } 611 612 arrayAnimator.recycle(); 613 if (arrayObjectAnimator != null) { 614 arrayObjectAnimator.recycle(); 615 } 616 617 return anim; 618 } 619} 620