Home | History | Annotate | Download | only in drawable
      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;
     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.Log;
     42 import android.util.TypedValue;
     43 import android.util.Xml;
     44 import android.view.InflateException;
     45 import android.view.animation.Interpolator;
     46 
     47 import androidx.annotation.AnimatorRes;
     48 import androidx.annotation.RestrictTo;
     49 import androidx.core.content.res.TypedArrayUtils;
     50 import androidx.core.graphics.PathParser;
     51 
     52 import org.xmlpull.v1.XmlPullParser;
     53 import org.xmlpull.v1.XmlPullParserException;
     54 
     55 import java.io.IOException;
     56 import 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)
     69 public 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