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