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