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