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