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