AnimatorInflater.java revision d422dc358f0100106dc07d7b903201eb9b043b11
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.ConfigurationBoundResourceCache;
20import android.content.res.ConstantState;
21import android.content.res.Resources;
22import android.content.res.Resources.NotFoundException;
23import android.content.res.Resources.Theme;
24import android.content.res.TypedArray;
25import android.content.res.XmlResourceParser;
26import android.graphics.Path;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.util.PathParser;
30import android.util.StateSet;
31import android.util.TypedValue;
32import android.util.Xml;
33import android.view.InflateException;
34import android.view.animation.AnimationUtils;
35import android.view.animation.BaseInterpolator;
36import android.view.animation.Interpolator;
37
38import com.android.internal.R;
39
40import org.xmlpull.v1.XmlPullParser;
41import org.xmlpull.v1.XmlPullParserException;
42
43import java.io.IOException;
44import 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 */
55public 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     */
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     */
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 */
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
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
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         */
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         */
273        public PathDataEvaluator(PathParser.PathDataNode[] nodeArray) {
274            mNodeArray = nodeArray;
275        }
276
277        @Override
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     */
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     */
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     */
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     */
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
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
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
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     */
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
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