content: /*
   * Copyright (C) 2010 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package android.animation;
  
  import android.annotation.AnimatorRes;
  import android.annotation.AnyRes;
  import android.annotation.NonNull;
  import android.content.Context;
  import android.content.pm.ActivityInfo.Config;
  import android.content.res.ConfigurationBoundResourceCache;
  import android.content.res.ConstantState;
  import android.content.res.Resources;
  import android.content.res.Resources.NotFoundException;
  import android.content.res.Resources.Theme;
  import android.content.res.TypedArray;
  import android.content.res.XmlResourceParser;
  import android.graphics.Path;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.PathParser;
  import android.util.StateSet;
  import android.util.TypedValue;
  import android.util.Xml;
  import android.view.InflateException;
  import android.view.animation.AnimationUtils;
  import android.view.animation.BaseInterpolator;
  import android.view.animation.Interpolator;
  
  import com.android.internal.R;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.IOException;
  import java.util.ArrayList;
  
  /**
   * This class is used to instantiate animator XML files into Animator objects.
   * <p>
   * For performance reasons, inflation relies heavily on pre-processing of
   * XML files that is done at build time. Therefore, it is not currently possible
   * to use this inflater with an XmlPullParser over a plain XML file at runtime;
   * it only works with an XmlPullParser returned from a compiled resource (R.
   * <em>something</em> file.)
   */
  public class AnimatorInflater {
      private static final String TAG = "AnimatorInflater";
      /**
       * These flags are used when parsing AnimatorSet objects
       */
      private static final int TOGETHER = 0;
      private static final int SEQUENTIALLY = 1;
  
      /**
       * Enum values used in XML attributes to indicate the value for mValueType
       */
      private static final int VALUE_TYPE_FLOAT       = 0;
      private static final int VALUE_TYPE_INT         = 1;
      private static final int VALUE_TYPE_PATH        = 2;
      private static final int VALUE_TYPE_COLOR       = 3;
      private static final int VALUE_TYPE_UNDEFINED   = 4;
  
      private static final boolean DBG_ANIMATOR_INFLATER = false;
  
      // used to calculate changing configs for resource references
      private static final TypedValue sTmpTypedValue = new TypedValue();
  
      /**
       * Loads an {@link Animator} object from a resource
       *
       * @param context Application context used to access resources
       * @param id The resource id of the animation to load
       * @return The animator object reference by the specified id
       * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded
       */
      public static Animator loadAnimator(Context context, @AnimatorRes int id)
              throws NotFoundException {
          return loadAnimator(context.getResources(), context.getTheme(), id);
      }
  
      /**
       * Loads an {@link Animator} object from a resource
       *
       * @param resources The resources
       * @param theme The theme
       * @param id The resource id of the animation to load
       * @return The animator object reference by the specified id
       * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded
       * @hide
       */
      public static Animator loadAnimator(Resources resources, Theme theme, int id)
              throws NotFoundException {
          return loadAnimator(resources, theme, id, 1);
      }
  
      /** @hide */
      public static Animator loadAnimator(Resources resources, Theme theme, int id,
              float pathErrorScale) throws NotFoundException {
          final ConfigurationBoundResourceCache<Animator> animatorCache = resources
                  .getAnimatorCache();
          Animator animator = animatorCache.getInstance(id, resources, theme);
          if (animator != null) {
              if (DBG_ANIMATOR_INFLATER) {
                  Log.d(TAG, "loaded animator from cache, " + resources.getResourceName(id));
              }
              return animator;
          } else if (DBG_ANIMATOR_INFLATER) {
              Log.d(TAG, "cache miss for animator " + resources.getResourceName(id));
          }
          XmlResourceParser parser = null;
          try {
              parser = resources.getAnimation(id);
              animator = createAnimatorFromXml(resources, theme, parser, pathErrorScale);
              if (animator != null) {
                  animator.appendChangingConfigurations(getChangingConfigs(resources, id));
                  final ConstantState<Animator> constantState = animator.createConstantState();
                  if (constantState != null) {
                      if (DBG_ANIMATOR_INFLATER) {
                          Log.d(TAG, "caching animator for res " + resources.getResourceName(id));
                      }
                      animatorCache.put(id, theme, constantState);
                      // create a new animator so that cached version is never used by the user
                      animator = constantState.newInstance(resources, theme);
                  }
              }
              return animator;
          } catch (XmlPullParserException ex) {
              Resources.NotFoundException rnf =
                      new Resources.NotFoundException("Can't load animation resource ID #0x" +
                              Integer.toHexString(id));
              rnf.initCause(ex);
              throw rnf;
          } catch (IOException ex) {
              Resources.NotFoundException rnf =
                      new Resources.NotFoundException("Can't load animation resource ID #0x" +
                              Integer.toHexString(id));
              rnf.initCause(ex);
              throw rnf;
          } finally {
              if (parser != null) parser.close();
          }
      }
  
      public static StateListAnimator loadStateListAnimator(Context context, int id)
              throws NotFoundException {
          final Resources resources = context.getResources();
          final ConfigurationBoundResourceCache<StateListAnimator> cache = resources
                  .getStateListAnimatorCache();
          final Theme theme = context.getTheme();
          StateListAnimator animator = cache.getInstance(id, resources, theme);
          if (animator != null) {
              return animator;
          }
          XmlResourceParser parser = null;
          try {
              parser = resources.getAnimation(id);
              animator = createStateListAnimatorFromXml(context, parser, Xml.asAttributeSet(parser));
              if (animator != null) {
                  animator.appendChangingConfigurations(getChangingConfigs(resources, id));
                  final ConstantState<StateListAnimator> constantState = animator
                          .createConstantState();
                  if (constantState != null) {
                      cache.put(id, theme, constantState);
                      // return a clone so that the animator in constant state is never used.
                      animator = constantState.newInstance(resources, theme);
                  }
              }
              return animator;
          } catch (XmlPullParserException ex) {
              Resources.NotFoundException rnf =
                      new Resources.NotFoundException(
                              "Can't load state list animator resource ID #0x" +
                                      Integer.toHexString(id)
                      );
              rnf.initCause(ex);
              throw rnf;
          } catch (IOException ex) {
              Resources.NotFoundException rnf =
                      new Resources.NotFoundException(
                              "Can't load state list animator resource ID #0x" +
                                      Integer.toHexString(id)
                      );
              rnf.initCause(ex);
              throw rnf;
          } finally {
              if (parser != null) {
                  parser.close();
              }
          }
      }
  
      private static StateListAnimator createStateListAnimatorFromXml(Context context,
              XmlPullParser parser, AttributeSet attributeSet)
              throws IOException, XmlPullParserException {
          int type;
          StateListAnimator stateListAnimator = new StateListAnimator();
  
          while (true) {
              type = parser.next();
              switch (type) {
                  case XmlPullParser.END_DOCUMENT:
                  case XmlPullParser.END_TAG:
                      return stateListAnimator;
  
                  case XmlPullParser.START_TAG:
                      // parse item
                      Animator animator = null;
                      if ("item".equals(parser.getName())) {
                          int attributeCount = parser.getAttributeCount();
                          int[] states = new int[attributeCount];
                          int stateIndex = 0;
                          for (int i = 0; i < attributeCount; i++) {
                              int attrName = attributeSet.getAttributeNameResource(i);
                              if (attrName == R.attr.animation) {
                                  final int animId = attributeSet.getAttributeResourceValue(i, 0);
                                  animator = loadAnimator(context, animId);
                              } else {
                                  states[stateIndex++] =
                                          attributeSet.getAttributeBooleanValue(i, false) ?
                                                  attrName : -attrName;
                              }
                          }
                          if (animator == null) {
                              animator = createAnimatorFromXml(context.getResources(),
                                      context.getTheme(), parser, 1f);
                          }
  
                          if (animator == null) {
                              throw new Resources.NotFoundException(
                                      "animation state item must have a valid animation");
                          }
                          stateListAnimator
                                  .addState(StateSet.trimStateSet(states, stateIndex), animator);
                      }
                      break;
              }
          }
      }
  
      /**
       * PathDataEvaluator is used to interpolate between two paths which are
       * represented in the same format but different control points' values.
       * The path is represented as verbs and points for each of the verbs.
       */
      private static class PathDataEvaluator implements TypeEvaluator<PathParser.PathData> {
          private final PathParser.PathData mPathData = new PathParser.PathData();
  
          @Override
          public PathParser.PathData evaluate(float fraction, PathParser.PathData startPathData,
                      PathParser.PathData endPathData) {
              if (!PathParser.interpolatePathData(mPathData, startPathData, endPathData, fraction)) {
                  throw new IllegalArgumentException("Can't interpolate between"
                          + " two incompatible pathData");
              }
              return mPathData;
          }
      }
  
      private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType,
              int valueFromId, int valueToId, String propertyName) {
  
          TypedValue tvFrom = styledAttributes.peekValue(valueFromId);
          boolean hasFrom = (tvFrom != null);
          int fromType = hasFrom ? tvFrom.type : 0;
          TypedValue tvTo = styledAttributes.peekValue(valueToId);
          boolean hasTo = (tvTo != null);
          int toType = hasTo ? tvTo.type : 0;
  
          if (valueType == VALUE_TYPE_UNDEFINED) {
              // Check whether it's color type. If not, fall back to default type (i.e. float type)
              if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) {
                  valueType = VALUE_TYPE_COLOR;
              } else {
                  valueType = VALUE_TYPE_FLOAT;
              }
          }
  
          boolean getFloats = (valueType == VALUE_TYPE_FLOAT);
  
          PropertyValuesHolder returnValue = null;
  
          if (valueType == VALUE_TYPE_PATH) {
              String fromString = styledAttributes.getString(valueFromId);
              String toString = styledAttributes.getString(valueToId);
              PathParser.PathData nodesFrom = fromString == null
                      ? null : new PathParser.PathData(fromString);
              PathParser.PathData nodesTo = toString == null
                      ? null : new PathParser.PathData(toString);
  
              if (nodesFrom != null || nodesTo != null) {
                  if (nodesFrom != null) {
                      TypeEvaluator evaluator = new PathDataEvaluator();
                      if (nodesTo != null) {
                          if (!PathParser.canMorph(nodesFrom, nodesTo)) {
                              throw new InflateException(" Can't morph from " + fromString + " to " +
                                      toString);
                          }
                          returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
                                  nodesFrom, nodesTo);
                      } else {
                          returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
                                  (Object) nodesFrom);
                      }
                  } else if (nodesTo != null) {
                      TypeEvaluator evaluator = new PathDataEvaluator();
                      returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
                              (Object) nodesTo);
                  }
              }
          } else {
              TypeEvaluator evaluator = null;
              // Integer and float value types are handled here.
              if (valueType == VALUE_TYPE_COLOR) {
                  // special case for colors: ignore valueType and get ints
                  evaluator = ArgbEvaluator.getInstance();
              }
              if (getFloats) {
                  float valueFrom;
                  float valueTo;
                  if (hasFrom) {
                      if (fromType == TypedValue.TYPE_DIMENSION) {
                          valueFrom = styledAttributes.getDimension(valueFromId, 0f);
                      } else {
                          valueFrom = styledAttributes.getFloat(valueFromId, 0f);
                      }
                      if (hasTo) {
                          if (toType == TypedValue.TYPE_DIMENSION) {
                              valueTo = styledAttributes.getDimension(valueToId, 0f);
                          } else {
                              valueTo = styledAttributes.getFloat(valueToId, 0f);
                          }
                          returnValue = PropertyValuesHolder.ofFloat(propertyName,
                                  valueFrom, valueTo);
                      } else {
                          returnValue = PropertyValuesHolder.ofFloat(propertyName, valueFrom);
                      }
                  } else {
                      if (toType == TypedValue.TYPE_DIMENSION) {
                          valueTo = styledAttributes.getDimension(valueToId, 0f);
                      } else {
                          valueTo = styledAttributes.getFloat(valueToId, 0f);
                      }
                      returnValue = PropertyValuesHolder.ofFloat(propertyName, valueTo);
                  }
              } else {
                  int valueFrom;
                  int valueTo;
                  if (hasFrom) {
                      if (fromType == TypedValue.TYPE_DIMENSION) {
                          valueFrom = (int) styledAttributes.getDimension(valueFromId, 0f);
                      } else if (isColorType(fromType)) {
                          valueFrom = styledAttributes.getColor(valueFromId, 0);
                      } else {
                          valueFrom = styledAttributes.getInt(valueFromId, 0);
                      }
                      if (hasTo) {
                          if (toType == TypedValue.TYPE_DIMENSION) {
                              valueTo = (int) styledAttributes.getDimension(valueToId, 0f);
                          } else if (isColorType(toType)) {
                              valueTo = styledAttributes.getColor(valueToId, 0);
                          } else {
                              valueTo = styledAttributes.getInt(valueToId, 0);
                          }
                          returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom, valueTo);
                      } else {
                          returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom);
                      }
                  } else {
                      if (hasTo) {
                          if (toType == TypedValue.TYPE_DIMENSION) {
                              valueTo = (int) styledAttributes.getDimension(valueToId, 0f);
                          } else if (isColorType(toType)) {
                              valueTo = styledAttributes.getColor(valueToId, 0);
                          } else {
                              valueTo = styledAttributes.getInt(valueToId, 0);
                          }
                          returnValue = PropertyValuesHolder.ofInt(propertyName, valueTo);
                      }
                  }
              }
              if (returnValue != null && evaluator != null) {
                  returnValue.setEvaluator(evaluator);
              }
          }
  
          return returnValue;
      }
  
      /**
       * @param anim The animator, must not be null
       * @param arrayAnimator Incoming typed array for Animator's attributes.
       * @param arrayObjectAnimator Incoming typed array for Object Animator's
       *            attributes.
       * @param pixelSize The relative pixel size, used to calculate the
       *                  maximum error for path animations.
       */
      private static void parseAnimatorFromTypeArray(ValueAnimator anim,
              TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) {
          long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300);
  
          long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0);
  
          int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, VALUE_TYPE_UNDEFINED);
  
          if (valueType == VALUE_TYPE_UNDEFINED) {
              valueType = inferValueTypeFromValues(arrayAnimator, R.styleable.Animator_valueFrom,
                      R.styleable.Animator_valueTo);
          }
          PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType,
                  R.styleable.Animator_valueFrom, R.styleable.Animator_valueTo, "");
          if (pvh != null) {
              anim.setValues(pvh);
          }
  
          anim.setDuration(duration);
          anim.setStartDelay(startDelay);
  
          if (arrayAnimator.hasValue(R.styleable.Animator_repeatCount)) {
              anim.setRepeatCount(
                      arrayAnimator.getInt(R.styleable.Animator_repeatCount, 0));
          }
          if (arrayAnimator.hasValue(R.styleable.Animator_repeatMode)) {
              anim.setRepeatMode(
                      arrayAnimator.getInt(R.styleable.Animator_repeatMode,
                              ValueAnimator.RESTART));
          }
  
          if (arrayObjectAnimator != null) {
              setupObjectAnimator(anim, arrayObjectAnimator, valueType, pixelSize);
          }
      }
  
      /**
       * Setup the Animator to achieve path morphing.
       *
       * @param anim The target Animator which will be updated.
       * @param arrayAnimator TypedArray for the ValueAnimator.
       * @return the PathDataEvaluator.
       */
      private static TypeEvaluator setupAnimatorForPath(ValueAnimator anim,
               TypedArray arrayAnimator) {
          TypeEvaluator evaluator = null;
          String fromString = arrayAnimator.getString(R.styleable.Animator_valueFrom);
          String toString = arrayAnimator.getString(R.styleable.Animator_valueTo);
          PathParser.PathData pathDataFrom = fromString == null
                  ? null : new PathParser.PathData(fromString);
          PathParser.PathData pathDataTo = toString == null
                  ? null : new PathParser.PathData(toString);
  
          if (pathDataFrom != null) {
              if (pathDataTo != null) {
                  anim.setObjectValues(pathDataFrom, pathDataTo);
                  if (!PathParser.canMorph(pathDataFrom, pathDataTo)) {
                      throw new InflateException(arrayAnimator.getPositionDescription()
                              + " Can't morph from " + fromString + " to " + toString);
                  }
              } else {
                  anim.setObjectValues((Object)pathDataFrom);
              }
              evaluator = new PathDataEvaluator();
          } else if (pathDataTo != null) {
              anim.setObjectValues((Object)pathDataTo);
              evaluator = new PathDataEvaluator();
          }
  
          if (DBG_ANIMATOR_INFLATER && evaluator != null) {
              Log.v(TAG, "create a new PathDataEvaluator here");
          }
  
          return evaluator;
      }
  
      /**
       * Setup ObjectAnimator's property or values from pathData.
       *
       * @param anim The target Animator which will be updated.
       * @param arrayObjectAnimator TypedArray for the ObjectAnimator.
       * @param getFloats True if the value type is float.
       * @param pixelSize The relative pixel size, used to calculate the
       *                  maximum error for path animations.
       */
      private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator,
              int valueType, float pixelSize) {
          ObjectAnimator oa = (ObjectAnimator) anim;
          String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData);
  
          // Path can be involved in an ObjectAnimator in the following 3 ways:
          // 1) Path morphing: the property to be animated is pathData, and valueFrom and valueTo
          //    are both of pathType. valueType = pathType needs to be explicitly defined.
          // 2) A property in X or Y dimension can be animated along a path: the property needs to be
          //    defined in propertyXName or propertyYName attribute, the path will be defined in the
          //    pathData attribute. valueFrom and valueTo will not be necessary for this animation.
          // 3) PathInterpolator can also define a path (in pathData) for its interpolation curve.
          // Here we are dealing with case 2:
          if (pathData != null) {
              String propertyXName =
                      arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyXName);
              String propertyYName =
                      arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyYName);
  
              if (valueType == VALUE_TYPE_PATH || valueType == VALUE_TYPE_UNDEFINED) {
                  // When pathData is defined, we are in case #2 mentioned above. ValueType can only
                  // be float type, or int type. Otherwise we fallback to default type.
                  valueType = VALUE_TYPE_FLOAT;
              }
              if (propertyXName == null && propertyYName == null) {
                  throw new InflateException(arrayObjectAnimator.getPositionDescription()
                          + " propertyXName or propertyYName is needed for PathData");
              } else {
                  Path path = PathParser.createPathFromPathData(pathData);
                  float error = 0.5f * pixelSize; // max half a pixel error
                  PathKeyframes keyframeSet = KeyframeSet.ofPath(path, error);
                  Keyframes xKeyframes;
                  Keyframes yKeyframes;
                  if (valueType == VALUE_TYPE_FLOAT) {
                      xKeyframes = keyframeSet.createXFloatKeyframes();
                      yKeyframes = keyframeSet.createYFloatKeyframes();
                  } else {
                      xKeyframes = keyframeSet.createXIntKeyframes();
                      yKeyframes = keyframeSet.createYIntKeyframes();
                  }
                  PropertyValuesHolder x = null;
                  PropertyValuesHolder y = null;
                  if (propertyXName != null) {
                      x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes);
                  }
                  if (propertyYName != null) {
                      y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes);
                  }
                  if (x == null) {
                      oa.setValues(y);
                  } else if (y == null) {
                      oa.setValues(x);
                  } else {
                      oa.setValues(x, y);
                  }
              }
          } else {
              String propertyName =
                      arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyName);
              oa.setPropertyName(propertyName);
          }
      }
  
      /**
       * Setup ValueAnimator's values.
       * This will handle all of the integer, float and color types.
       *
       * @param anim The target Animator which will be updated.
       * @param arrayAnimator TypedArray for the ValueAnimator.
       * @param getFloats True if the value type is float.
       * @param hasFrom True if "valueFrom" exists.
       * @param fromType The type of "valueFrom".
       * @param hasTo True if "valueTo" exists.
       * @param toType The type of "valueTo".
       */
      private static void setupValues(ValueAnimator anim, TypedArray arrayAnimator,
              boolean getFloats, boolean hasFrom, int fromType, boolean hasTo, int toType) {
          int valueFromIndex = R.styleable.Animator_valueFrom;
          int valueToIndex = R.styleable.Animator_valueTo;
          if (getFloats) {
              float valueFrom;
              float valueTo;
              if (hasFrom) {
                  if (fromType == TypedValue.TYPE_DIMENSION) {
                      valueFrom = arrayAnimator.getDimension(valueFromIndex, 0f);
                  } else {
                      valueFrom = arrayAnimator.getFloat(valueFromIndex, 0f);
                  }
                  if (hasTo) {
                      if (toType == TypedValue.TYPE_DIMENSION) {
                          valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
                      } else {
                          valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
                      }
                      anim.setFloatValues(valueFrom, valueTo);
                  } else {
                      anim.setFloatValues(valueFrom);
                  }
              } else {
                  if (toType == TypedValue.TYPE_DIMENSION) {
                      valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
                  } else {
                      valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
                  }
                  anim.setFloatValues(valueTo);
              }
          } else {
              int valueFrom;
              int valueTo;
              if (hasFrom) {
                  if (fromType == TypedValue.TYPE_DIMENSION) {
                      valueFrom = (int) arrayAnimator.getDimension(valueFromIndex, 0f);
                  } else if (isColorType(fromType)) {
                      valueFrom = arrayAnimator.getColor(valueFromIndex, 0);
                  } else {
                      valueFrom = arrayAnimator.getInt(valueFromIndex, 0);
                  }
                  if (hasTo) {
                      if (toType == TypedValue.TYPE_DIMENSION) {
                          valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
                      } else if (isColorType(toType)) {
                          valueTo = arrayAnimator.getColor(valueToIndex, 0);
                      } else {
                          valueTo = arrayAnimator.getInt(valueToIndex, 0);
                      }
                      anim.setIntValues(valueFrom, valueTo);
                  } else {
                      anim.setIntValues(valueFrom);
                  }
              } else {
                  if (hasTo) {
                      if (toType == TypedValue.TYPE_DIMENSION) {
                          valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
                      } else if (isColorType(toType)) {
                          valueTo = arrayAnimator.getColor(valueToIndex, 0);
                      } else {
                          valueTo = arrayAnimator.getInt(valueToIndex, 0);
                      }
                      anim.setIntValues(valueTo);
                  }
              }
          }
      }
  
      private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
              float pixelSize)
              throws XmlPullParserException, IOException {
          return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0,
                  pixelSize);
      }
  
      private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
              AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)
              throws XmlPullParserException, IOException {
          Animator anim = null;
          ArrayList<Animator> childAnims = null;
  
          // Make sure we are on a start tag.
          int type;
          int depth = parser.getDepth();
  
          while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
                  && type != XmlPullParser.END_DOCUMENT) {
  
              if (type != XmlPullParser.START_TAG) {
                  continue;
              }
  
              String name = parser.getName();
              boolean gotValues = false;
  
              if (name.equals("objectAnimator")) {
                  anim = loadObjectAnimator(res, theme, attrs, pixelSize);
              } else if (name.equals("animator")) {
                  anim = loadAnimator(res, theme, attrs, null, pixelSize);
              } else if (name.equals("set")) {
                  anim = new AnimatorSet();
                  TypedArray a;
                  if (theme != null) {
                      a = theme.obtainStyledAttributes(attrs, R.styleable.AnimatorSet, 0, 0);
                  } else {
                      a = res.obtainAttributes(attrs, R.styleable.AnimatorSet);
                  }
                  anim.appendChangingConfigurations(a.getChangingConfigurations());
                  int ordering = a.getInt(R.styleable.AnimatorSet_ordering, TOGETHER);
                  createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering,
                          pixelSize);
                  a.recycle();
              } else if (name.equals("propertyValuesHolder")) {
                  PropertyValuesHolder[] values = loadValues(res, theme, parser,
                          Xml.asAttributeSet(parser));
                  if (values != null && anim != null && (anim instanceof ValueAnimator)) {
                      ((ValueAnimator) anim).setValues(values);
                  }
                  gotValues = true;
              } else {
                  throw new RuntimeException("Unknown animator name: " + parser.getName());
              }
  
              if (parent != null && !gotValues) {
                  if (childAnims == null) {
                      childAnims = new ArrayList<Animator>();
                  }
                  childAnims.add(anim);
              }
          }
          if (parent != null && childAnims != null) {
              Animator[] animsArray = new Animator[childAnims.size()];
              int index = 0;
              for (Animator a : childAnims) {
                  animsArray[index++] = a;
              }
              if (sequenceOrdering == TOGETHER) {
                  parent.playTogether(animsArray);
              } else {
                  parent.playSequentially(animsArray);
              }
          }
          return anim;
      }
  
      private static PropertyValuesHolder[] loadValues(Resources res, Theme theme,
              XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
          ArrayList<PropertyValuesHolder> values = null;
  
          int type;
          while ((type = parser.getEventType()) != XmlPullParser.END_TAG &&
                  type != XmlPullParser.END_DOCUMENT) {
  
              if (type != XmlPullParser.START_TAG) {
                  parser.next();
                  continue;
              }
  
              String name = parser.getName();
  
              if (name.equals("propertyValuesHolder")) {
                  TypedArray a;
                  if (theme != null) {
                      a = theme.obtainStyledAttributes(attrs, R.styleable.PropertyValuesHolder, 0, 0);
                  } else {
                      a = res.obtainAttributes(attrs, R.styleable.PropertyValuesHolder);
                  }
                  String propertyName = a.getString(R.styleable.PropertyValuesHolder_propertyName);
                  int valueType = a.getInt(R.styleable.PropertyValuesHolder_valueType,
                          VALUE_TYPE_UNDEFINED);
  
                  PropertyValuesHolder pvh = loadPvh(res, theme, parser, propertyName, valueType);
                  if (pvh == null) {
                      pvh = getPVH(a, valueType,
                              R.styleable.PropertyValuesHolder_valueFrom,
                              R.styleable.PropertyValuesHolder_valueTo, propertyName);
                  }
                  if (pvh != null) {
                      if (values == null) {
                          values = new ArrayList<PropertyValuesHolder>();
                      }
                      values.add(pvh);
                  }
                  a.recycle();
              }
  
              parser.next();
          }
  
          PropertyValuesHolder[] valuesArray = null;
          if (values != null) {
              int count = values.size();
              valuesArray = new PropertyValuesHolder[count];
              for (int i = 0; i < count; ++i) {
                  valuesArray[i] = values.get(i);
              }
          }
          return valuesArray;
      }
  
      // When no value type is provided in keyframe, we need to infer the type from the value. i.e.
      // if value is defined in the style of a color value, then the color type is returned.
      // Otherwise, default float type is returned.
      private static int inferValueTypeOfKeyframe(Resources res, Theme theme, AttributeSet attrs) {
          int valueType;
          TypedArray a;
          if (theme != null) {
              a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0);
          } else {
              a = res.obtainAttributes(attrs, R.styleable.Keyframe);
          }
  
          TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value);
          boolean hasValue = (keyframeValue != null);
          // When no value type is provided, check whether it's a color type first.
          // If not, fall back to default value type (i.e. float type).
          if (hasValue && isColorType(keyframeValue.type)) {
              valueType = VALUE_TYPE_COLOR;
          } else {
              valueType = VALUE_TYPE_FLOAT;
          }
          a.recycle();
          return valueType;
      }
  
      private static int inferValueTypeFromValues(TypedArray styledAttributes, int valueFromId,
              int valueToId) {
          TypedValue tvFrom = styledAttributes.peekValue(valueFromId);
          boolean hasFrom = (tvFrom != null);
          int fromType = hasFrom ? tvFrom.type : 0;
          TypedValue tvTo = styledAttributes.peekValue(valueToId);
          boolean hasTo = (tvTo != null);
          int toType = hasTo ? tvTo.type : 0;
  
          int valueType;
          // Check whether it's color type. If not, fall back to default type (i.e. float type)
          if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) {
              valueType = VALUE_TYPE_COLOR;
          } else {
              valueType = VALUE_TYPE_FLOAT;
          }
          return valueType;
      }
  
      private static void dumpKeyframes(Object[] keyframes, String header) {
          if (keyframes == null || keyframes.length == 0) {
              return;
          }
          Log.d(TAG, header);
          int count = keyframes.length;
          for (int i = 0; i < count; ++i) {
              Keyframe keyframe = (Keyframe) keyframes[i];
              Log.d(TAG, "Keyframe " + i + ": fraction " +
                      (keyframe.getFraction() < 0 ? "null" : keyframe.getFraction()) + ", " +
                      ", value : " + ((keyframe.hasValue()) ? keyframe.getValue() : "null"));
          }
      }
  
      // Load property values holder if there are keyframes defined in it. Otherwise return null.
      private static PropertyValuesHolder loadPvh(Resources res, Theme theme, XmlPullParser parser,
              String propertyName, int valueType)
              throws XmlPullParserException, IOException {
  
          PropertyValuesHolder value = null;
          ArrayList<Keyframe> keyframes = null;
  
          int type;
          while ((type = parser.next()) != XmlPullParser.END_TAG &&
                  type != XmlPullParser.END_DOCUMENT) {
              String name = parser.getName();
              if (name.equals("keyframe")) {
                  if (valueType == VALUE_TYPE_UNDEFINED) {
                      valueType = inferValueTypeOfKeyframe(res, theme, Xml.asAttributeSet(parser));
                  }
                  Keyframe keyframe = loadKeyframe(res, theme, Xml.asAttributeSet(parser), valueType);
                  if (keyframe != null) {
                      if (keyframes == null) {
                          keyframes = new ArrayList<Keyframe>();
                      }
                      keyframes.add(keyframe);
                  }
                  parser.next();
              }
          }
  
          int count;
          if (keyframes != null && (count = keyframes.size()) > 0) {
              // make sure we have keyframes at 0 and 1
              // If we have keyframes with set fractions, add keyframes at start/end
              // appropriately. If start/end have no set fractions:
              // if there's only one keyframe, set its fraction to 1 and add one at 0
              // if >1 keyframe, set the last fraction to 1, the first fraction to 0
              Keyframe firstKeyframe = keyframes.get(0);
              Keyframe lastKeyframe = keyframes.get(count - 1);
              float endFraction = lastKeyframe.getFraction();
              if (endFraction < 1) {
                  if (endFraction < 0) {
                      lastKeyframe.setFraction(1);
                  } else {
                      keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1));
                      ++count;
                  }
              }
              float startFraction = firstKeyframe.getFraction();
              if (startFraction != 0) {
                  if (startFraction < 0) {
                      firstKeyframe.setFraction(0);
                  } else {
                      keyframes.add(0, createNewKeyframe(firstKeyframe, 0));
                      ++count;
                  }
              }
              Keyframe[] keyframeArray = new Keyframe[count];
              keyframes.toArray(keyframeArray);
              for (int i = 0; i < count; ++i) {
                  Keyframe keyframe = keyframeArray[i];
                  if (keyframe.getFraction() < 0) {
                      if (i == 0) {
                          keyframe.setFraction(0);
                      } else if (i == count - 1) {
                          keyframe.setFraction(1);
                      } else {
                          // figure out the start/end parameters of the current gap
                          // in fractions and distribute the gap among those keyframes
                          int startIndex = i;
                          int endIndex = i;
                          for (int j = startIndex + 1; j < count - 1; ++j) {
                              if (keyframeArray[j].getFraction() >= 0) {
                                  break;
                              }
                              endIndex = j;
                          }
                          float gap = keyframeArray[endIndex + 1].getFraction() -
                                  keyframeArray[startIndex - 1].getFraction();
                          distributeKeyframes(keyframeArray, gap, startIndex, endIndex);
                      }
                  }
              }
              value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray);
              if (valueType == VALUE_TYPE_COLOR) {
                  value.setEvaluator(ArgbEvaluator.getInstance());
              }
          }
  
          return value;
      }
  
      private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) {
          return sampleKeyframe.getType() == float.class ?
                              Keyframe.ofFloat(fraction) :
                              (sampleKeyframe.getType() == int.class) ?
                                      Keyframe.ofInt(fraction) :
                                      Keyframe.ofObject(fraction);
      }
  
      /**
       * Utility function to set fractions on keyframes to cover a gap in which the
       * fractions are not currently set. Keyframe fractions will be distributed evenly
       * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap
       * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the
       * keyframe before startIndex.
       * Assumptions:
       * - First and last keyframe fractions (bounding this spread) are already set. So,
       * for example, if no fractions are set, we will already set first and last keyframe
       * fraction values to 0 and 1.
       * - startIndex must be >0 (which follows from first assumption).
       * - endIndex must be >= startIndex.
       *
       * @param keyframes the array of keyframes
       * @param gap The total gap we need to distribute
       * @param startIndex The index of the first keyframe whose fraction must be set
       * @param endIndex The index of the last keyframe whose fraction must be set
       */
      private static void distributeKeyframes(Keyframe[] keyframes, float gap,
              int startIndex, int endIndex) {
          int count = endIndex - startIndex + 2;
          float increment = gap / count;
          for (int i = startIndex; i <= endIndex; ++i) {
              keyframes[i].setFraction(keyframes[i-1].getFraction() + increment);
          }
      }
  
      private static Keyframe loadKeyframe(Resources res, Theme theme, AttributeSet attrs,
              int valueType)
              throws XmlPullParserException, IOException {
  
          TypedArray a;
          if (theme != null) {
              a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0);
          } else {
              a = res.obtainAttributes(attrs, R.styleable.Keyframe);
          }
  
          Keyframe keyframe = null;
  
          float fraction = a.getFloat(R.styleable.Keyframe_fraction, -1);
  
          TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value);
          boolean hasValue = (keyframeValue != null);
          if (valueType == VALUE_TYPE_UNDEFINED) {
              // When no value type is provided, check whether it's a color type first.
              // If not, fall back to default value type (i.e. float type).
              if (hasValue && isColorType(keyframeValue.type)) {
                  valueType = VALUE_TYPE_COLOR;
              } else {
                  valueType = VALUE_TYPE_FLOAT;
              }
          }
  
          if (hasValue) {
              switch (valueType) {
                  case VALUE_TYPE_FLOAT:
                      float value = a.getFloat(R.styleable.Keyframe_value, 0);
                      keyframe = Keyframe.ofFloat(fraction, value);
                      break;
                  case VALUE_TYPE_COLOR:
                  case VALUE_TYPE_INT:
                      int intValue = a.getInt(R.styleable.Keyframe_value, 0);
                      keyframe = Keyframe.ofInt(fraction, intValue);
                      break;
              }
          } else {
              keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) :
                      Keyframe.ofInt(fraction);
          }
  
          final int resID = a.getResourceId(R.styleable.Keyframe_interpolator, 0);
          if (resID > 0) {
              final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID);
              keyframe.setInterpolator(interpolator);
          }
          a.recycle();
  
          return keyframe;
      }
  
      private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs,
              float pathErrorScale) throws NotFoundException {
          ObjectAnimator anim = new ObjectAnimator();
  
          loadAnimator(res, theme, attrs, anim, pathErrorScale);
  
          return anim;
      }
  
      /**
       * Creates a new animation whose parameters come from the specified context
       * and attributes set.
       *
       * @param res The resources
       * @param attrs The set of attributes holding the animation parameters
       * @param anim Null if this is a ValueAnimator, otherwise this is an
       *            ObjectAnimator
       */
      private static ValueAnimator loadAnimator(Resources res, Theme theme,
              AttributeSet attrs, ValueAnimator anim, float pathErrorScale)
              throws NotFoundException {
          TypedArray arrayAnimator = null;
          TypedArray arrayObjectAnimator = null;
  
          if (theme != null) {
              arrayAnimator = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0);
          } else {
              arrayAnimator = res.obtainAttributes(attrs, R.styleable.Animator);
          }
  
          // If anim is not null, then it is an object animator.
          if (anim != null) {
              if (theme != null) {
                  arrayObjectAnimator = theme.obtainStyledAttributes(attrs,
                          R.styleable.PropertyAnimator, 0, 0);
              } else {
                  arrayObjectAnimator = res.obtainAttributes(attrs, R.styleable.PropertyAnimator);
              }
              anim.appendChangingConfigurations(arrayObjectAnimator.getChangingConfigurations());
          }
  
          if (anim == null) {
              anim = new ValueAnimator();
          }
          anim.appendChangingConfigurations(arrayAnimator.getChangingConfigurations());
  
          parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale);
  
          final int resID = arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0);
          if (resID > 0) {
              final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID);
              if (interpolator instanceof BaseInterpolator) {
                  anim.appendChangingConfigurations(
                          ((BaseInterpolator) interpolator).getChangingConfiguration());
              }
              anim.setInterpolator(interpolator);
          }
  
          arrayAnimator.recycle();
          if (arrayObjectAnimator != null) {
              arrayObjectAnimator.recycle();
          }
          return anim;
      }
  
      private static @Config int getChangingConfigs(@NonNull Resources resources, @AnyRes int id) {
          synchronized (sTmpTypedValue) {
              resources.getValue(id, sTmpTypedValue, true);
              return sTmpTypedValue.changingConfigurations;
          }
      }
  
      private static boolean isColorType(int type) {
         return (type >= TypedValue.TYPE_FIRST_COLOR_INT) && (type <= TypedValue.TYPE_LAST_COLOR_INT);
      }
  }
  