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