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