1 /*
2  * Copyright (C) 2018 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 androidx.core.animation;
17 
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.content.res.Resources.NotFoundException;
21 import android.content.res.Resources.Theme;
22 import android.content.res.TypedArray;
23 import android.content.res.XmlResourceParser;
24 import android.graphics.Path;
25 import android.util.AttributeSet;
26 import android.util.TypedValue;
27 import android.util.Xml;
28 import android.view.InflateException;
29 
30 import androidx.annotation.AnimatorRes;
31 import androidx.annotation.InterpolatorRes;
32 import androidx.core.graphics.PathParser;
33 
34 import org.jspecify.annotations.NonNull;
35 import org.jspecify.annotations.Nullable;
36 import org.xmlpull.v1.XmlPullParser;
37 import org.xmlpull.v1.XmlPullParserException;
38 
39 import java.io.IOException;
40 import java.util.ArrayList;
41 
42 /**
43  * This class is used to instantiate animator XML files into Animator objects.
44  * <p>
45  * For performance reasons, inflation relies heavily on pre-processing of
46  * XML files that is done at build time. Therefore, it is not currently possible
47  * to use this inflater with an XmlPullParser over a plain XML file at runtime;
48  * it only works with an XmlPullParser returned from a compiled resource (R.
49  * <em>something</em> file.)
50  */
51 public class AnimatorInflater {
52     private static final String TAG = "AnimatorInflater";
53     /**
54      * These flags are used when parsing AnimatorSet objects
55      */
56     private static final int TOGETHER = 0;
57     @SuppressWarnings("unused") // kept around for parity with XML values.
58     private static final int SEQUENTIALLY = 1;
59 
60     /**
61      * Enum values used in XML attributes to indicate the value for mValueType
62      */
63     private static final int VALUE_TYPE_FLOAT       = 0;
64     private static final int VALUE_TYPE_INT         = 1;
65     private static final int VALUE_TYPE_PATH        = 2;
66     private static final int VALUE_TYPE_COLOR       = 3;
67     private static final int VALUE_TYPE_UNDEFINED   = 4;
68 
AnimatorInflater()69     private AnimatorInflater() {}
70 
71     /**
72      * Loads an {@link Animator} object from a resource
73      *
74      * @param context Application context used to access resources
75      * @param id The resource id of the animation to load
76      * @return The animator object reference by the specified id
77      * @throws NotFoundException when the animation cannot be loaded
78      */
loadAnimator(@onNull Context context, @AnimatorRes int id)79     public static @NonNull Animator loadAnimator(@NonNull Context context, @AnimatorRes int id)
80             throws NotFoundException {
81         return loadAnimator(context.getResources(), context.getTheme(), id);
82     }
83 
84     /**
85      * Loads an {@link Animator} object from a resource
86      *
87      * @param resources The resources
88      * @param theme The theme
89      * @param id The resource id of the animation to load
90      * @return The animator object reference by the specified id
91      * @throws NotFoundException when the animation cannot be loaded
92      */
loadAnimator(@onNull Resources resources, @Nullable Theme theme, @AnimatorRes int id)93     public static @NonNull Animator loadAnimator(@NonNull Resources resources,
94             @Nullable Theme theme, @AnimatorRes int id) throws NotFoundException {
95         return loadAnimator(resources, theme, id, 1);
96     }
97 
loadAnimator(Resources resources, Theme theme, int id, float pathErrorScale)98     static Animator loadAnimator(Resources resources, Theme theme, int id, float pathErrorScale)
99             throws NotFoundException {
100 
101         Animator animator;
102         XmlResourceParser parser = null;
103         try {
104             parser = resources.getAnimation(id);
105             animator = createAnimatorFromXml(resources, theme, parser, pathErrorScale);
106             return animator;
107         } catch (XmlPullParserException ex) {
108             Resources.NotFoundException rnf =
109                     new Resources.NotFoundException("Can't load animation resource ID #0x"
110                             + Integer.toHexString(id));
111             rnf.initCause(ex);
112             throw rnf;
113         } catch (IOException ex) {
114             Resources.NotFoundException rnf =
115                     new Resources.NotFoundException("Can't load animation resource ID #0x"
116                             + Integer.toHexString(id));
117             rnf.initCause(ex);
118             throw rnf;
119         } finally {
120             if (parser != null) parser.close();
121         }
122     }
123 
124     /**
125      * PathDataEvaluator is used to interpolate between two paths which are
126      * represented in the same format but different control points' values.
127      * The path is represented as verbs and points for each of the verbs.
128      *
129      * An instance of this class cannot be reused for different paths as its
130      * buffer array is structured to match the first path pattern.
131      */
132     static class PathDataEvaluator implements TypeEvaluator<PathParser.PathDataNode[]> {
133         private PathParser.PathDataNode[] mPathData;
134 
135         @Override
evaluate( float fraction, PathParser.PathDataNode @NonNull [] startPathData, PathParser.PathDataNode @NonNull [] endPathData)136         public PathParser.PathDataNode @NonNull [] evaluate(
137                 float fraction, PathParser.PathDataNode @NonNull [] startPathData,
138                 PathParser.PathDataNode @NonNull [] endPathData) {
139             if (mPathData == null) {
140                 // This path buffer has to have the same size and structure as the morphing path.
141                 mPathData = PathParser.deepCopyNodes(endPathData);
142             }
143             PathParser.interpolatePathDataNodes(mPathData, fraction, startPathData, endPathData);
144             return mPathData;
145         }
146     }
147 
getPVH(TypedArray styledAttributes, int valueType, int valueFromId, int valueToId, String propertyName)148     private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType,
149             int valueFromId, int valueToId, String propertyName) {
150 
151         TypedValue tvFrom = styledAttributes.peekValue(valueFromId);
152         boolean hasFrom = (tvFrom != null);
153         int fromType = hasFrom ? tvFrom.type : 0;
154         TypedValue tvTo = styledAttributes.peekValue(valueToId);
155         boolean hasTo = (tvTo != null);
156         int toType = hasTo ? tvTo.type : 0;
157 
158         if (valueType == VALUE_TYPE_UNDEFINED) {
159             // Check whether it's color type. If not, fall back to default type (i.e. float type)
160             if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) {
161                 valueType = VALUE_TYPE_COLOR;
162             } else {
163                 valueType = VALUE_TYPE_FLOAT;
164             }
165         }
166 
167         boolean getFloats = (valueType == VALUE_TYPE_FLOAT);
168 
169         PropertyValuesHolder returnValue = null;
170 
171         if (valueType == VALUE_TYPE_PATH) {
172             String fromString = styledAttributes.getString(valueFromId);
173             String toString = styledAttributes.getString(valueToId);
174             PathParser.PathDataNode[] nodesFrom = fromString == null
175                     ? null : PathParser.createNodesFromPathData(fromString);
176             PathParser.PathDataNode[] nodesTo = toString == null
177                     ? null : PathParser.createNodesFromPathData(toString);
178 
179             if (nodesFrom != null || nodesTo != null) {
180                 if (nodesFrom != null) {
181                     TypeEvaluator evaluator = new PathDataEvaluator();
182                     if (nodesTo != null) {
183                         if (!PathParser.canMorph(nodesFrom, nodesTo)) {
184                             throw new InflateException(" Can't morph from " + fromString + " to "
185                                     + toString);
186                         }
187                         returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
188                                 nodesFrom, nodesTo);
189                     } else {
190                         returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
191                                 (Object) nodesFrom);
192                     }
193                 } else if (nodesTo != null) {
194                     TypeEvaluator evaluator = new PathDataEvaluator();
195                     returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
196                             (Object) nodesTo);
197                 }
198             }
199         } else {
200             TypeEvaluator evaluator = null;
201             // Integer and float value types are handled here.
202             if (valueType == VALUE_TYPE_COLOR) {
203                 // special case for colors: ignore valueType and get ints
204                 evaluator = ArgbEvaluator.getInstance();
205             }
206             if (getFloats) {
207                 float valueFrom;
208                 float valueTo;
209                 if (hasFrom) {
210                     if (fromType == TypedValue.TYPE_DIMENSION) {
211                         valueFrom = styledAttributes.getDimension(valueFromId, 0f);
212                     } else {
213                         valueFrom = styledAttributes.getFloat(valueFromId, 0f);
214                     }
215                     if (hasTo) {
216                         if (toType == TypedValue.TYPE_DIMENSION) {
217                             valueTo = styledAttributes.getDimension(valueToId, 0f);
218                         } else {
219                             valueTo = styledAttributes.getFloat(valueToId, 0f);
220                         }
221                         returnValue = PropertyValuesHolder.ofFloat(propertyName,
222                                 valueFrom, valueTo);
223                     } else {
224                         returnValue = PropertyValuesHolder.ofFloat(propertyName, valueFrom);
225                     }
226                 } else {
227                     if (toType == TypedValue.TYPE_DIMENSION) {
228                         valueTo = styledAttributes.getDimension(valueToId, 0f);
229                     } else {
230                         valueTo = styledAttributes.getFloat(valueToId, 0f);
231                     }
232                     returnValue = PropertyValuesHolder.ofFloat(propertyName, valueTo);
233                 }
234             } else {
235                 int valueFrom;
236                 int valueTo;
237                 if (hasFrom) {
238                     if (fromType == TypedValue.TYPE_DIMENSION) {
239                         valueFrom = (int) styledAttributes.getDimension(valueFromId, 0f);
240                     } else if (isColorType(fromType)) {
241                         valueFrom = styledAttributes.getColor(valueFromId, 0);
242                     } else {
243                         valueFrom = styledAttributes.getInt(valueFromId, 0);
244                     }
245                     if (hasTo) {
246                         if (toType == TypedValue.TYPE_DIMENSION) {
247                             valueTo = (int) styledAttributes.getDimension(valueToId, 0f);
248                         } else if (isColorType(toType)) {
249                             valueTo = styledAttributes.getColor(valueToId, 0);
250                         } else {
251                             valueTo = styledAttributes.getInt(valueToId, 0);
252                         }
253                         returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom, valueTo);
254                     } else {
255                         returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom);
256                     }
257                 } else {
258                     if (hasTo) {
259                         if (toType == TypedValue.TYPE_DIMENSION) {
260                             valueTo = (int) styledAttributes.getDimension(valueToId, 0f);
261                         } else if (isColorType(toType)) {
262                             valueTo = styledAttributes.getColor(valueToId, 0);
263                         } else {
264                             valueTo = styledAttributes.getInt(valueToId, 0);
265                         }
266                         returnValue = PropertyValuesHolder.ofInt(propertyName, valueTo);
267                     }
268                 }
269             }
270             if (returnValue != null && evaluator != null) {
271                 returnValue.setEvaluator(evaluator);
272             }
273         }
274 
275         return returnValue;
276     }
277 
278     /**
279      * @param anim The animator, must not be null
280      * @param arrayAnimator Incoming typed array for Animator's attributes.
281      * @param arrayObjectAnimator Incoming typed array for Object Animator's
282      *            attributes.
283      * @param pixelSize The relative pixel size, used to calculate the
284      *                  maximum error for path animations.
285      */
parseAnimatorFromTypeArray(ValueAnimator anim, TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize)286     private static void parseAnimatorFromTypeArray(ValueAnimator anim,
287             TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) {
288         long duration = arrayAnimator.getInt(AndroidResources.STYLEABLE_ANIMATOR_DURATION, 300);
289 
290         long startDelay = arrayAnimator.getInt(AndroidResources.STYLEABLE_ANIMATOR_START_OFFSET, 0);
291 
292         int valueType = arrayAnimator.getInt(AndroidResources.STYLEABLE_ANIMATOR_VALUE_TYPE,
293                 VALUE_TYPE_UNDEFINED);
294 
295         if (valueType == VALUE_TYPE_UNDEFINED) {
296             valueType = inferValueTypeFromValues(arrayAnimator,
297                     AndroidResources.STYLEABLE_ANIMATOR_VALUE_FROM,
298                     AndroidResources.STYLEABLE_ANIMATOR_VALUE_TO);
299         }
300         PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType,
301                 AndroidResources.STYLEABLE_ANIMATOR_VALUE_FROM,
302                 AndroidResources.STYLEABLE_ANIMATOR_VALUE_TO, "");
303         if (pvh != null) {
304             anim.setValues(pvh);
305         }
306 
307         anim.setDuration(duration);
308         anim.setStartDelay(startDelay);
309 
310         if (arrayAnimator.hasValue(AndroidResources.STYLEABLE_ANIMATOR_REPEAT_COUNT)) {
311             anim.setRepeatCount(
312                     arrayAnimator.getInt(AndroidResources.STYLEABLE_ANIMATOR_REPEAT_COUNT, 0));
313         }
314         if (arrayAnimator.hasValue(AndroidResources.STYLEABLE_ANIMATOR_REPEAT_MODE)) {
315             anim.setRepeatMode(
316                     arrayAnimator.getInt(AndroidResources.STYLEABLE_ANIMATOR_REPEAT_MODE,
317                             ValueAnimator.RESTART));
318         }
319 
320         if (arrayObjectAnimator != null) {
321             setupObjectAnimator(anim, arrayObjectAnimator, valueType, pixelSize);
322         }
323     }
324 
325     /**
326      * Setup ObjectAnimator's property or values from pathData.
327      *
328      * @param anim The target Animator which will be updated.
329      * @param arrayObjectAnimator TypedArray for the ObjectAnimator.
330      * @param valueType the type of value that could be any of VALUE_TYPE_INT, VALUE_TYPE_FLOAT,
331      *                 VALUE_TYPE_COLOR, VALUE_TYPE_PATH, VALUE_TYPE_UNDEFINED
332      * @param pixelSize The relative pixel size, used to calculate the
333      *                  maximum error for path animations.
334      */
setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator, int valueType, float pixelSize)335     private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator,
336             int valueType, float pixelSize) {
337         ObjectAnimator oa = (ObjectAnimator) anim;
338         String pathData = null;
339 
340         // This works around an issue in API 19 where TypedArray.getString(int) returns a reference
341         // wrapped in a string when the attribute at given index isn't defined.
342         TypedValue typedValue = new TypedValue();
343         arrayObjectAnimator.getValue(
344                 AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PATH_DATA, typedValue);
345         if (typedValue.type == TypedValue.TYPE_STRING) {
346             pathData = typedValue.string.toString();
347         }
348 
349         // Path can be involved in an ObjectAnimator in the following 3 ways:
350         // 1) Path morphing: the property to be animated is pathData, and valueFrom and valueTo
351         //    are both of pathType. valueType = pathType needs to be explicitly defined.
352         // 2) A property in X or Y dimension can be animated along a path: the property needs to be
353         //    defined in propertyXName or propertyYName attribute, the path will be defined in the
354         //    pathData attribute. valueFrom and valueTo will not be necessary for this animation.
355         // 3) PathInterpolator can also define a path (in pathData) for its interpolation curve.
356         // Here we are dealing with case 2:
357         if (pathData != null) {
358             String propertyXName = arrayObjectAnimator.getString(
359                     AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_X_NAME);
360             String propertyYName = arrayObjectAnimator.getString(
361                     AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_Y_NAME);
362 
363             if (valueType == VALUE_TYPE_PATH || valueType == VALUE_TYPE_UNDEFINED) {
364                 // When pathData is defined, we are in case #2 mentioned above. ValueType can only
365                 // be float type, or int type. Otherwise we fallback to default type.
366                 valueType = VALUE_TYPE_FLOAT;
367             }
368             if (propertyXName == null && propertyYName == null) {
369                 throw new InflateException(arrayObjectAnimator.getPositionDescription()
370                         + " propertyXName or propertyYName is needed for PathData");
371             } else {
372                 Path path = PathParser.createPathFromPathData(pathData);
373                 float error = 0.5f * pixelSize; // max half a pixel error
374                 PathKeyframes keyframeSet = KeyframeSet.ofPath(path, error);
375                 Keyframes xKeyframes;
376                 Keyframes yKeyframes;
377                 if (valueType == VALUE_TYPE_FLOAT) {
378                     xKeyframes = keyframeSet.createXFloatKeyframes();
379                     yKeyframes = keyframeSet.createYFloatKeyframes();
380                 } else {
381                     xKeyframes = keyframeSet.createXIntKeyframes();
382                     yKeyframes = keyframeSet.createYIntKeyframes();
383                 }
384                 PropertyValuesHolder x = null;
385                 PropertyValuesHolder y = null;
386                 if (propertyXName != null) {
387                     x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes);
388                 }
389                 if (propertyYName != null) {
390                     y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes);
391                 }
392                 if (x == null) {
393                     oa.setValues(y);
394                 } else if (y == null) {
395                     oa.setValues(x);
396                 } else {
397                     oa.setValues(x, y);
398                 }
399             }
400         } else {
401             String propertyName = arrayObjectAnimator.getString(
402                     AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_NAME);
403             oa.setPropertyName(propertyName);
404         }
405     }
406 
createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, float pixelSize)407     private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
408             float pixelSize)
409             throws XmlPullParserException, IOException {
410         return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0,
411                 pixelSize);
412     }
413 
createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)414     private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
415             AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)
416             throws XmlPullParserException, IOException {
417         Animator anim = null;
418         ArrayList<Animator> childAnims = null;
419 
420         // Make sure we are on a start tag.
421         int type;
422         int depth = parser.getDepth();
423 
424         while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
425                 && type != XmlPullParser.END_DOCUMENT) {
426 
427             if (type != XmlPullParser.START_TAG) {
428                 continue;
429             }
430 
431             String name = parser.getName();
432             boolean gotValues = false;
433 
434             if (name.equals("objectAnimator")) {
435                 anim = loadObjectAnimator(res, theme, attrs, pixelSize);
436             } else if (name.equals("animator")) {
437                 anim = loadAnimator(res, theme, attrs, null, pixelSize);
438             } else if (name.equals("set")) {
439                 anim = new AnimatorSet();
440                 TypedArray a;
441                 if (theme != null) {
442                     a = theme.obtainStyledAttributes(attrs, AndroidResources.STYLEABLE_ANIMATOR_SET,
443                             0, 0);
444                 } else {
445                     a = res.obtainAttributes(attrs, AndroidResources.STYLEABLE_ANIMATOR_SET);
446                 }
447                 int ordering = a.getInt(AndroidResources.STYLEABLE_ANIMATOR_SET_ORDERING, TOGETHER);
448                 createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering,
449                         pixelSize);
450                 a.recycle();
451             } else if (name.equals("propertyValuesHolder")) {
452                 PropertyValuesHolder[] values = loadValues(res, theme, parser,
453                         Xml.asAttributeSet(parser));
454                 if (values != null && anim != null && (anim instanceof ValueAnimator)) {
455                     ((ValueAnimator) anim).setValues(values);
456                 }
457                 gotValues = true;
458             } else {
459                 throw new RuntimeException("Unknown animator name: " + parser.getName());
460             }
461 
462             if (parent != null && !gotValues) {
463                 if (childAnims == null) {
464                     childAnims = new ArrayList<Animator>();
465                 }
466                 childAnims.add(anim);
467             }
468         }
469         if (parent != null && childAnims != null) {
470             Animator[] animsArray = new Animator[childAnims.size()];
471             int index = 0;
472             for (Animator a : childAnims) {
473                 animsArray[index++] = a;
474             }
475             if (sequenceOrdering == TOGETHER) {
476                 parent.playTogether(animsArray);
477             } else {
478                 parent.playSequentially(animsArray);
479             }
480         }
481         return anim;
482     }
483 
loadValues(Resources res, Theme theme, XmlPullParser parser, AttributeSet attrs)484     private static PropertyValuesHolder[] loadValues(Resources res, Theme theme,
485             XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
486         ArrayList<PropertyValuesHolder> values = null;
487 
488         int type;
489         while ((type = parser.getEventType()) != XmlPullParser.END_TAG
490                 && type != XmlPullParser.END_DOCUMENT) {
491 
492             if (type != XmlPullParser.START_TAG) {
493                 parser.next();
494                 continue;
495             }
496 
497             String name = parser.getName();
498 
499             if (name.equals("propertyValuesHolder")) {
500                 TypedArray a;
501                 if (theme != null) {
502                     a = theme.obtainStyledAttributes(attrs,
503                             AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER, 0, 0);
504                 } else {
505                     a = res.obtainAttributes(attrs,
506                             AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER);
507                 }
508                 String propertyName = a.getString(
509                         AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER_PROPERTY_NAME);
510                 int valueType = a.getInt(
511                         AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TYPE,
512                         VALUE_TYPE_UNDEFINED);
513 
514                 PropertyValuesHolder pvh = loadPvh(res, theme, parser, propertyName, valueType);
515                 if (pvh == null) {
516                     pvh = getPVH(a, valueType,
517                             AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_FROM,
518                             AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TO,
519                             propertyName);
520                 }
521                 if (pvh != null) {
522                     if (values == null) {
523                         values = new ArrayList<PropertyValuesHolder>();
524                     }
525                     values.add(pvh);
526                 }
527                 a.recycle();
528             }
529 
530             parser.next();
531         }
532 
533         PropertyValuesHolder[] valuesArray = null;
534         if (values != null) {
535             int count = values.size();
536             valuesArray = new PropertyValuesHolder[count];
537             for (int i = 0; i < count; ++i) {
538                 valuesArray[i] = values.get(i);
539             }
540         }
541         return valuesArray;
542     }
543 
544     // When no value type is provided in keyframe, we need to infer the type from the value. i.e.
545     // if value is defined in the style of a color value, then the color type is returned.
546     // Otherwise, default float type is returned.
inferValueTypeOfKeyframe(Resources res, Theme theme, AttributeSet attrs)547     private static int inferValueTypeOfKeyframe(Resources res, Theme theme, AttributeSet attrs) {
548         int valueType;
549         TypedArray a;
550         if (theme != null) {
551             a = theme.obtainStyledAttributes(attrs, AndroidResources.STYLEABLE_KEYFRAME, 0, 0);
552         } else {
553             a = res.obtainAttributes(attrs, AndroidResources.STYLEABLE_KEYFRAME);
554         }
555 
556         TypedValue keyframeValue = a.peekValue(AndroidResources.STYLEABLE_KEYFRAME_VALUE);
557         boolean hasValue = (keyframeValue != null);
558         // When no value type is provided, check whether it's a color type first.
559         // If not, fall back to default value type (i.e. float type).
560         if (hasValue && isColorType(keyframeValue.type)) {
561             valueType = VALUE_TYPE_COLOR;
562         } else {
563             valueType = VALUE_TYPE_FLOAT;
564         }
565         a.recycle();
566         return valueType;
567     }
568 
inferValueTypeFromValues(TypedArray styledAttributes, int valueFromId, int valueToId)569     private static int inferValueTypeFromValues(TypedArray styledAttributes, int valueFromId,
570             int valueToId) {
571         TypedValue tvFrom = styledAttributes.peekValue(valueFromId);
572         boolean hasFrom = (tvFrom != null);
573         int fromType = hasFrom ? tvFrom.type : 0;
574         TypedValue tvTo = styledAttributes.peekValue(valueToId);
575         boolean hasTo = (tvTo != null);
576         int toType = hasTo ? tvTo.type : 0;
577 
578         int valueType;
579         // Check whether it's color type. If not, fall back to default type (i.e. float type)
580         if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) {
581             valueType = VALUE_TYPE_COLOR;
582         } else {
583             valueType = VALUE_TYPE_FLOAT;
584         }
585         return valueType;
586     }
587 
588     // 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)589     private static PropertyValuesHolder loadPvh(Resources res, Theme theme, XmlPullParser parser,
590             String propertyName, int valueType)
591             throws XmlPullParserException, IOException {
592 
593         PropertyValuesHolder value = null;
594         ArrayList<Keyframe> keyframes = null;
595 
596         int type;
597         while ((type = parser.next()) != XmlPullParser.END_TAG
598                 && type != XmlPullParser.END_DOCUMENT) {
599             String name = parser.getName();
600             if (name.equals("keyframe")) {
601                 if (valueType == VALUE_TYPE_UNDEFINED) {
602                     valueType = inferValueTypeOfKeyframe(res, theme, Xml.asAttributeSet(parser));
603                 }
604                 Keyframe keyframe = loadKeyframe(res, theme, Xml.asAttributeSet(parser), valueType);
605                 if (keyframe != null) {
606                     if (keyframes == null) {
607                         keyframes = new ArrayList<Keyframe>();
608                     }
609                     keyframes.add(keyframe);
610                 }
611                 parser.next();
612             }
613         }
614 
615         int count;
616         if (keyframes != null && (count = keyframes.size()) > 0) {
617             // make sure we have keyframes at 0 and 1
618             // If we have keyframes with set fractions, add keyframes at start/end
619             // appropriately. If start/end have no set fractions:
620             // if there's only one keyframe, set its fraction to 1 and add one at 0
621             // if >1 keyframe, set the last fraction to 1, the first fraction to 0
622             Keyframe firstKeyframe = keyframes.get(0);
623             Keyframe lastKeyframe = keyframes.get(count - 1);
624             float endFraction = lastKeyframe.getFraction();
625             if (endFraction < 1) {
626                 if (endFraction < 0) {
627                     lastKeyframe.setFraction(1);
628                 } else {
629                     keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1));
630                     ++count;
631                 }
632             }
633             float startFraction = firstKeyframe.getFraction();
634             if (startFraction != 0) {
635                 if (startFraction < 0) {
636                     firstKeyframe.setFraction(0);
637                 } else {
638                     keyframes.add(0, createNewKeyframe(firstKeyframe, 0));
639                     ++count;
640                 }
641             }
642             Keyframe[] keyframeArray = new Keyframe[count];
643             keyframes.toArray(keyframeArray);
644             for (int i = 0; i < count; ++i) {
645                 Keyframe keyframe = keyframeArray[i];
646                 if (keyframe.getFraction() < 0) {
647                     if (i == 0) {
648                         keyframe.setFraction(0);
649                     } else if (i == count - 1) {
650                         keyframe.setFraction(1);
651                     } else {
652                         // figure out the start/end parameters of the current gap
653                         // in fractions and distribute the gap among those keyframes
654                         int startIndex = i;
655                         int endIndex = i;
656                         for (int j = startIndex + 1; j < count - 1; ++j) {
657                             if (keyframeArray[j].getFraction() >= 0) {
658                                 break;
659                             }
660                             endIndex = j;
661                         }
662                         float gap = keyframeArray[endIndex + 1].getFraction()
663                                 - keyframeArray[startIndex - 1].getFraction();
664                         distributeKeyframes(keyframeArray, gap, startIndex, endIndex);
665                     }
666                 }
667             }
668             value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray);
669             if (valueType == VALUE_TYPE_COLOR) {
670                 value.setEvaluator(ArgbEvaluator.getInstance());
671             }
672         }
673 
674         return value;
675     }
676 
createNewKeyframe(Keyframe sampleKeyframe, float fraction)677     private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) {
678         Class<?> type = sampleKeyframe.getType();
679         if (type == float.class) {
680             return Keyframe.ofFloat(fraction);
681         } else if (type == int.class) {
682             return Keyframe.ofInt(fraction);
683         } else {
684             return Keyframe.ofObject(fraction);
685         }
686     }
687 
688     /**
689      * Utility function to set fractions on keyframes to cover a gap in which the
690      * fractions are not currently set. Keyframe fractions will be distributed evenly
691      * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap
692      * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the
693      * keyframe before startIndex.
694      * Assumptions:
695      * - First and last keyframe fractions (bounding this spread) are already set. So,
696      * for example, if no fractions are set, we will already set first and last keyframe
697      * fraction values to 0 and 1.
698      * - startIndex must be >0 (which follows from first assumption).
699      * - endIndex must be >= startIndex.
700      *
701      * @param keyframes the array of keyframes
702      * @param gap The total gap we need to distribute
703      * @param startIndex The index of the first keyframe whose fraction must be set
704      * @param endIndex The index of the last keyframe whose fraction must be set
705      */
distributeKeyframes(Keyframe[] keyframes, float gap, int startIndex, int endIndex)706     private static void distributeKeyframes(Keyframe[] keyframes, float gap,
707             int startIndex, int endIndex) {
708         int count = endIndex - startIndex + 2;
709         float increment = gap / count;
710         for (int i = startIndex; i <= endIndex; ++i) {
711             keyframes[i].setFraction(keyframes[i - 1].getFraction() + increment);
712         }
713     }
714 
loadKeyframe(Resources res, Theme theme, AttributeSet attrs, int valueType)715     private static Keyframe loadKeyframe(Resources res, Theme theme, AttributeSet attrs,
716             int valueType)
717             throws XmlPullParserException, IOException {
718 
719         TypedArray a;
720         if (theme != null) {
721             a = theme.obtainStyledAttributes(attrs, AndroidResources.STYLEABLE_KEYFRAME, 0, 0);
722         } else {
723             a = res.obtainAttributes(attrs, AndroidResources.STYLEABLE_KEYFRAME);
724         }
725 
726         Keyframe keyframe = null;
727 
728         float fraction = a.getFloat(AndroidResources.STYLEABLE_KEYFRAME_FRACTION, -1);
729 
730         TypedValue keyframeValue = a.peekValue(AndroidResources.STYLEABLE_KEYFRAME_VALUE);
731         boolean hasValue = (keyframeValue != null);
732         if (valueType == VALUE_TYPE_UNDEFINED) {
733             // When no value type is provided, check whether it's a color type first.
734             // If not, fall back to default value type (i.e. float type).
735             if (hasValue && isColorType(keyframeValue.type)) {
736                 valueType = VALUE_TYPE_COLOR;
737             } else {
738                 valueType = VALUE_TYPE_FLOAT;
739             }
740         }
741 
742         if (hasValue) {
743             switch (valueType) {
744                 case VALUE_TYPE_FLOAT:
745                     float value = a.getFloat(AndroidResources.STYLEABLE_KEYFRAME_VALUE, 0);
746                     keyframe = Keyframe.ofFloat(fraction, value);
747                     break;
748                 case VALUE_TYPE_COLOR:
749                 case VALUE_TYPE_INT:
750                     int intValue = a.getInt(AndroidResources.STYLEABLE_KEYFRAME_VALUE, 0);
751                     keyframe = Keyframe.ofInt(fraction, intValue);
752                     break;
753             }
754         } else {
755             keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) :
756                     Keyframe.ofInt(fraction);
757         }
758 
759         final int resID = a.getResourceId(AndroidResources.STYLEABLE_KEYFRAME_INTERPOLATOR, 0);
760         if (resID > 0) {
761             final Interpolator interpolator = loadInterpolator(res, theme, resID);
762             keyframe.setInterpolator(interpolator);
763         }
764         a.recycle();
765 
766         return keyframe;
767     }
768 
loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs, float pathErrorScale)769     private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs,
770             float pathErrorScale) throws NotFoundException {
771         ObjectAnimator anim = new ObjectAnimator();
772 
773         loadAnimator(res, theme, attrs, anim, pathErrorScale);
774 
775         return anim;
776     }
777 
778     /**
779      * Creates a new animation whose parameters come from the specified context
780      * and attributes set.
781      *
782      * @param res The resources
783      * @param theme The theme that is being used to inflate the animator
784      * @param attrs The set of attributes holding the animation parameters
785      * @param anim Null if this is a ValueAnimator, otherwise this is an
786      *            ObjectAnimator
787      * @param pathErrorScale Acceptable error in the unit of pixels
788      * @return a ValueAnimator that was inflated from the given resources
789      */
loadAnimator(Resources res, Theme theme, AttributeSet attrs, ValueAnimator anim, float pathErrorScale)790     private static ValueAnimator loadAnimator(Resources res, Theme theme, AttributeSet attrs,
791             ValueAnimator anim, float pathErrorScale) throws NotFoundException {
792         TypedArray arrayAnimator = null;
793         TypedArray arrayObjectAnimator = null;
794 
795         if (theme != null) {
796             arrayAnimator = theme.obtainStyledAttributes(attrs, AndroidResources.STYLEABLE_ANIMATOR,
797                     0, 0);
798         } else {
799             arrayAnimator = res.obtainAttributes(attrs, AndroidResources.STYLEABLE_ANIMATOR);
800         }
801 
802         // If anim is not null, then it is an object animator.
803         if (anim != null) {
804             if (theme != null) {
805                 arrayObjectAnimator = theme.obtainStyledAttributes(attrs,
806                         AndroidResources.STYLEABLE_PROPERTY_ANIMATOR, 0, 0);
807             } else {
808                 arrayObjectAnimator = res.obtainAttributes(attrs,
809                         AndroidResources.STYLEABLE_PROPERTY_ANIMATOR);
810             }
811         }
812 
813         if (anim == null) {
814             anim = new ValueAnimator();
815         }
816 
817         parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale);
818 
819         final int resID = arrayAnimator.getResourceId(
820                 AndroidResources.STYLEABLE_ANIMATOR_INTERPOLATOR, 0);
821         if (resID > 0) {
822             final Interpolator interpolator = loadInterpolator(res, theme, resID);
823             anim.setInterpolator(interpolator);
824         }
825 
826         arrayAnimator.recycle();
827         if (arrayObjectAnimator != null) {
828             arrayObjectAnimator.recycle();
829         }
830         return anim;
831     }
832 
isColorType(int type)833     private static boolean isColorType(int type) {
834         return type >= TypedValue.TYPE_FIRST_COLOR_INT && type <= TypedValue.TYPE_LAST_COLOR_INT;
835     }
836 
837     /**
838      * Loads an {@link Interpolator} object from a resource
839      *
840      * @param context Application context used to access resources
841      * @param id The resource id of the animation to load
842      * @return The animation object reference by the specified id
843      * @throws NotFoundException when interpolator resources cannot be loaded
844      */
loadInterpolator(@onNull Context context, @AnimatorRes @InterpolatorRes int id)845     public static @NonNull Interpolator loadInterpolator(@NonNull Context context,
846             @AnimatorRes @InterpolatorRes int id) throws NotFoundException {
847         XmlResourceParser parser = null;
848         try {
849             parser = context.getResources().getAnimation(id);
850             return createInterpolatorFromXml(context.getResources(), context.getTheme(), parser);
851         } catch (XmlPullParserException ex) {
852             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x"
853                     + Integer.toHexString(id));
854             rnf.initCause(ex);
855             throw rnf;
856         } catch (IOException ex) {
857             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x"
858                     + Integer.toHexString(id));
859             rnf.initCause(ex);
860             throw rnf;
861         } finally {
862             if (parser != null) {
863                 parser.close();
864             }
865         }
866 
867     }
868 
869     /**
870      * Loads an {@link Interpolator} object from a resource
871      *
872      * @param res The resources
873      * @param id The resource id of the animation to load
874      * @return The interpolator object reference by the specified id
875      * @throws NotFoundException when interpolator resources cannot be loaded
876      */
loadInterpolator(Resources res, Theme theme, int id)877     static Interpolator loadInterpolator(Resources res, Theme theme, int id)
878             throws NotFoundException {
879         // Special treatment for the interpolator introduced at API 21.
880         if (id == AndroidResources.FAST_OUT_LINEAR_IN) {
881             return new PathInterpolator(0.4f, 0f, 1f, 1f);
882         } else if (id == AndroidResources.FAST_OUT_SLOW_IN) {
883             return new PathInterpolator(0.4f, 0f, 0.2f, 1f);
884         } else if (id == AndroidResources.LINEAR_OUT_SLOW_IN) {
885             return new PathInterpolator(0f, 0f, 0.2f, 1f);
886         }
887 
888         XmlResourceParser parser = null;
889         try {
890             parser = res.getAnimation(id);
891             return createInterpolatorFromXml(res, theme, parser);
892         } catch (XmlPullParserException ex) {
893             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x"
894                     + Integer.toHexString(id));
895             rnf.initCause(ex);
896             throw rnf;
897         } catch (IOException ex) {
898             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x"
899                     + Integer.toHexString(id));
900             rnf.initCause(ex);
901             throw rnf;
902         } finally {
903             if (parser != null) {
904                 parser.close();
905             }
906         }
907 
908     }
909 
createInterpolatorFromXml(Resources res, Theme theme, XmlPullParser parser)910     private static Interpolator createInterpolatorFromXml(Resources res, Theme theme,
911             XmlPullParser parser) throws XmlPullParserException, IOException {
912 
913         Interpolator interpolator = null;
914 
915         // Make sure we are on a start tag.
916         int type;
917         int depth = parser.getDepth();
918 
919         while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
920                 && type != XmlPullParser.END_DOCUMENT) {
921 
922             if (type != XmlPullParser.START_TAG) {
923                 continue;
924             }
925 
926             AttributeSet attrs = Xml.asAttributeSet(parser);
927 
928             String name = parser.getName();
929 
930             if (name.equals("linearInterpolator")) {
931                 interpolator = new LinearInterpolator();
932             } else if (name.equals("accelerateInterpolator")) {
933                 interpolator = new AccelerateInterpolator(res, theme, attrs);
934             } else if (name.equals("decelerateInterpolator")) {
935                 interpolator = new DecelerateInterpolator(res, theme, attrs);
936             } else if (name.equals("accelerateDecelerateInterpolator")) {
937                 interpolator = new AccelerateDecelerateInterpolator();
938             } else if (name.equals("cycleInterpolator")) {
939                 interpolator = new CycleInterpolator(res, theme, attrs);
940             } else if (name.equals("anticipateInterpolator")) {
941                 interpolator = new AnticipateInterpolator(res, theme, attrs);
942             } else if (name.equals("overshootInterpolator")) {
943                 interpolator = new OvershootInterpolator(res, theme, attrs);
944             } else if (name.equals("anticipateOvershootInterpolator")) {
945                 interpolator = new AnticipateOvershootInterpolator(res, theme, attrs);
946             } else if (name.equals("bounceInterpolator")) {
947                 interpolator = new BounceInterpolator();
948             } else if (name.equals("pathInterpolator")) {
949                 interpolator = new PathInterpolator(res, theme, attrs, parser);
950             } else {
951                 throw new RuntimeException("Unknown interpolator name: " + name);
952             }
953         }
954         return interpolator;
955     }
956 }
957