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