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