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