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