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