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, pixelSize);
443        }
444    }
445
446    /**
447     * Setup the Animator to achieve path morphing.
448     *
449     * @param anim The target Animator which will be updated.
450     * @param arrayAnimator TypedArray for the ValueAnimator.
451     * @return the PathDataEvaluator.
452     */
453    private static TypeEvaluator setupAnimatorForPath(ValueAnimator anim,
454             TypedArray arrayAnimator) {
455        TypeEvaluator evaluator = null;
456        String fromString = arrayAnimator.getString(R.styleable.Animator_valueFrom);
457        String toString = arrayAnimator.getString(R.styleable.Animator_valueTo);
458        PathParser.PathData pathDataFrom = fromString == null
459                ? null : new PathParser.PathData(fromString);
460        PathParser.PathData pathDataTo = toString == null
461                ? null : new PathParser.PathData(toString);
462
463        if (pathDataFrom != null) {
464            if (pathDataTo != null) {
465                anim.setObjectValues(pathDataFrom, pathDataTo);
466                if (!PathParser.canMorph(pathDataFrom, pathDataTo)) {
467                    throw new InflateException(arrayAnimator.getPositionDescription()
468                            + " Can't morph from " + fromString + " to " + toString);
469                }
470            } else {
471                anim.setObjectValues((Object)pathDataFrom);
472            }
473            evaluator = new PathDataEvaluator();
474        } else if (pathDataTo != null) {
475            anim.setObjectValues((Object)pathDataTo);
476            evaluator = new PathDataEvaluator();
477        }
478
479        if (DBG_ANIMATOR_INFLATER && evaluator != null) {
480            Log.v(TAG, "create a new PathDataEvaluator here");
481        }
482
483        return evaluator;
484    }
485
486    /**
487     * Setup ObjectAnimator's property or values from pathData.
488     *
489     * @param anim The target Animator which will be updated.
490     * @param arrayObjectAnimator TypedArray for the ObjectAnimator.
491     * @param getFloats True if the value type is float.
492     * @param pixelSize The relative pixel size, used to calculate the
493     *                  maximum error for path animations.
494     */
495    private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator,
496            int valueType, float pixelSize) {
497        ObjectAnimator oa = (ObjectAnimator) anim;
498        String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData);
499
500        // Path can be involved in an ObjectAnimator in the following 3 ways:
501        // 1) Path morphing: the property to be animated is pathData, and valueFrom and valueTo
502        //    are both of pathType. valueType = pathType needs to be explicitly defined.
503        // 2) A property in X or Y dimension can be animated along a path: the property needs to be
504        //    defined in propertyXName or propertyYName attribute, the path will be defined in the
505        //    pathData attribute. valueFrom and valueTo will not be necessary for this animation.
506        // 3) PathInterpolator can also define a path (in pathData) for its interpolation curve.
507        // Here we are dealing with case 2:
508        if (pathData != null) {
509            String propertyXName =
510                    arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyXName);
511            String propertyYName =
512                    arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyYName);
513
514            if (valueType == VALUE_TYPE_PATH || valueType == VALUE_TYPE_UNDEFINED) {
515                // When pathData is defined, we are in case #2 mentioned above. ValueType can only
516                // be float type, or int type. Otherwise we fallback to default type.
517                valueType = VALUE_TYPE_FLOAT;
518            }
519            if (propertyXName == null && propertyYName == null) {
520                throw new InflateException(arrayObjectAnimator.getPositionDescription()
521                        + " propertyXName or propertyYName is needed for PathData");
522            } else {
523                Path path = PathParser.createPathFromPathData(pathData);
524                float error = 0.5f * pixelSize; // max half a pixel error
525                PathKeyframes keyframeSet = KeyframeSet.ofPath(path, error);
526                Keyframes xKeyframes;
527                Keyframes yKeyframes;
528                if (valueType == VALUE_TYPE_FLOAT) {
529                    xKeyframes = keyframeSet.createXFloatKeyframes();
530                    yKeyframes = keyframeSet.createYFloatKeyframes();
531                } else {
532                    xKeyframes = keyframeSet.createXIntKeyframes();
533                    yKeyframes = keyframeSet.createYIntKeyframes();
534                }
535                PropertyValuesHolder x = null;
536                PropertyValuesHolder y = null;
537                if (propertyXName != null) {
538                    x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes);
539                }
540                if (propertyYName != null) {
541                    y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes);
542                }
543                if (x == null) {
544                    oa.setValues(y);
545                } else if (y == null) {
546                    oa.setValues(x);
547                } else {
548                    oa.setValues(x, y);
549                }
550            }
551        } else {
552            String propertyName =
553                    arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyName);
554            oa.setPropertyName(propertyName);
555        }
556    }
557
558    /**
559     * Setup ValueAnimator's values.
560     * This will handle all of the integer, float and color types.
561     *
562     * @param anim The target Animator which will be updated.
563     * @param arrayAnimator TypedArray for the ValueAnimator.
564     * @param getFloats True if the value type is float.
565     * @param hasFrom True if "valueFrom" exists.
566     * @param fromType The type of "valueFrom".
567     * @param hasTo True if "valueTo" exists.
568     * @param toType The type of "valueTo".
569     */
570    private static void setupValues(ValueAnimator anim, TypedArray arrayAnimator,
571            boolean getFloats, boolean hasFrom, int fromType, boolean hasTo, int toType) {
572        int valueFromIndex = R.styleable.Animator_valueFrom;
573        int valueToIndex = R.styleable.Animator_valueTo;
574        if (getFloats) {
575            float valueFrom;
576            float valueTo;
577            if (hasFrom) {
578                if (fromType == TypedValue.TYPE_DIMENSION) {
579                    valueFrom = arrayAnimator.getDimension(valueFromIndex, 0f);
580                } else {
581                    valueFrom = arrayAnimator.getFloat(valueFromIndex, 0f);
582                }
583                if (hasTo) {
584                    if (toType == TypedValue.TYPE_DIMENSION) {
585                        valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
586                    } else {
587                        valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
588                    }
589                    anim.setFloatValues(valueFrom, valueTo);
590                } else {
591                    anim.setFloatValues(valueFrom);
592                }
593            } else {
594                if (toType == TypedValue.TYPE_DIMENSION) {
595                    valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
596                } else {
597                    valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
598                }
599                anim.setFloatValues(valueTo);
600            }
601        } else {
602            int valueFrom;
603            int valueTo;
604            if (hasFrom) {
605                if (fromType == TypedValue.TYPE_DIMENSION) {
606                    valueFrom = (int) arrayAnimator.getDimension(valueFromIndex, 0f);
607                } else if (isColorType(fromType)) {
608                    valueFrom = arrayAnimator.getColor(valueFromIndex, 0);
609                } else {
610                    valueFrom = arrayAnimator.getInt(valueFromIndex, 0);
611                }
612                if (hasTo) {
613                    if (toType == TypedValue.TYPE_DIMENSION) {
614                        valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
615                    } else if (isColorType(toType)) {
616                        valueTo = arrayAnimator.getColor(valueToIndex, 0);
617                    } else {
618                        valueTo = arrayAnimator.getInt(valueToIndex, 0);
619                    }
620                    anim.setIntValues(valueFrom, valueTo);
621                } else {
622                    anim.setIntValues(valueFrom);
623                }
624            } else {
625                if (hasTo) {
626                    if (toType == TypedValue.TYPE_DIMENSION) {
627                        valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
628                    } else if (isColorType(toType)) {
629                        valueTo = arrayAnimator.getColor(valueToIndex, 0);
630                    } else {
631                        valueTo = arrayAnimator.getInt(valueToIndex, 0);
632                    }
633                    anim.setIntValues(valueTo);
634                }
635            }
636        }
637    }
638
639    private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
640            float pixelSize)
641            throws XmlPullParserException, IOException {
642        return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0,
643                pixelSize);
644    }
645
646    private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
647            AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)
648            throws XmlPullParserException, IOException {
649        Animator anim = null;
650        ArrayList<Animator> childAnims = null;
651
652        // Make sure we are on a start tag.
653        int type;
654        int depth = parser.getDepth();
655
656        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
657                && type != XmlPullParser.END_DOCUMENT) {
658
659            if (type != XmlPullParser.START_TAG) {
660                continue;
661            }
662
663            String name = parser.getName();
664            boolean gotValues = false;
665
666            if (name.equals("objectAnimator")) {
667                anim = loadObjectAnimator(res, theme, attrs, pixelSize);
668            } else if (name.equals("animator")) {
669                anim = loadAnimator(res, theme, attrs, null, pixelSize);
670            } else if (name.equals("set")) {
671                anim = new AnimatorSet();
672                TypedArray a;
673                if (theme != null) {
674                    a = theme.obtainStyledAttributes(attrs, R.styleable.AnimatorSet, 0, 0);
675                } else {
676                    a = res.obtainAttributes(attrs, R.styleable.AnimatorSet);
677                }
678                anim.appendChangingConfigurations(a.getChangingConfigurations());
679                int ordering = a.getInt(R.styleable.AnimatorSet_ordering, TOGETHER);
680                createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering,
681                        pixelSize);
682                a.recycle();
683            } else if (name.equals("propertyValuesHolder")) {
684                PropertyValuesHolder[] values = loadValues(res, theme, parser,
685                        Xml.asAttributeSet(parser));
686                if (values != null && anim != null && (anim instanceof ValueAnimator)) {
687                    ((ValueAnimator) anim).setValues(values);
688                }
689                gotValues = true;
690            } else {
691                throw new RuntimeException("Unknown animator name: " + parser.getName());
692            }
693
694            if (parent != null && !gotValues) {
695                if (childAnims == null) {
696                    childAnims = new ArrayList<Animator>();
697                }
698                childAnims.add(anim);
699            }
700        }
701        if (parent != null && childAnims != null) {
702            Animator[] animsArray = new Animator[childAnims.size()];
703            int index = 0;
704            for (Animator a : childAnims) {
705                animsArray[index++] = a;
706            }
707            if (sequenceOrdering == TOGETHER) {
708                parent.playTogether(animsArray);
709            } else {
710                parent.playSequentially(animsArray);
711            }
712        }
713        return anim;
714    }
715
716    private static PropertyValuesHolder[] loadValues(Resources res, Theme theme,
717            XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
718        ArrayList<PropertyValuesHolder> values = null;
719
720        int type;
721        while ((type = parser.getEventType()) != XmlPullParser.END_TAG &&
722                type != XmlPullParser.END_DOCUMENT) {
723
724            if (type != XmlPullParser.START_TAG) {
725                parser.next();
726                continue;
727            }
728
729            String name = parser.getName();
730
731            if (name.equals("propertyValuesHolder")) {
732                TypedArray a;
733                if (theme != null) {
734                    a = theme.obtainStyledAttributes(attrs, R.styleable.PropertyValuesHolder, 0, 0);
735                } else {
736                    a = res.obtainAttributes(attrs, R.styleable.PropertyValuesHolder);
737                }
738                String propertyName = a.getString(R.styleable.PropertyValuesHolder_propertyName);
739                int valueType = a.getInt(R.styleable.PropertyValuesHolder_valueType,
740                        VALUE_TYPE_UNDEFINED);
741
742                PropertyValuesHolder pvh = loadPvh(res, theme, parser, propertyName, valueType);
743                if (pvh == null) {
744                    pvh = getPVH(a, valueType,
745                            R.styleable.PropertyValuesHolder_valueFrom,
746                            R.styleable.PropertyValuesHolder_valueTo, propertyName);
747                }
748                if (pvh != null) {
749                    if (values == null) {
750                        values = new ArrayList<PropertyValuesHolder>();
751                    }
752                    values.add(pvh);
753                }
754                a.recycle();
755            }
756
757            parser.next();
758        }
759
760        PropertyValuesHolder[] valuesArray = null;
761        if (values != null) {
762            int count = values.size();
763            valuesArray = new PropertyValuesHolder[count];
764            for (int i = 0; i < count; ++i) {
765                valuesArray[i] = values.get(i);
766            }
767        }
768        return valuesArray;
769    }
770
771    // When no value type is provided in keyframe, we need to infer the type from the value. i.e.
772    // if value is defined in the style of a color value, then the color type is returned.
773    // Otherwise, default float type is returned.
774    private static int inferValueTypeOfKeyframe(Resources res, Theme theme, AttributeSet attrs) {
775        int valueType;
776        TypedArray a;
777        if (theme != null) {
778            a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0);
779        } else {
780            a = res.obtainAttributes(attrs, R.styleable.Keyframe);
781        }
782
783        TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value);
784        boolean hasValue = (keyframeValue != null);
785        // When no value type is provided, check whether it's a color type first.
786        // If not, fall back to default value type (i.e. float type).
787        if (hasValue && isColorType(keyframeValue.type)) {
788            valueType = VALUE_TYPE_COLOR;
789        } else {
790            valueType = VALUE_TYPE_FLOAT;
791        }
792        a.recycle();
793        return valueType;
794    }
795
796    private static int inferValueTypeFromValues(TypedArray styledAttributes, int valueFromId,
797            int valueToId) {
798        TypedValue tvFrom = styledAttributes.peekValue(valueFromId);
799        boolean hasFrom = (tvFrom != null);
800        int fromType = hasFrom ? tvFrom.type : 0;
801        TypedValue tvTo = styledAttributes.peekValue(valueToId);
802        boolean hasTo = (tvTo != null);
803        int toType = hasTo ? tvTo.type : 0;
804
805        int valueType;
806        // Check whether it's color type. If not, fall back to default type (i.e. float type)
807        if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) {
808            valueType = VALUE_TYPE_COLOR;
809        } else {
810            valueType = VALUE_TYPE_FLOAT;
811        }
812        return valueType;
813    }
814
815    private static void dumpKeyframes(Object[] keyframes, String header) {
816        if (keyframes == null || keyframes.length == 0) {
817            return;
818        }
819        Log.d(TAG, header);
820        int count = keyframes.length;
821        for (int i = 0; i < count; ++i) {
822            Keyframe keyframe = (Keyframe) keyframes[i];
823            Log.d(TAG, "Keyframe " + i + ": fraction " +
824                    (keyframe.getFraction() < 0 ? "null" : keyframe.getFraction()) + ", " +
825                    ", value : " + ((keyframe.hasValue()) ? keyframe.getValue() : "null"));
826        }
827    }
828
829    // Load property values holder if there are keyframes defined in it. Otherwise return null.
830    private static PropertyValuesHolder loadPvh(Resources res, Theme theme, XmlPullParser parser,
831            String propertyName, int valueType)
832            throws XmlPullParserException, IOException {
833
834        PropertyValuesHolder value = null;
835        ArrayList<Keyframe> keyframes = null;
836
837        int type;
838        while ((type = parser.next()) != XmlPullParser.END_TAG &&
839                type != XmlPullParser.END_DOCUMENT) {
840            String name = parser.getName();
841            if (name.equals("keyframe")) {
842                if (valueType == VALUE_TYPE_UNDEFINED) {
843                    valueType = inferValueTypeOfKeyframe(res, theme, Xml.asAttributeSet(parser));
844                }
845                Keyframe keyframe = loadKeyframe(res, theme, Xml.asAttributeSet(parser), valueType);
846                if (keyframe != null) {
847                    if (keyframes == null) {
848                        keyframes = new ArrayList<Keyframe>();
849                    }
850                    keyframes.add(keyframe);
851                }
852                parser.next();
853            }
854        }
855
856        int count;
857        if (keyframes != null && (count = keyframes.size()) > 0) {
858            // make sure we have keyframes at 0 and 1
859            // If we have keyframes with set fractions, add keyframes at start/end
860            // appropriately. If start/end have no set fractions:
861            // if there's only one keyframe, set its fraction to 1 and add one at 0
862            // if >1 keyframe, set the last fraction to 1, the first fraction to 0
863            Keyframe firstKeyframe = keyframes.get(0);
864            Keyframe lastKeyframe = keyframes.get(count - 1);
865            float endFraction = lastKeyframe.getFraction();
866            if (endFraction < 1) {
867                if (endFraction < 0) {
868                    lastKeyframe.setFraction(1);
869                } else {
870                    keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1));
871                    ++count;
872                }
873            }
874            float startFraction = firstKeyframe.getFraction();
875            if (startFraction != 0) {
876                if (startFraction < 0) {
877                    firstKeyframe.setFraction(0);
878                } else {
879                    keyframes.add(0, createNewKeyframe(firstKeyframe, 0));
880                    ++count;
881                }
882            }
883            Keyframe[] keyframeArray = new Keyframe[count];
884            keyframes.toArray(keyframeArray);
885            for (int i = 0; i < count; ++i) {
886                Keyframe keyframe = keyframeArray[i];
887                if (keyframe.getFraction() < 0) {
888                    if (i == 0) {
889                        keyframe.setFraction(0);
890                    } else if (i == count - 1) {
891                        keyframe.setFraction(1);
892                    } else {
893                        // figure out the start/end parameters of the current gap
894                        // in fractions and distribute the gap among those keyframes
895                        int startIndex = i;
896                        int endIndex = i;
897                        for (int j = startIndex + 1; j < count - 1; ++j) {
898                            if (keyframeArray[j].getFraction() >= 0) {
899                                break;
900                            }
901                            endIndex = j;
902                        }
903                        float gap = keyframeArray[endIndex + 1].getFraction() -
904                                keyframeArray[startIndex - 1].getFraction();
905                        distributeKeyframes(keyframeArray, gap, startIndex, endIndex);
906                    }
907                }
908            }
909            value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray);
910            if (valueType == VALUE_TYPE_COLOR) {
911                value.setEvaluator(ArgbEvaluator.getInstance());
912            }
913        }
914
915        return value;
916    }
917
918    private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) {
919        return sampleKeyframe.getType() == float.class ?
920                            Keyframe.ofFloat(fraction) :
921                            (sampleKeyframe.getType() == int.class) ?
922                                    Keyframe.ofInt(fraction) :
923                                    Keyframe.ofObject(fraction);
924    }
925
926    /**
927     * Utility function to set fractions on keyframes to cover a gap in which the
928     * fractions are not currently set. Keyframe fractions will be distributed evenly
929     * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap
930     * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the
931     * keyframe before startIndex.
932     * Assumptions:
933     * - First and last keyframe fractions (bounding this spread) are already set. So,
934     * for example, if no fractions are set, we will already set first and last keyframe
935     * fraction values to 0 and 1.
936     * - startIndex must be >0 (which follows from first assumption).
937     * - endIndex must be >= startIndex.
938     *
939     * @param keyframes the array of keyframes
940     * @param gap The total gap we need to distribute
941     * @param startIndex The index of the first keyframe whose fraction must be set
942     * @param endIndex The index of the last keyframe whose fraction must be set
943     */
944    private static void distributeKeyframes(Keyframe[] keyframes, float gap,
945            int startIndex, int endIndex) {
946        int count = endIndex - startIndex + 2;
947        float increment = gap / count;
948        for (int i = startIndex; i <= endIndex; ++i) {
949            keyframes[i].setFraction(keyframes[i-1].getFraction() + increment);
950        }
951    }
952
953    private static Keyframe loadKeyframe(Resources res, Theme theme, AttributeSet attrs,
954            int valueType)
955            throws XmlPullParserException, IOException {
956
957        TypedArray a;
958        if (theme != null) {
959            a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0);
960        } else {
961            a = res.obtainAttributes(attrs, R.styleable.Keyframe);
962        }
963
964        Keyframe keyframe = null;
965
966        float fraction = a.getFloat(R.styleable.Keyframe_fraction, -1);
967
968        TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value);
969        boolean hasValue = (keyframeValue != null);
970        if (valueType == VALUE_TYPE_UNDEFINED) {
971            // When no value type is provided, check whether it's a color type first.
972            // If not, fall back to default value type (i.e. float type).
973            if (hasValue && isColorType(keyframeValue.type)) {
974                valueType = VALUE_TYPE_COLOR;
975            } else {
976                valueType = VALUE_TYPE_FLOAT;
977            }
978        }
979
980        if (hasValue) {
981            switch (valueType) {
982                case VALUE_TYPE_FLOAT:
983                    float value = a.getFloat(R.styleable.Keyframe_value, 0);
984                    keyframe = Keyframe.ofFloat(fraction, value);
985                    break;
986                case VALUE_TYPE_COLOR:
987                case VALUE_TYPE_INT:
988                    int intValue = a.getInt(R.styleable.Keyframe_value, 0);
989                    keyframe = Keyframe.ofInt(fraction, intValue);
990                    break;
991            }
992        } else {
993            keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) :
994                    Keyframe.ofInt(fraction);
995        }
996
997        final int resID = a.getResourceId(R.styleable.Keyframe_interpolator, 0);
998        if (resID > 0) {
999            final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID);
1000            keyframe.setInterpolator(interpolator);
1001        }
1002        a.recycle();
1003
1004        return keyframe;
1005    }
1006
1007    private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs,
1008            float pathErrorScale) throws NotFoundException {
1009        ObjectAnimator anim = new ObjectAnimator();
1010
1011        loadAnimator(res, theme, attrs, anim, pathErrorScale);
1012
1013        return anim;
1014    }
1015
1016    /**
1017     * Creates a new animation whose parameters come from the specified context
1018     * and attributes set.
1019     *
1020     * @param res The resources
1021     * @param attrs The set of attributes holding the animation parameters
1022     * @param anim Null if this is a ValueAnimator, otherwise this is an
1023     *            ObjectAnimator
1024     */
1025    private static ValueAnimator loadAnimator(Resources res, Theme theme,
1026            AttributeSet attrs, ValueAnimator anim, float pathErrorScale)
1027            throws NotFoundException {
1028        TypedArray arrayAnimator = null;
1029        TypedArray arrayObjectAnimator = null;
1030
1031        if (theme != null) {
1032            arrayAnimator = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0);
1033        } else {
1034            arrayAnimator = res.obtainAttributes(attrs, R.styleable.Animator);
1035        }
1036
1037        // If anim is not null, then it is an object animator.
1038        if (anim != null) {
1039            if (theme != null) {
1040                arrayObjectAnimator = theme.obtainStyledAttributes(attrs,
1041                        R.styleable.PropertyAnimator, 0, 0);
1042            } else {
1043                arrayObjectAnimator = res.obtainAttributes(attrs, R.styleable.PropertyAnimator);
1044            }
1045            anim.appendChangingConfigurations(arrayObjectAnimator.getChangingConfigurations());
1046        }
1047
1048        if (anim == null) {
1049            anim = new ValueAnimator();
1050        }
1051        anim.appendChangingConfigurations(arrayAnimator.getChangingConfigurations());
1052
1053        parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale);
1054
1055        final int resID = arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0);
1056        if (resID > 0) {
1057            final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID);
1058            if (interpolator instanceof BaseInterpolator) {
1059                anim.appendChangingConfigurations(
1060                        ((BaseInterpolator) interpolator).getChangingConfiguration());
1061            }
1062            anim.setInterpolator(interpolator);
1063        }
1064
1065        arrayAnimator.recycle();
1066        if (arrayObjectAnimator != null) {
1067            arrayObjectAnimator.recycle();
1068        }
1069        return anim;
1070    }
1071
1072    private static @Config int getChangingConfigs(@NonNull Resources resources, @AnyRes int id) {
1073        synchronized (sTmpTypedValue) {
1074            resources.getValue(id, sTmpTypedValue, true);
1075            return sTmpTypedValue.changingConfigurations;
1076        }
1077    }
1078
1079    private static boolean isColorType(int type) {
1080       return (type >= TypedValue.TYPE_FIRST_COLOR_INT) && (type <= TypedValue.TYPE_LAST_COLOR_INT);
1081    }
1082}
1083