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