();
+ int numListeners = oldListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ anim.mPauseListeners.add(oldListeners.get(i));
+ }
+ }
+ return anim;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * This method tells the object to use appropriate information to extract
+ * starting values for the animation. For example, a AnimatorSet object will pass
+ * this call to its child objects to tell them to set up the values. A
+ * ObjectAnimator object will use the information it has about its target object
+ * and PropertyValuesHolder objects to get the start values for its properties.
+ * A ValueAnimator object will ignore the request since it does not have enough
+ * information (such as a target object) to gather these values.
+ */
+ public void setupStartValues() {
+ }
+
+ /**
+ * This method tells the object to use appropriate information to extract
+ * ending values for the animation. For example, a AnimatorSet object will pass
+ * this call to its child objects to tell them to set up the values. A
+ * ObjectAnimator object will use the information it has about its target object
+ * and PropertyValuesHolder objects to get the start values for its properties.
+ * A ValueAnimator object will ignore the request since it does not have enough
+ * information (such as a target object) to gather these values.
+ */
+ public void setupEndValues() {
+ }
+
+ /**
+ * Sets the target object whose property will be animated by this animation. Not all subclasses
+ * operate on target objects (for example, {@link android.animation.ValueAnimator}, but this method
+ * is on the superclass for the convenience of dealing generically with those subclasses
+ * that do handle targets.
+ *
+ * @param target The object being animated
+ */
+ public void setTarget(Object target) {
+ }
+
+ // Hide reverse() and canReverse() for now since reverse() only work for simple
+ // cases, like we don't support sequential, neither startDelay.
+ // TODO: make reverse() works for all the Animators.
+ /**
+ * @hide
+ */
+ public boolean canReverse() {
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ public void reverse() {
+ throw new IllegalStateException("Reverse is not supported");
+ }
+
+ /**
+ * An animation listener receives notifications from an animation.
+ * Notifications indicate animation related events, such as the end or the
+ * repetition of the animation.
+ */
+ public static interface AnimatorListener {
+ /**
+ * Notifies the start of the animation.
+ *
+ * @param animation The started animation.
+ */
+ void onAnimationStart(Animator animation);
+
+ /**
+ * Notifies the end of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.
+ *
+ * @param animation The animation which reached its end.
+ */
+ void onAnimationEnd(Animator animation);
+
+ /**
+ * Notifies the cancellation of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.
+ *
+ * @param animation The animation which was canceled.
+ */
+ void onAnimationCancel(Animator animation);
+
+ /**
+ * Notifies the repetition of the animation.
+ *
+ * @param animation The animation which was repeated.
+ */
+ void onAnimationRepeat(Animator animation);
+ }
+
+ /**
+ * A pause listener receives notifications from an animation when the
+ * animation is {@link #pause() paused} or {@link #resume() resumed}.
+ *
+ * @see #addPauseListener(AnimatorPauseListener)
+ */
+ public static interface AnimatorPauseListener {
+ /**
+ * Notifies that the animation was paused.
+ *
+ * @param animation The animaton being paused.
+ * @see #pause()
+ */
+ void onAnimationPause(Animator animation);
+
+ /**
+ * Notifies that the animation was resumed, after being
+ * previously paused.
+ *
+ * @param animation The animation being resumed.
+ * @see #resume()
+ */
+ void onAnimationResume(Animator animation);
+ }
+
+ /**
+ * Whether or not the Animator is allowed to run asynchronously off of
+ * the UI thread. This is a hint that informs the Animator that it is
+ * OK to run the animation off-thread, however the Animator may decide
+ * that it must run the animation on the UI thread anyway.
+ *
+ *
Regardless of whether or not the animation runs asynchronously, all
+ * listener callbacks will be called on the UI thread.
+ *
+ * To be able to use this hint the following must be true:
+ *
+ * - The animator is immutable while {@link #isStarted()} is true. Requests
+ * to change duration, delay, etc... may be ignored.
+ * - Lifecycle callback events may be asynchronous. Events such as
+ * {@link Animator.AnimatorListener#onAnimationEnd(Animator)} or
+ * {@link Animator.AnimatorListener#onAnimationRepeat(Animator)} may end up delayed
+ * as they must be posted back to the UI thread, and any actions performed
+ * by those callbacks (such as starting new animations) will not happen
+ * in the same frame.
+ * - State change requests ({@link #cancel()}, {@link #end()}, {@link #reverse()}, etc...)
+ * may be asynchronous. It is guaranteed that all state changes that are
+ * performed on the UI thread in the same frame will be applied as a single
+ * atomic update, however that frame may be the current frame,
+ * the next frame, or some future frame. This will also impact the observed
+ * state of the Animator. For example, {@link #isStarted()} may still return true
+ * after a call to {@link #end()}. Using the lifecycle callbacks is preferred over
+ * queries to {@link #isStarted()}, {@link #isRunning()}, and {@link #isPaused()}
+ * for this reason.
+ *
+ * @hide
+ */
+ public void setAllowRunningAsynchronously(boolean mayRunAsync) {
+ // It is up to subclasses to support this, if they can.
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/animation/AnimatorInflater.java b/app/src/main/java/com/nineoldandroids/animation/AnimatorInflater.java
new file mode 100644
index 000000000..82a98534a
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/AnimatorInflater.java
@@ -0,0 +1,344 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.content.res.Resources.NotFoundException;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.view.animation.AnimationUtils;
+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.
+ *
+ * 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.
+ * something file.)
+ */
+public class AnimatorInflater {
+ private static final int[] AnimatorSet = new int[] {
+ /* 0 */ android.R.attr.ordering,
+ };
+ private static final int AnimatorSet_ordering = 0;
+
+ private static final int[] PropertyAnimator = new int[] {
+ /* 0 */ android.R.attr.propertyName,
+ };
+ private static final int PropertyAnimator_propertyName = 0;
+
+ private static final int[] Animator = new int[] {
+ /* 0 */ android.R.attr.interpolator,
+ /* 1 */ android.R.attr.duration,
+ /* 2 */ android.R.attr.startOffset,
+ /* 3 */ android.R.attr.repeatCount,
+ /* 4 */ android.R.attr.repeatMode,
+ /* 5 */ android.R.attr.valueFrom,
+ /* 6 */ android.R.attr.valueTo,
+ /* 7 */ android.R.attr.valueType,
+ };
+ private static final int Animator_interpolator = 0;
+ private static final int Animator_duration = 1;
+ private static final int Animator_startOffset = 2;
+ private static final int Animator_repeatCount = 3;
+ private static final int Animator_repeatMode = 4;
+ private static final int Animator_valueFrom = 5;
+ private static final int Animator_valueTo = 6;
+ private static final int Animator_valueType = 7;
+
+ /**
+ * 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_COLOR = 4;
+ //private static final int VALUE_TYPE_CUSTOM = 5;
+
+ /**
+ * Loads an {@link com.nineoldandroids.animation.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, int id)
+ throws NotFoundException {
+
+ XmlResourceParser parser = null;
+ try {
+ parser = context.getResources().getAnimation(id);
+ return createAnimatorFromXml(context, parser);
+ } catch (XmlPullParserException ex) {
+ NotFoundException rnf =
+ new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ NotFoundException rnf =
+ new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ private static Animator createAnimatorFromXml(Context c, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+
+ return createAnimatorFromXml(c, parser, Xml.asAttributeSet(parser), null, 0);
+ }
+
+ private static Animator createAnimatorFromXml(Context c, XmlPullParser parser,
+ AttributeSet attrs, AnimatorSet parent, int sequenceOrdering)
+ throws XmlPullParserException, IOException {
+
+ Animator anim = null;
+ ArrayList 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();
+
+ if (name.equals("objectAnimator")) {
+ anim = loadObjectAnimator(c, attrs);
+ } else if (name.equals("animator")) {
+ anim = loadAnimator(c, attrs, null);
+ } else if (name.equals("set")) {
+ anim = new AnimatorSet();
+ TypedArray a = c.obtainStyledAttributes(attrs,
+ /*com.android.internal.R.styleable.*/AnimatorSet);
+
+ TypedValue orderingValue = new TypedValue();
+ a.getValue(/*com.android.internal.R.styleable.*/AnimatorSet_ordering, orderingValue);
+ int ordering = orderingValue.type == TypedValue.TYPE_INT_DEC ? orderingValue.data : TOGETHER;
+
+ createAnimatorFromXml(c, parser, attrs, (AnimatorSet) anim, ordering);
+ a.recycle();
+ } else {
+ throw new RuntimeException("Unknown animator name: " + parser.getName());
+ }
+
+ if (parent != null) {
+ if (childAnims == null) {
+ childAnims = new ArrayList();
+ }
+ 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 ObjectAnimator loadObjectAnimator(Context context, AttributeSet attrs)
+ throws NotFoundException {
+
+ ObjectAnimator anim = new ObjectAnimator();
+
+ loadAnimator(context, attrs, anim);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, /*com.android.internal.R.styleable.*/PropertyAnimator);
+
+ String propertyName = a.getString(/*com.android.internal.R.styleable.*/PropertyAnimator_propertyName);
+
+ anim.setPropertyName(propertyName);
+
+ a.recycle();
+
+ return anim;
+ }
+
+ /**
+ * Creates a new animation whose parameters come from the specified context and
+ * attributes set.
+ *
+ * @param context the application environment
+ * @param attrs the set of attributes holding the animation parameters
+ */
+ private static ValueAnimator loadAnimator(Context context, AttributeSet attrs, ValueAnimator anim)
+ throws NotFoundException {
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, /*com.android.internal.R.styleable.*/Animator);
+
+ long duration = a.getInt(/*com.android.internal.R.styleable.*/Animator_duration, 0);
+
+ long startDelay = a.getInt(/*com.android.internal.R.styleable.*/Animator_startOffset, 0);
+
+ int valueType = a.getInt(/*com.android.internal.R.styleable.*/Animator_valueType,
+ VALUE_TYPE_FLOAT);
+
+ if (anim == null) {
+ anim = new ValueAnimator();
+ }
+ //TypeEvaluator evaluator = null;
+
+ int valueFromIndex = /*com.android.internal.R.styleable.*/Animator_valueFrom;
+ int valueToIndex = /*com.android.internal.R.styleable.*/Animator_valueTo;
+
+ boolean getFloats = (valueType == VALUE_TYPE_FLOAT);
+
+ TypedValue tvFrom = a.peekValue(valueFromIndex);
+ boolean hasFrom = (tvFrom != null);
+ int fromType = hasFrom ? tvFrom.type : 0;
+ TypedValue tvTo = a.peekValue(valueToIndex);
+ boolean hasTo = (tvTo != null);
+ int toType = hasTo ? tvTo.type : 0;
+
+ if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+ (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) ||
+ (hasTo && (toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+ (toType <= TypedValue.TYPE_LAST_COLOR_INT))) {
+ // special case for colors: ignore valueType and get ints
+ getFloats = false;
+ anim.setEvaluator(new ArgbEvaluator());
+ }
+
+ if (getFloats) {
+ float valueFrom;
+ float valueTo;
+ if (hasFrom) {
+ if (fromType == TypedValue.TYPE_DIMENSION) {
+ valueFrom = a.getDimension(valueFromIndex, 0f);
+ } else {
+ valueFrom = a.getFloat(valueFromIndex, 0f);
+ }
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = a.getDimension(valueToIndex, 0f);
+ } else {
+ valueTo = a.getFloat(valueToIndex, 0f);
+ }
+ anim.setFloatValues(valueFrom, valueTo);
+ } else {
+ anim.setFloatValues(valueFrom);
+ }
+ } else {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = a.getDimension(valueToIndex, 0f);
+ } else {
+ valueTo = a.getFloat(valueToIndex, 0f);
+ }
+ anim.setFloatValues(valueTo);
+ }
+ } else {
+ int valueFrom;
+ int valueTo;
+ if (hasFrom) {
+ if (fromType == TypedValue.TYPE_DIMENSION) {
+ valueFrom = (int) a.getDimension(valueFromIndex, 0f);
+ } else if ((fromType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+ (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) {
+ valueFrom = a.getColor(valueFromIndex, 0);
+ } else {
+ valueFrom = a.getInt(valueFromIndex, 0);
+ }
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = (int) a.getDimension(valueToIndex, 0f);
+ } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+ (toType <= TypedValue.TYPE_LAST_COLOR_INT)) {
+ valueTo = a.getColor(valueToIndex, 0);
+ } else {
+ valueTo = a.getInt(valueToIndex, 0);
+ }
+ anim.setIntValues(valueFrom, valueTo);
+ } else {
+ anim.setIntValues(valueFrom);
+ }
+ } else {
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = (int) a.getDimension(valueToIndex, 0f);
+ } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+ (toType <= TypedValue.TYPE_LAST_COLOR_INT)) {
+ valueTo = a.getColor(valueToIndex, 0);
+ } else {
+ valueTo = a.getInt(valueToIndex, 0);
+ }
+ anim.setIntValues(valueTo);
+ }
+ }
+ }
+
+ anim.setDuration(duration);
+ anim.setStartDelay(startDelay);
+
+ if (a.hasValue(/*com.android.internal.R.styleable.*/Animator_repeatCount)) {
+ anim.setRepeatCount(
+ a.getInt(/*com.android.internal.R.styleable.*/Animator_repeatCount, 0));
+ }
+ if (a.hasValue(/*com.android.internal.R.styleable.*/Animator_repeatMode)) {
+ anim.setRepeatMode(
+ a.getInt(/*com.android.internal.R.styleable.*/Animator_repeatMode,
+ ValueAnimator.RESTART));
+ }
+ //if (evaluator != null) {
+ // anim.setEvaluator(evaluator);
+ //}
+
+ final int resID =
+ a.getResourceId(/*com.android.internal.R.styleable.*/Animator_interpolator, 0);
+ if (resID > 0) {
+ anim.setInterpolator(AnimationUtils.loadInterpolator(context, resID));
+ }
+ a.recycle();
+
+ return anim;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nineoldandroids/animation/AnimatorListenerAdapter.java b/app/src/main/java/com/nineoldandroids/animation/AnimatorListenerAdapter.java
new file mode 100644
index 000000000..fa5cef3b9
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/AnimatorListenerAdapter.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+/**
+ * This adapter class provides empty implementations of the methods from {@link android.animation.Animator.AnimatorListener}.
+ * Any custom listener that cares only about a subset of the methods of this listener can
+ * simply subclass this adapter class instead of implementing the interface directly.
+ */
+public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+}
diff --git a/app/src/main/java/com/nineoldandroids/animation/AnimatorSet.java b/app/src/main/java/com/nineoldandroids/animation/AnimatorSet.java
new file mode 100644
index 000000000..b12f2d535
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/AnimatorSet.java
@@ -0,0 +1,1114 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+import android.animation.TimeInterpolator;
+import android.view.animation.Interpolator;
+
+/**
+ * This class plays a set of {@link com.nineoldandroids.animation.Animator} objects in the specified order. Animations
+ * can be set up to play together, in sequence, or after a specified delay.
+ *
+ * There are two different approaches to adding animations to a AnimatorSet:
+ * either the {@link com.nineoldandroids.animation.AnimatorSet#playTogether(com.nineoldandroids.animation.Animator[]) playTogether()} or
+ * {@link com.nineoldandroids.animation.AnimatorSet#playSequentially(com.nineoldandroids.animation.Animator[]) playSequentially()} methods can be called to add
+ * a set of animations all at once, or the {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} can be
+ * used in conjunction with methods in the {@link com.nineoldandroids.animation.AnimatorSet.Builder Builder}
+ * class to add animations
+ * one by one.
+ *
+ * It is possible to set up a AnimatorSet with circular dependencies between
+ * its animations. For example, an animation a1 could be set up to start before animation a2, a2
+ * before a3, and a3 before a1. The results of this configuration are undefined, but will typically
+ * result in none of the affected animations being played. Because of this (and because
+ * circular dependencies do not make logical sense anyway), circular dependencies
+ * should be avoided, and the dependency flow of animations should only be in one direction.
+ */
+public final class AnimatorSet extends Animator {
+
+ /**
+ * Internal variables
+ * NOTE: This object implements the clone() method, making a deep copy of any referenced
+ * objects. As other non-trivial fields are added to this class, make sure to add logic
+ * to clone() to make deep copies of them.
+ */
+
+ /**
+ * Tracks animations currently being played, so that we know what to
+ * cancel or end when cancel() or end() is called on this AnimatorSet
+ */
+ private ArrayList mPlayingSet = new ArrayList();
+
+ /**
+ * Contains all nodes, mapped to their respective Animators. When new
+ * dependency information is added for an Animator, we want to add it
+ * to a single node representing that Animator, not create a new Node
+ * if one already exists.
+ */
+ private HashMap mNodeMap = new HashMap();
+
+ /**
+ * Set of all nodes created for this AnimatorSet. This list is used upon
+ * starting the set, and the nodes are placed in sorted order into the
+ * sortedNodes collection.
+ */
+ private ArrayList mNodes = new ArrayList();
+
+ /**
+ * The sorted list of nodes. This is the order in which the animations will
+ * be played. The details about when exactly they will be played depend
+ * on the dependency relationships of the nodes.
+ */
+ private ArrayList mSortedNodes = new ArrayList();
+
+ /**
+ * Flag indicating whether the nodes should be sorted prior to playing. This
+ * flag allows us to cache the previous sorted nodes so that if the sequence
+ * is replayed with no changes, it does not have to re-sort the nodes again.
+ */
+ private boolean mNeedsSort = true;
+
+ private AnimatorSetListener mSetListener = null;
+
+ /**
+ * Flag indicating that the AnimatorSet has been manually
+ * terminated (by calling cancel() or end()).
+ * This flag is used to avoid starting other animations when currently-playing
+ * child animations of this AnimatorSet end. It also determines whether cancel/end
+ * notifications are sent out via the normal AnimatorSetListener mechanism.
+ */
+ boolean mTerminated = false;
+
+ /**
+ * Indicates whether an AnimatorSet has been start()'d, whether or
+ * not there is a nonzero startDelay.
+ */
+ private boolean mStarted = false;
+
+ // The amount of time in ms to delay starting the animation after start() is called
+ private long mStartDelay = 0;
+
+ // Animator used for a nonzero startDelay
+ private ValueAnimator mDelayAnim = null;
+
+
+ // How long the child animations should last in ms. The default value is negative, which
+ // simply means that there is no duration set on the AnimatorSet. When a real duration is
+ // set, it is passed along to the child animations.
+ private long mDuration = -1;
+
+
+ /**
+ * Sets up this AnimatorSet to play all of the supplied animations at the same time.
+ *
+ * @param items The animations that will be started simultaneously.
+ */
+ public void playTogether(Animator... items) {
+ if (items != null) {
+ mNeedsSort = true;
+ Builder builder = play(items[0]);
+ for (int i = 1; i < items.length; ++i) {
+ builder.with(items[i]);
+ }
+ }
+ }
+
+ /**
+ * Sets up this AnimatorSet to play all of the supplied animations at the same time.
+ *
+ * @param items The animations that will be started simultaneously.
+ */
+ public void playTogether(Collection items) {
+ if (items != null && items.size() > 0) {
+ mNeedsSort = true;
+ Builder builder = null;
+ for (Animator anim : items) {
+ if (builder == null) {
+ builder = play(anim);
+ } else {
+ builder.with(anim);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets up this AnimatorSet to play each of the supplied animations when the
+ * previous animation ends.
+ *
+ * @param items The animations that will be started one after another.
+ */
+ public void playSequentially(Animator... items) {
+ if (items != null) {
+ mNeedsSort = true;
+ if (items.length == 1) {
+ play(items[0]);
+ } else {
+ for (int i = 0; i < items.length - 1; ++i) {
+ play(items[i]).before(items[i+1]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets up this AnimatorSet to play each of the supplied animations when the
+ * previous animation ends.
+ *
+ * @param items The animations that will be started one after another.
+ */
+ public void playSequentially(List items) {
+ if (items != null && items.size() > 0) {
+ mNeedsSort = true;
+ if (items.size() == 1) {
+ play(items.get(0));
+ } else {
+ for (int i = 0; i < items.size() - 1; ++i) {
+ play(items.get(i)).before(items.get(i+1));
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the current list of child Animator objects controlled by this
+ * AnimatorSet. This is a copy of the internal list; modifications to the returned list
+ * will not affect the AnimatorSet, although changes to the underlying Animator objects
+ * will affect those objects being managed by the AnimatorSet.
+ *
+ * @return ArrayList The list of child animations of this AnimatorSet.
+ */
+ public ArrayList getChildAnimations() {
+ ArrayList childList = new ArrayList();
+ for (Node node : mNodes) {
+ childList.add(node.animation);
+ }
+ return childList;
+ }
+
+ /**
+ * Sets the target object for all current {@link #getChildAnimations() child animations}
+ * of this AnimatorSet that take targets ({@link ObjectAnimator} and
+ * AnimatorSet).
+ *
+ * @param target The object being animated
+ */
+ @Override
+ public void setTarget(Object target) {
+ for (Node node : mNodes) {
+ Animator animation = node.animation;
+ if (animation instanceof AnimatorSet) {
+ ((AnimatorSet)animation).setTarget(target);
+ } else if (animation instanceof ObjectAnimator) {
+ ((ObjectAnimator)animation).setTarget(target);
+ }
+ }
+ }
+
+ /**
+ * Sets the TimeInterpolator for all current {@link #getChildAnimations() child animations}
+ * of this AnimatorSet.
+ *
+ * @param interpolator the interpolator to be used by each child animation of this AnimatorSet
+ */
+ @Override
+ public void setInterpolator(Interpolator interpolator) {
+ for (Node node : mNodes) {
+ node.animation.setInterpolator(interpolator);
+ }
+ }
+
+ /**
+ * This method creates a Builder object, which is used to
+ * set up playing constraints. This initial play() method
+ * tells the Builder the animation that is the dependency for
+ * the succeeding commands to the Builder. For example,
+ * calling play(a1).with(a2) sets up the AnimatorSet to play
+ * a1 and a2 at the same time,
+ * play(a1).before(a2) sets up the AnimatorSet to play
+ * a1 first, followed by a2, and
+ * play(a1).after(a2) sets up the AnimatorSet to play
+ * a2 first, followed by a1.
+ *
+ * Note that play() is the only way to tell the
+ * Builder the animation upon which the dependency is created,
+ * so successive calls to the various functions in Builder
+ * will all refer to the initial parameter supplied in play()
+ * as the dependency of the other animations. For example, calling
+ * play(a1).before(a2).before(a3) will play both a2
+ * and a3 when a1 ends; it does not set up a dependency between
+ * a2 and a3.
+ *
+ * @param anim The animation that is the dependency used in later calls to the
+ * methods in the returned Builder object. A null parameter will result
+ * in a null Builder return value.
+ * @return Builder The object that constructs the AnimatorSet based on the dependencies
+ * outlined in the calls to play and the other methods in the
+ * BuilderNote that canceling a AnimatorSet also cancels all of the animations that it
+ * is responsible for.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public void cancel() {
+ mTerminated = true;
+ if (isStarted()) {
+ ArrayList tmpListeners = null;
+ if (mListeners != null) {
+ tmpListeners = (ArrayList) mListeners.clone();
+ for (AnimatorListener listener : tmpListeners) {
+ listener.onAnimationCancel(this);
+ }
+ }
+ if (mDelayAnim != null && mDelayAnim.isRunning()) {
+ // If we're currently in the startDelay period, just cancel that animator and
+ // send out the end event to all listeners
+ mDelayAnim.cancel();
+ } else if (mSortedNodes.size() > 0) {
+ for (Node node : mSortedNodes) {
+ node.animation.cancel();
+ }
+ }
+ if (tmpListeners != null) {
+ for (AnimatorListener listener : tmpListeners) {
+ listener.onAnimationEnd(this);
+ }
+ }
+ mStarted = false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Note that ending a AnimatorSet also ends all of the animations that it is
+ * responsible for.
+ */
+ @Override
+ public void end() {
+ mTerminated = true;
+ if (isStarted()) {
+ if (mSortedNodes.size() != mNodes.size()) {
+ // hasn't been started yet - sort the nodes now, then end them
+ sortNodes();
+ for (Node node : mSortedNodes) {
+ if (mSetListener == null) {
+ mSetListener = new AnimatorSetListener(this);
+ }
+ node.animation.addListener(mSetListener);
+ }
+ }
+ if (mDelayAnim != null) {
+ mDelayAnim.cancel();
+ }
+ if (mSortedNodes.size() > 0) {
+ for (Node node : mSortedNodes) {
+ node.animation.end();
+ }
+ }
+ if (mListeners != null) {
+ ArrayList tmpListeners =
+ (ArrayList) mListeners.clone();
+ for (AnimatorListener listener : tmpListeners) {
+ listener.onAnimationEnd(this);
+ }
+ }
+ mStarted = false;
+ }
+ }
+
+ /**
+ * Returns true if any of the child animations of this AnimatorSet have been started and have
+ * not yet ended.
+ * @return Whether this AnimatorSet has been started and has not yet ended.
+ */
+ @Override
+ public boolean isRunning() {
+ for (Node node : mNodes) {
+ if (node.animation.isRunning()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isStarted() {
+ return mStarted;
+ }
+
+ /**
+ * The amount of time, in milliseconds, to delay starting the animation after
+ * {@link #start()} is called.
+ *
+ * @return the number of milliseconds to delay running the animation
+ */
+ @Override
+ public long getStartDelay() {
+ return mStartDelay;
+ }
+
+ /**
+ * The amount of time, in milliseconds, to delay starting the animation after
+ * {@link #start()} is called.
+
+ * @param startDelay The amount of the delay, in milliseconds
+ */
+ @Override
+ public void setStartDelay(long startDelay) {
+ mStartDelay = startDelay;
+ }
+
+ /**
+ * Gets the length of each of the child animations of this AnimatorSet. This value may
+ * be less than 0, which indicates that no duration has been set on this AnimatorSet
+ * and each of the child animations will use their own duration.
+ *
+ * @return The length of the animation, in milliseconds, of each of the child
+ * animations of this AnimatorSet.
+ */
+ @Override
+ public long getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Sets the length of each of the current child animations of this AnimatorSet. By default,
+ * each child animation will use its own duration. If the duration is set on the AnimatorSet,
+ * then each child animation inherits this duration.
+ *
+ * @param duration The length of the animation, in milliseconds, of each of the child
+ * animations of this AnimatorSet.
+ */
+ @Override
+ public AnimatorSet setDuration(long duration) {
+ if (duration < 0) {
+ throw new IllegalArgumentException("duration must be a value of zero or greater");
+ }
+ for (Node node : mNodes) {
+ // TODO: don't set the duration of the timing-only nodes created by AnimatorSet to
+ // insert "play-after" delays
+ node.animation.setDuration(duration);
+ }
+ mDuration = duration;
+ return this;
+ }
+
+ @Override
+ public void setupStartValues() {
+ for (Node node : mNodes) {
+ node.animation.setupStartValues();
+ }
+ }
+
+ @Override
+ public void setupEndValues() {
+ for (Node node : mNodes) {
+ node.animation.setupEndValues();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Starting this AnimatorSet will, in turn, start the animations for which
+ * it is responsible. The details of when exactly those animations are started depends on
+ * the dependency relationships that have been set up between the animations.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public void start() {
+ mTerminated = false;
+ mStarted = true;
+
+ // First, sort the nodes (if necessary). This will ensure that sortedNodes
+ // contains the animation nodes in the correct order.
+ sortNodes();
+
+ int numSortedNodes = mSortedNodes.size();
+ for (int i = 0; i < numSortedNodes; ++i) {
+ Node node = mSortedNodes.get(i);
+ // First, clear out the old listeners
+ ArrayList oldListeners = node.animation.getListeners();
+ if (oldListeners != null && oldListeners.size() > 0) {
+ final ArrayList clonedListeners = new
+ ArrayList(oldListeners);
+
+ for (AnimatorListener listener : clonedListeners) {
+ if (listener instanceof DependencyListener ||
+ listener instanceof AnimatorSetListener) {
+ node.animation.removeListener(listener);
+ }
+ }
+ }
+ }
+
+ // nodesToStart holds the list of nodes to be started immediately. We don't want to
+ // start the animations in the loop directly because we first need to set up
+ // dependencies on all of the nodes. For example, we don't want to start an animation
+ // when some other animation also wants to start when the first animation begins.
+ final ArrayList nodesToStart = new ArrayList();
+ for (int i = 0; i < numSortedNodes; ++i) {
+ Node node = mSortedNodes.get(i);
+ if (mSetListener == null) {
+ mSetListener = new AnimatorSetListener(this);
+ }
+ if (node.dependencies == null || node.dependencies.size() == 0) {
+ nodesToStart.add(node);
+ } else {
+ int numDependencies = node.dependencies.size();
+ for (int j = 0; j < numDependencies; ++j) {
+ Dependency dependency = node.dependencies.get(j);
+ dependency.node.animation.addListener(
+ new DependencyListener(this, node, dependency.rule));
+ }
+ node.tmpDependencies = (ArrayList) node.dependencies.clone();
+ }
+ node.animation.addListener(mSetListener);
+ }
+ // Now that all dependencies are set up, start the animations that should be started.
+ if (mStartDelay <= 0) {
+ for (Node node : nodesToStart) {
+ node.animation.start();
+ mPlayingSet.add(node.animation);
+ }
+ } else {
+ mDelayAnim = ValueAnimator.ofFloat(0f, 1f);
+ mDelayAnim.setDuration(mStartDelay);
+ mDelayAnim.addListener(new AnimatorListenerAdapter() {
+ boolean canceled = false;
+ public void onAnimationCancel(Animator anim) {
+ canceled = true;
+ }
+ public void onAnimationEnd(Animator anim) {
+ if (!canceled) {
+ int numNodes = nodesToStart.size();
+ for (int i = 0; i < numNodes; ++i) {
+ Node node = nodesToStart.get(i);
+ node.animation.start();
+ mPlayingSet.add(node.animation);
+ }
+ }
+ }
+ });
+ mDelayAnim.start();
+ }
+ if (mListeners != null) {
+ ArrayList tmpListeners =
+ (ArrayList) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationStart(this);
+ }
+ }
+ if (mNodes.size() == 0 && mStartDelay == 0) {
+ // Handle unusual case where empty AnimatorSet is started - should send out
+ // end event immediately since the event will not be sent out at all otherwise
+ mStarted = false;
+ if (mListeners != null) {
+ ArrayList tmpListeners =
+ (ArrayList) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationEnd(this);
+ }
+ }
+ }
+ }
+
+ @Override
+ public AnimatorSet clone() {
+ final AnimatorSet anim = (AnimatorSet) super.clone();
+ /*
+ * The basic clone() operation copies all items. This doesn't work very well for
+ * AnimatorSet, because it will copy references that need to be recreated and state
+ * that may not apply. What we need to do now is put the clone in an uninitialized
+ * state, with fresh, empty data structures. Then we will build up the nodes list
+ * manually, as we clone each Node (and its animation). The clone will then be sorted,
+ * and will populate any appropriate lists, when it is started.
+ */
+ anim.mNeedsSort = true;
+ anim.mTerminated = false;
+ anim.mStarted = false;
+ anim.mPlayingSet = new ArrayList();
+ anim.mNodeMap = new HashMap();
+ anim.mNodes = new ArrayList();
+ anim.mSortedNodes = new ArrayList();
+
+ // Walk through the old nodes list, cloning each node and adding it to the new nodemap.
+ // One problem is that the old node dependencies point to nodes in the old AnimatorSet.
+ // We need to track the old/new nodes in order to reconstruct the dependencies in the clone.
+ HashMap nodeCloneMap = new HashMap(); //
+ for (Node node : mNodes) {
+ Node nodeClone = node.clone();
+ nodeCloneMap.put(node, nodeClone);
+ anim.mNodes.add(nodeClone);
+ anim.mNodeMap.put(nodeClone.animation, nodeClone);
+ // Clear out the dependencies in the clone; we'll set these up manually later
+ nodeClone.dependencies = null;
+ nodeClone.tmpDependencies = null;
+ nodeClone.nodeDependents = null;
+ nodeClone.nodeDependencies = null;
+ // clear out any listeners that were set up by the AnimatorSet; these will
+ // be set up when the clone's nodes are sorted
+ ArrayList cloneListeners = nodeClone.animation.getListeners();
+ if (cloneListeners != null) {
+ ArrayList listenersToRemove = null;
+ for (AnimatorListener listener : cloneListeners) {
+ if (listener instanceof AnimatorSetListener) {
+ if (listenersToRemove == null) {
+ listenersToRemove = new ArrayList();
+ }
+ listenersToRemove.add(listener);
+ }
+ }
+ if (listenersToRemove != null) {
+ for (AnimatorListener listener : listenersToRemove) {
+ cloneListeners.remove(listener);
+ }
+ }
+ }
+ }
+ // Now that we've cloned all of the nodes, we're ready to walk through their
+ // dependencies, mapping the old dependencies to the new nodes
+ for (Node node : mNodes) {
+ Node nodeClone = nodeCloneMap.get(node);
+ if (node.dependencies != null) {
+ for (Dependency dependency : node.dependencies) {
+ Node clonedDependencyNode = nodeCloneMap.get(dependency.node);
+ Dependency cloneDependency = new Dependency(clonedDependencyNode,
+ dependency.rule);
+ nodeClone.addDependency(cloneDependency);
+ }
+ }
+ }
+
+ return anim;
+ }
+
+ /**
+ * This class is the mechanism by which animations are started based on events in other
+ * animations. If an animation has multiple dependencies on other animations, then
+ * all dependencies must be satisfied before the animation is started.
+ */
+ private static class DependencyListener implements AnimatorListener {
+
+ private AnimatorSet mAnimatorSet;
+
+ // The node upon which the dependency is based.
+ private Node mNode;
+
+ // The Dependency rule (WITH or AFTER) that the listener should wait for on
+ // the node
+ private int mRule;
+
+ public DependencyListener(AnimatorSet animatorSet, Node node, int rule) {
+ this.mAnimatorSet = animatorSet;
+ this.mNode = node;
+ this.mRule = rule;
+ }
+
+ /**
+ * Ignore cancel events for now. We may want to handle this eventually,
+ * to prevent follow-on animations from running when some dependency
+ * animation is canceled.
+ */
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ /**
+ * An end event is received - see if this is an event we are listening for
+ */
+ public void onAnimationEnd(Animator animation) {
+ if (mRule == Dependency.AFTER) {
+ startIfReady(animation);
+ }
+ }
+
+ /**
+ * Ignore repeat events for now
+ */
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ /**
+ * A start event is received - see if this is an event we are listening for
+ */
+ public void onAnimationStart(Animator animation) {
+ if (mRule == Dependency.WITH) {
+ startIfReady(animation);
+ }
+ }
+
+ /**
+ * Check whether the event received is one that the node was waiting for.
+ * If so, mark it as complete and see whether it's time to start
+ * the animation.
+ * @param dependencyAnimation the animation that sent the event.
+ */
+ private void startIfReady(Animator dependencyAnimation) {
+ if (mAnimatorSet.mTerminated) {
+ // if the parent AnimatorSet was canceled, then don't start any dependent anims
+ return;
+ }
+ Dependency dependencyToRemove = null;
+ int numDependencies = mNode.tmpDependencies.size();
+ for (int i = 0; i < numDependencies; ++i) {
+ Dependency dependency = mNode.tmpDependencies.get(i);
+ if (dependency.rule == mRule &&
+ dependency.node.animation == dependencyAnimation) {
+ // rule fired - remove the dependency and listener and check to
+ // see whether it's time to start the animation
+ dependencyToRemove = dependency;
+ dependencyAnimation.removeListener(this);
+ break;
+ }
+ }
+ mNode.tmpDependencies.remove(dependencyToRemove);
+ if (mNode.tmpDependencies.size() == 0) {
+ // all dependencies satisfied: start the animation
+ mNode.animation.start();
+ mAnimatorSet.mPlayingSet.add(mNode.animation);
+ }
+ }
+
+ }
+
+ private class AnimatorSetListener implements AnimatorListener {
+
+ private AnimatorSet mAnimatorSet;
+
+ AnimatorSetListener(AnimatorSet animatorSet) {
+ mAnimatorSet = animatorSet;
+ }
+
+ public void onAnimationCancel(Animator animation) {
+ if (!mTerminated) {
+ // Listeners are already notified of the AnimatorSet canceling in cancel().
+ // The logic below only kicks in when animations end normally
+ if (mPlayingSet.size() == 0) {
+ if (mListeners != null) {
+ int numListeners = mListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ mListeners.get(i).onAnimationCancel(mAnimatorSet);
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public void onAnimationEnd(Animator animation) {
+ animation.removeListener(this);
+ mPlayingSet.remove(animation);
+ Node animNode = mAnimatorSet.mNodeMap.get(animation);
+ animNode.done = true;
+ if (!mTerminated) {
+ // Listeners are already notified of the AnimatorSet ending in cancel() or
+ // end(); the logic below only kicks in when animations end normally
+ ArrayList sortedNodes = mAnimatorSet.mSortedNodes;
+ boolean allDone = true;
+ int numSortedNodes = sortedNodes.size();
+ for (int i = 0; i < numSortedNodes; ++i) {
+ if (!sortedNodes.get(i).done) {
+ allDone = false;
+ break;
+ }
+ }
+ if (allDone) {
+ // If this was the last child animation to end, then notify listeners that this
+ // AnimatorSet has ended
+ if (mListeners != null) {
+ ArrayList tmpListeners =
+ (ArrayList) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationEnd(mAnimatorSet);
+ }
+ }
+ mAnimatorSet.mStarted = false;
+ }
+ }
+ }
+
+ // Nothing to do
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ // Nothing to do
+ public void onAnimationStart(Animator animation) {
+ }
+
+ }
+
+ /**
+ * This method sorts the current set of nodes, if needed. The sort is a simple
+ * DependencyGraph sort, which goes like this:
+ * - All nodes without dependencies become 'roots'
+ * - while roots list is not null
+ * - for each root r
+ * - add r to sorted list
+ * - remove r as a dependency from any other node
+ * - any nodes with no dependencies are added to the roots list
+ */
+ private void sortNodes() {
+ if (mNeedsSort) {
+ mSortedNodes.clear();
+ ArrayList roots = new ArrayList();
+ int numNodes = mNodes.size();
+ for (int i = 0; i < numNodes; ++i) {
+ Node node = mNodes.get(i);
+ if (node.dependencies == null || node.dependencies.size() == 0) {
+ roots.add(node);
+ }
+ }
+ ArrayList tmpRoots = new ArrayList();
+ while (roots.size() > 0) {
+ int numRoots = roots.size();
+ for (int i = 0; i < numRoots; ++i) {
+ Node root = roots.get(i);
+ mSortedNodes.add(root);
+ if (root.nodeDependents != null) {
+ int numDependents = root.nodeDependents.size();
+ for (int j = 0; j < numDependents; ++j) {
+ Node node = root.nodeDependents.get(j);
+ node.nodeDependencies.remove(root);
+ if (node.nodeDependencies.size() == 0) {
+ tmpRoots.add(node);
+ }
+ }
+ }
+ }
+ roots.clear();
+ roots.addAll(tmpRoots);
+ tmpRoots.clear();
+ }
+ mNeedsSort = false;
+ if (mSortedNodes.size() != mNodes.size()) {
+ throw new IllegalStateException("Circular dependencies cannot exist"
+ + " in AnimatorSet");
+ }
+ } else {
+ // Doesn't need sorting, but still need to add in the nodeDependencies list
+ // because these get removed as the event listeners fire and the dependencies
+ // are satisfied
+ int numNodes = mNodes.size();
+ for (int i = 0; i < numNodes; ++i) {
+ Node node = mNodes.get(i);
+ if (node.dependencies != null && node.dependencies.size() > 0) {
+ int numDependencies = node.dependencies.size();
+ for (int j = 0; j < numDependencies; ++j) {
+ Dependency dependency = node.dependencies.get(j);
+ if (node.nodeDependencies == null) {
+ node.nodeDependencies = new ArrayList();
+ }
+ if (!node.nodeDependencies.contains(dependency.node)) {
+ node.nodeDependencies.add(dependency.node);
+ }
+ }
+ }
+ // nodes are 'done' by default; they become un-done when started, and done
+ // again when ended
+ node.done = false;
+ }
+ }
+ }
+
+ /**
+ * Dependency holds information about the node that some other node is
+ * dependent upon and the nature of that dependency.
+ *
+ */
+ private static class Dependency {
+ static final int WITH = 0; // dependent node must start with this dependency node
+ static final int AFTER = 1; // dependent node must start when this dependency node finishes
+
+ // The node that the other node with this Dependency is dependent upon
+ public Node node;
+
+ // The nature of the dependency (WITH or AFTER)
+ public int rule;
+
+ public Dependency(Node node, int rule) {
+ this.node = node;
+ this.rule = rule;
+ }
+ }
+
+ /**
+ * A Node is an embodiment of both the Animator that it wraps as well as
+ * any dependencies that are associated with that Animation. This includes
+ * both dependencies upon other nodes (in the dependencies list) as
+ * well as dependencies of other nodes upon this (in the nodeDependents list).
+ */
+ private static class Node implements Cloneable {
+ public Animator animation;
+
+ /**
+ * These are the dependencies that this node's animation has on other
+ * nodes. For example, if this node's animation should begin with some
+ * other animation ends, then there will be an item in this node's
+ * dependencies list for that other animation's node.
+ */
+ public ArrayList dependencies = null;
+
+ /**
+ * tmpDependencies is a runtime detail. We use the dependencies list for sorting.
+ * But we also use the list to keep track of when multiple dependencies are satisfied,
+ * but removing each dependency as it is satisfied. We do not want to remove
+ * the dependency itself from the list, because we need to retain that information
+ * if the AnimatorSet is launched in the future. So we create a copy of the dependency
+ * list when the AnimatorSet starts and use this tmpDependencies list to track the
+ * list of satisfied dependencies.
+ */
+ public ArrayList tmpDependencies = null;
+
+ /**
+ * nodeDependencies is just a list of the nodes that this Node is dependent upon.
+ * This information is used in sortNodes(), to determine when a node is a root.
+ */
+ public ArrayList nodeDependencies = null;
+
+ /**
+ * nodeDepdendents is the list of nodes that have this node as a dependency. This
+ * is a utility field used in sortNodes to facilitate removing this node as a
+ * dependency when it is a root node.
+ */
+ public ArrayList nodeDependents = null;
+
+ /**
+ * Flag indicating whether the animation in this node is finished. This flag
+ * is used by AnimatorSet to check, as each animation ends, whether all child animations
+ * are done and it's time to send out an end event for the entire AnimatorSet.
+ */
+ public boolean done = false;
+
+ /**
+ * Constructs the Node with the animation that it encapsulates. A Node has no
+ * dependencies by default; dependencies are added via the addDependency()
+ * method.
+ *
+ * @param animation The animation that the Node encapsulates.
+ */
+ public Node(Animator animation) {
+ this.animation = animation;
+ }
+
+ /**
+ * Add a dependency to this Node. The dependency includes information about the
+ * node that this node is dependency upon and the nature of the dependency.
+ * @param dependency
+ */
+ public void addDependency(Dependency dependency) {
+ if (dependencies == null) {
+ dependencies = new ArrayList();
+ nodeDependencies = new ArrayList();
+ }
+ dependencies.add(dependency);
+ if (!nodeDependencies.contains(dependency.node)) {
+ nodeDependencies.add(dependency.node);
+ }
+ Node dependencyNode = dependency.node;
+ if (dependencyNode.nodeDependents == null) {
+ dependencyNode.nodeDependents = new ArrayList();
+ }
+ dependencyNode.nodeDependents.add(this);
+ }
+
+ @Override
+ public Node clone() {
+ try {
+ Node node = (Node) super.clone();
+ node.animation = (Animator) animation.clone();
+ return node;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+ }
+
+ /**
+ * The Builder object is a utility class to facilitate adding animations to a
+ * AnimatorSet along with the relationships between the various animations. The
+ * intention of the Builder methods, along with the {@link
+ * com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator) play()} method of AnimatorSet is to make it possible
+ * to express the dependency relationships of animations in a natural way. Developers can also
+ * use the {@link com.nineoldandroids.animation.AnimatorSet#playTogether(com.nineoldandroids.animation.Animator[]) playTogether()} and {@link
+ * com.nineoldandroids.animation.AnimatorSet#playSequentially(com.nineoldandroids.animation.Animator[]) playSequentially()} methods if these suit the need,
+ * but it might be easier in some situations to express the AnimatorSet of animations in pairs.
+ *
+ * The Builder object cannot be constructed directly, but is rather constructed
+ * internally via a call to {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)}.
+ *
+ * For example, this sets up a AnimatorSet to play anim1 and anim2 at the same time, anim3 to
+ * play when anim2 finishes, and anim4 to play when anim3 finishes:
+ *
+ * AnimatorSet s = new AnimatorSet();
+ * s.play(anim1).with(anim2);
+ * s.play(anim2).before(anim3);
+ * s.play(anim4).after(anim3);
+ *
+ *
+ * Note in the example that both {@link com.nineoldandroids.animation.AnimatorSet.Builder#before(com.nineoldandroids.animation.Animator)} and {@link
+ * com.nineoldandroids.animation.AnimatorSet.Builder#after(com.nineoldandroids.animation.Animator)} are used. These are just different ways of expressing the same
+ * relationship and are provided to make it easier to say things in a way that is more natural,
+ * depending on the situation.
+ *
+ * It is possible to make several calls into the same Builder object to express
+ * multiple relationships. However, note that it is only the animation passed into the initial
+ * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} method that is the dependency in any of the successive
+ * calls to the Builder object. For example, the following code starts both anim2
+ * and anim3 when anim1 ends; there is no direct dependency relationship between anim2 and
+ * anim3:
+ *
+ * AnimatorSet s = new AnimatorSet();
+ * s.play(anim1).before(anim2).before(anim3);
+ *
+ * If the desired result is to play anim1 then anim2 then anim3, this code expresses the
+ * relationship correctly:
+ *
+ * AnimatorSet s = new AnimatorSet();
+ * s.play(anim1).before(anim2);
+ * s.play(anim2).before(anim3);
+ *
+ *
+ * Note that it is possible to express relationships that cannot be resolved and will not
+ * result in sensible results. For example, play(anim1).after(anim1) makes no
+ * sense. In general, circular dependencies like this one (or more indirect ones where a depends
+ * on b, which depends on c, which depends on a) should be avoided. Only create AnimatorSets
+ * that can boil down to a simple, one-way relationship of animations starting with, before, and
+ * after other, different, animations.
+ */
+ public class Builder {
+
+ /**
+ * This tracks the current node being processed. It is supplied to the play() method
+ * of AnimatorSet and passed into the constructor of Builder.
+ */
+ private Node mCurrentNode;
+
+ /**
+ * package-private constructor. Builders are only constructed by AnimatorSet, when the
+ * play() method is called.
+ *
+ * @param anim The animation that is the dependency for the other animations passed into
+ * the other methods of this Builder object.
+ */
+ Builder(Animator anim) {
+ mCurrentNode = mNodeMap.get(anim);
+ if (mCurrentNode == null) {
+ mCurrentNode = new Node(anim);
+ mNodeMap.put(anim, mCurrentNode);
+ mNodes.add(mCurrentNode);
+ }
+ }
+
+ /**
+ * Sets up the given animation to play at the same time as the animation supplied in the
+ * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} call that created this Builder object.
+ *
+ * @param anim The animation that will play when the animation supplied to the
+ * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} method starts.
+ */
+ public Builder with(Animator anim) {
+ Node node = mNodeMap.get(anim);
+ if (node == null) {
+ node = new Node(anim);
+ mNodeMap.put(anim, node);
+ mNodes.add(node);
+ }
+ Dependency dependency = new Dependency(mCurrentNode, Dependency.WITH);
+ node.addDependency(dependency);
+ return this;
+ }
+
+ /**
+ * Sets up the given animation to play when the animation supplied in the
+ * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} call that created this Builder object
+ * ends.
+ *
+ * @param anim The animation that will play when the animation supplied to the
+ * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} method ends.
+ */
+ public Builder before(Animator anim) {
+ Node node = mNodeMap.get(anim);
+ if (node == null) {
+ node = new Node(anim);
+ mNodeMap.put(anim, node);
+ mNodes.add(node);
+ }
+ Dependency dependency = new Dependency(mCurrentNode, Dependency.AFTER);
+ node.addDependency(dependency);
+ return this;
+ }
+
+ /**
+ * Sets up the given animation to play when the animation supplied in the
+ * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} call that created this Builder object
+ * to start when the animation supplied in this method call ends.
+ *
+ * @param anim The animation whose end will cause the animation supplied to the
+ * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} method to play.
+ */
+ public Builder after(Animator anim) {
+ Node node = mNodeMap.get(anim);
+ if (node == null) {
+ node = new Node(anim);
+ mNodeMap.put(anim, node);
+ mNodes.add(node);
+ }
+ Dependency dependency = new Dependency(node, Dependency.AFTER);
+ mCurrentNode.addDependency(dependency);
+ return this;
+ }
+
+ /**
+ * Sets up the animation supplied in the
+ * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} call that created this Builder object
+ * to play when the given amount of time elapses.
+ *
+ * @param delay The number of milliseconds that should elapse before the
+ * animation starts.
+ */
+ public Builder after(long delay) {
+ // setup dummy ValueAnimator just to run the clock
+ ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
+ anim.setDuration(delay);
+ after(anim);
+ return this;
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/com/nineoldandroids/animation/ArgbEvaluator.java b/app/src/main/java/com/nineoldandroids/animation/ArgbEvaluator.java
new file mode 100644
index 000000000..6b550c3b0
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/ArgbEvaluator.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between integer
+ * values that represent ARGB colors.
+ */
+public class ArgbEvaluator implements TypeEvaluator {
+ private static final ArgbEvaluator sInstance = new ArgbEvaluator();
+
+ /**
+ * Returns an instance of ArgbEvaluator that may be used in
+ * {@link ValueAnimator#setEvaluator(TypeEvaluator)}. The same instance may
+ * be used in multiple Animators because it holds no state.
+ * @return An instance of ArgbEvalutor.
+ *
+ * @hide
+ */
+ public static ArgbEvaluator getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * This function returns the calculated in-between value for a color
+ * given integers that represent the start and end values in the four
+ * bytes of the 32-bit int. Each channel is separately linearly interpolated
+ * and the resulting calculated values are recombined into the return value.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue A 32-bit int value representing colors in the
+ * separate bytes of the parameter
+ * @param endValue A 32-bit int value representing colors in the
+ * separate bytes of the parameter
+ * @return A value that is calculated to be the linearly interpolated
+ * result, derived by separating the start and end values into separate
+ * color channels and interpolating each one separately, recombining the
+ * resulting values in the same way.
+ */
+ public Object evaluate(float fraction, Object startValue, Object endValue) {
+ int startInt = (Integer) startValue;
+ int startA = (startInt >> 24) & 0xff;
+ int startR = (startInt >> 16) & 0xff;
+ int startG = (startInt >> 8) & 0xff;
+ int startB = startInt & 0xff;
+
+ int endInt = (Integer) endValue;
+ int endA = (endInt >> 24) & 0xff;
+ int endR = (endInt >> 16) & 0xff;
+ int endG = (endInt >> 8) & 0xff;
+ int endB = endInt & 0xff;
+
+ return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
+ (int)((startR + (int)(fraction * (endR - startR))) << 16) |
+ (int)((startG + (int)(fraction * (endG - startG))) << 8) |
+ (int)((startB + (int)(fraction * (endB - startB))));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nineoldandroids/animation/FloatEvaluator.java b/app/src/main/java/com/nineoldandroids/animation/FloatEvaluator.java
new file mode 100644
index 000000000..c53cdfd39
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/FloatEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between float values.
+ */
+public class FloatEvaluator implements TypeEvaluator {
+
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * fraction representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0),
+ * where x0 is startValue, x1 is endValue,
+ * and t is fraction.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value; should be of type float or
+ * Float
+ * @param endValue The end value; should be of type float or Float
+ * @return A linear interpolation between the start and end values, given the
+ * fraction parameter.
+ */
+ public Float evaluate(float fraction, Number startValue, Number endValue) {
+ float startFloat = startValue.floatValue();
+ return startFloat + fraction * (endValue.floatValue() - startFloat);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nineoldandroids/animation/FloatKeyframeSet.java b/app/src/main/java/com/nineoldandroids/animation/FloatKeyframeSet.java
new file mode 100644
index 000000000..1c32f9cfd
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/FloatKeyframeSet.java
@@ -0,0 +1,136 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+import android.view.animation.Interpolator;
+
+import com.nineoldandroids.animation.Keyframe.FloatKeyframe;
+
+import java.util.ArrayList;
+
+/**
+ * This class holds a collection of FloatKeyframe objects and is called by ValueAnimator to calculate
+ * values between those keyframes for a given animation. The class internal to the animation
+ * package because it is an implementation detail of how Keyframes are stored and used.
+ *
+ * This type-specific subclass of KeyframeSet, along with the other type-specific subclass for
+ * int, exists to speed up the getValue() method when there is no custom
+ * TypeEvaluator set for the animation, so that values can be calculated without autoboxing to the
+ * Object equivalents of these primitive types.
+ */
+class FloatKeyframeSet extends KeyframeSet {
+ private float firstValue;
+ private float lastValue;
+ private float deltaValue;
+ private boolean firstTime = true;
+
+ public FloatKeyframeSet(FloatKeyframe... keyframes) {
+ super(keyframes);
+ }
+
+ @Override
+ public Object getValue(float fraction) {
+ return getFloatValue(fraction);
+ }
+
+ @Override
+ public FloatKeyframeSet clone() {
+ ArrayList keyframes = mKeyframes;
+ int numKeyframes = mKeyframes.size();
+ FloatKeyframe[] newKeyframes = new FloatKeyframe[numKeyframes];
+ for (int i = 0; i < numKeyframes; ++i) {
+ newKeyframes[i] = (FloatKeyframe) keyframes.get(i).clone();
+ }
+ FloatKeyframeSet newSet = new FloatKeyframeSet(newKeyframes);
+ return newSet;
+ }
+
+ public float getFloatValue(float fraction) {
+ if (mNumKeyframes == 2) {
+ if (firstTime) {
+ firstTime = false;
+ firstValue = ((FloatKeyframe) mKeyframes.get(0)).getFloatValue();
+ lastValue = ((FloatKeyframe) mKeyframes.get(1)).getFloatValue();
+ deltaValue = lastValue - firstValue;
+ }
+ if (mInterpolator != null) {
+ fraction = mInterpolator.getInterpolation(fraction);
+ }
+ if (mEvaluator == null) {
+ return firstValue + fraction * deltaValue;
+ } else {
+ return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).floatValue();
+ }
+ }
+ if (fraction <= 0f) {
+ final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0);
+ final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(1);
+ float prevValue = prevKeyframe.getFloatValue();
+ float nextValue = nextKeyframe.getFloatValue();
+ float prevFraction = prevKeyframe.getFraction();
+ float nextFraction = nextKeyframe.getFraction();
+ final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
+ return mEvaluator == null ?
+ prevValue + intervalFraction * (nextValue - prevValue) :
+ ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
+ floatValue();
+ } else if (fraction >= 1f) {
+ final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 2);
+ final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 1);
+ float prevValue = prevKeyframe.getFloatValue();
+ float nextValue = nextKeyframe.getFloatValue();
+ float prevFraction = prevKeyframe.getFraction();
+ float nextFraction = nextKeyframe.getFraction();
+ final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
+ return mEvaluator == null ?
+ prevValue + intervalFraction * (nextValue - prevValue) :
+ ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
+ floatValue();
+ }
+ FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0);
+ for (int i = 1; i < mNumKeyframes; ++i) {
+ FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(i);
+ if (fraction < nextKeyframe.getFraction()) {
+ final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ float intervalFraction = (fraction - prevKeyframe.getFraction()) /
+ (nextKeyframe.getFraction() - prevKeyframe.getFraction());
+ float prevValue = prevKeyframe.getFloatValue();
+ float nextValue = nextKeyframe.getFloatValue();
+ return mEvaluator == null ?
+ prevValue + intervalFraction * (nextValue - prevValue) :
+ ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
+ floatValue();
+ }
+ prevKeyframe = nextKeyframe;
+ }
+ // shouldn't get here
+ return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).floatValue();
+ }
+
+}
+
diff --git a/app/src/main/java/com/nineoldandroids/animation/IntEvaluator.java b/app/src/main/java/com/nineoldandroids/animation/IntEvaluator.java
new file mode 100644
index 000000000..d60542b9e
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/IntEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between int values.
+ */
+public class IntEvaluator implements TypeEvaluator {
+
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * fraction representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0),
+ * where x0 is startValue, x1 is endValue,
+ * and t is fraction.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value; should be of type int or
+ * Integer
+ * @param endValue The end value; should be of type int or Integer
+ * @return A linear interpolation between the start and end values, given the
+ * fraction parameter.
+ */
+ public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
+ int startInt = startValue;
+ return (int)(startInt + fraction * (endValue - startInt));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nineoldandroids/animation/IntKeyframeSet.java b/app/src/main/java/com/nineoldandroids/animation/IntKeyframeSet.java
new file mode 100644
index 000000000..9c26c1e8c
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/IntKeyframeSet.java
@@ -0,0 +1,135 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+import android.view.animation.Interpolator;
+
+import com.nineoldandroids.animation.Keyframe.IntKeyframe;
+
+import java.util.ArrayList;
+
+/**
+ * This class holds a collection of IntKeyframe objects and is called by ValueAnimator to calculate
+ * values between those keyframes for a given animation. The class internal to the animation
+ * package because it is an implementation detail of how Keyframes are stored and used.
+ *
+ * This type-specific subclass of KeyframeSet, along with the other type-specific subclass for
+ * float, exists to speed up the getValue() method when there is no custom
+ * TypeEvaluator set for the animation, so that values can be calculated without autoboxing to the
+ * Object equivalents of these primitive types.
+ */
+class IntKeyframeSet extends KeyframeSet {
+ private int firstValue;
+ private int lastValue;
+ private int deltaValue;
+ private boolean firstTime = true;
+
+ public IntKeyframeSet(IntKeyframe... keyframes) {
+ super(keyframes);
+ }
+
+ @Override
+ public Object getValue(float fraction) {
+ return getIntValue(fraction);
+ }
+
+ @Override
+ public IntKeyframeSet clone() {
+ ArrayList keyframes = mKeyframes;
+ int numKeyframes = mKeyframes.size();
+ IntKeyframe[] newKeyframes = new IntKeyframe[numKeyframes];
+ for (int i = 0; i < numKeyframes; ++i) {
+ newKeyframes[i] = (IntKeyframe) keyframes.get(i).clone();
+ }
+ IntKeyframeSet newSet = new IntKeyframeSet(newKeyframes);
+ return newSet;
+ }
+
+ public int getIntValue(float fraction) {
+ if (mNumKeyframes == 2) {
+ if (firstTime) {
+ firstTime = false;
+ firstValue = ((IntKeyframe) mKeyframes.get(0)).getIntValue();
+ lastValue = ((IntKeyframe) mKeyframes.get(1)).getIntValue();
+ deltaValue = lastValue - firstValue;
+ }
+ if (mInterpolator != null) {
+ fraction = mInterpolator.getInterpolation(fraction);
+ }
+ if (mEvaluator == null) {
+ return firstValue + (int)(fraction * deltaValue);
+ } else {
+ return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).intValue();
+ }
+ }
+ if (fraction <= 0f) {
+ final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0);
+ final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(1);
+ int prevValue = prevKeyframe.getIntValue();
+ int nextValue = nextKeyframe.getIntValue();
+ float prevFraction = prevKeyframe.getFraction();
+ float nextFraction = nextKeyframe.getFraction();
+ final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
+ return mEvaluator == null ?
+ prevValue + (int)(intervalFraction * (nextValue - prevValue)) :
+ ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
+ intValue();
+ } else if (fraction >= 1f) {
+ final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 2);
+ final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 1);
+ int prevValue = prevKeyframe.getIntValue();
+ int nextValue = nextKeyframe.getIntValue();
+ float prevFraction = prevKeyframe.getFraction();
+ float nextFraction = nextKeyframe.getFraction();
+ final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
+ return mEvaluator == null ?
+ prevValue + (int)(intervalFraction * (nextValue - prevValue)) :
+ ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue();
+ }
+ IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0);
+ for (int i = 1; i < mNumKeyframes; ++i) {
+ IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(i);
+ if (fraction < nextKeyframe.getFraction()) {
+ final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ float intervalFraction = (fraction - prevKeyframe.getFraction()) /
+ (nextKeyframe.getFraction() - prevKeyframe.getFraction());
+ int prevValue = prevKeyframe.getIntValue();
+ int nextValue = nextKeyframe.getIntValue();
+ return mEvaluator == null ?
+ prevValue + (int)(intervalFraction * (nextValue - prevValue)) :
+ ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
+ intValue();
+ }
+ prevKeyframe = nextKeyframe;
+ }
+ // shouldn't get here
+ return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).intValue();
+ }
+
+}
+
diff --git a/app/src/main/java/com/nineoldandroids/animation/Keyframe.java b/app/src/main/java/com/nineoldandroids/animation/Keyframe.java
new file mode 100644
index 000000000..e7062a66a
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/Keyframe.java
@@ -0,0 +1,360 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+import android.view.animation.Interpolator;
+
+/**
+ * This class holds a time/value pair for an animation. The Keyframe class is used
+ * by {@link ValueAnimator} to define the values that the animation target will have over the course
+ * of the animation. As the time proceeds from one keyframe to the other, the value of the
+ * target object will animate between the value at the previous keyframe and the value at the
+ * next keyframe. Each keyframe also holds an optional {@link TimeInterpolator}
+ * object, which defines the time interpolation over the intervalue preceding the keyframe.
+ *
+ * The Keyframe class itself is abstract. The type-specific factory methods will return
+ * a subclass of Keyframe specific to the type of value being stored. This is done to improve
+ * performance when dealing with the most common cases (e.g., float and
+ * int values). Other types will fall into a more general Keyframe class that
+ * treats its values as Objects. Unless your animation requires dealing with a custom type
+ * or a data structure that needs to be animated directly (and evaluated using an implementation
+ * of {@link TypeEvaluator}), you should stick to using float and int as animations using those
+ * types have lower runtime overhead than other types.
+ */
+public abstract class Keyframe implements Cloneable {
+ /**
+ * The time at which mValue will hold true.
+ */
+ float mFraction;
+
+ /**
+ * The type of the value in this Keyframe. This type is determined at construction time,
+ * based on the type of the value object passed into the constructor.
+ */
+ Class mValueType;
+
+ /**
+ * The optional time interpolator for the interval preceding this keyframe. A null interpolator
+ * (the default) results in linear interpolation over the interval.
+ */
+ private /*Time*/Interpolator mInterpolator = null;
+
+ /**
+ * Flag to indicate whether this keyframe has a valid value. This flag is used when an
+ * animation first starts, to populate placeholder keyframes with real values derived
+ * from the target object.
+ */
+ boolean mHasValue = false;
+
+ /**
+ * Constructs a Keyframe object with the given time and value. The time defines the
+ * time, as a proportion of an overall animation's duration, at which the value will hold true
+ * for the animation. The value for the animation between keyframes will be calculated as
+ * an interpolation between the values at those keyframes.
+ *
+ * @param fraction The time, expressed as a value between 0 and 1, representing the fraction
+ * of time elapsed of the overall animation duration.
+ * @param value The value that the object will animate to as the animation time approaches
+ * the time in this keyframe, and the the value animated from as the time passes the time in
+ * this keyframe.
+ */
+ public static Keyframe ofInt(float fraction, int value) {
+ return new IntKeyframe(fraction, value);
+ }
+
+ /**
+ * Constructs a Keyframe object with the given time. The value at this time will be derived
+ * from the target object when the animation first starts (note that this implies that keyframes
+ * with no initial value must be used as part of an {@link ObjectAnimator}).
+ * The time defines the
+ * time, as a proportion of an overall animation's duration, at which the value will hold true
+ * for the animation. The value for the animation between keyframes will be calculated as
+ * an interpolation between the values at those keyframes.
+ *
+ * @param fraction The time, expressed as a value between 0 and 1, representing the fraction
+ * of time elapsed of the overall animation duration.
+ */
+ public static Keyframe ofInt(float fraction) {
+ return new IntKeyframe(fraction);
+ }
+
+ /**
+ * Constructs a Keyframe object with the given time and value. The time defines the
+ * time, as a proportion of an overall animation's duration, at which the value will hold true
+ * for the animation. The value for the animation between keyframes will be calculated as
+ * an interpolation between the values at those keyframes.
+ *
+ * @param fraction The time, expressed as a value between 0 and 1, representing the fraction
+ * of time elapsed of the overall animation duration.
+ * @param value The value that the object will animate to as the animation time approaches
+ * the time in this keyframe, and the the value animated from as the time passes the time in
+ * this keyframe.
+ */
+ public static Keyframe ofFloat(float fraction, float value) {
+ return new FloatKeyframe(fraction, value);
+ }
+
+ /**
+ * Constructs a Keyframe object with the given time. The value at this time will be derived
+ * from the target object when the animation first starts (note that this implies that keyframes
+ * with no initial value must be used as part of an {@link ObjectAnimator}).
+ * The time defines the
+ * time, as a proportion of an overall animation's duration, at which the value will hold true
+ * for the animation. The value for the animation between keyframes will be calculated as
+ * an interpolation between the values at those keyframes.
+ *
+ * @param fraction The time, expressed as a value between 0 and 1, representing the fraction
+ * of time elapsed of the overall animation duration.
+ */
+ public static Keyframe ofFloat(float fraction) {
+ return new FloatKeyframe(fraction);
+ }
+
+ /**
+ * Constructs a Keyframe object with the given time and value. The time defines the
+ * time, as a proportion of an overall animation's duration, at which the value will hold true
+ * for the animation. The value for the animation between keyframes will be calculated as
+ * an interpolation between the values at those keyframes.
+ *
+ * @param fraction The time, expressed as a value between 0 and 1, representing the fraction
+ * of time elapsed of the overall animation duration.
+ * @param value The value that the object will animate to as the animation time approaches
+ * the time in this keyframe, and the the value animated from as the time passes the time in
+ * this keyframe.
+ */
+ public static Keyframe ofObject(float fraction, Object value) {
+ return new ObjectKeyframe(fraction, value);
+ }
+
+ /**
+ * Constructs a Keyframe object with the given time. The value at this time will be derived
+ * from the target object when the animation first starts (note that this implies that keyframes
+ * with no initial value must be used as part of an {@link ObjectAnimator}).
+ * The time defines the
+ * time, as a proportion of an overall animation's duration, at which the value will hold true
+ * for the animation. The value for the animation between keyframes will be calculated as
+ * an interpolation between the values at those keyframes.
+ *
+ * @param fraction The time, expressed as a value between 0 and 1, representing the fraction
+ * of time elapsed of the overall animation duration.
+ */
+ public static Keyframe ofObject(float fraction) {
+ return new ObjectKeyframe(fraction, null);
+ }
+
+ /**
+ * Indicates whether this keyframe has a valid value. This method is called internally when
+ * an {@link ObjectAnimator} first starts; keyframes without values are assigned values at
+ * that time by deriving the value for the property from the target object.
+ *
+ * @return boolean Whether this object has a value assigned.
+ */
+ public boolean hasValue() {
+ return mHasValue;
+ }
+
+ /**
+ * Gets the value for this Keyframe.
+ *
+ * @return The value for this Keyframe.
+ */
+ public abstract Object getValue();
+
+ /**
+ * Sets the value for this Keyframe.
+ *
+ * @param value value for this Keyframe.
+ */
+ public abstract void setValue(Object value);
+
+ /**
+ * Gets the time for this keyframe, as a fraction of the overall animation duration.
+ *
+ * @return The time associated with this keyframe, as a fraction of the overall animation
+ * duration. This should be a value between 0 and 1.
+ */
+ public float getFraction() {
+ return mFraction;
+ }
+
+ /**
+ * Sets the time for this keyframe, as a fraction of the overall animation duration.
+ *
+ * @param fraction time associated with this keyframe, as a fraction of the overall animation
+ * duration. This should be a value between 0 and 1.
+ */
+ public void setFraction(float fraction) {
+ mFraction = fraction;
+ }
+
+ /**
+ * Gets the optional interpolator for this Keyframe. A value of null indicates
+ * that there is no interpolation, which is the same as linear interpolation.
+ *
+ * @return The optional interpolator for this Keyframe.
+ */
+ public /*Time*/Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * Sets the optional interpolator for this Keyframe. A value of null indicates
+ * that there is no interpolation, which is the same as linear interpolation.
+ *
+ * @return The optional interpolator for this Keyframe.
+ */
+ public void setInterpolator(/*Time*/Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ /**
+ * Gets the type of keyframe. This information is used by ValueAnimator to determine the type of
+ * {@link TypeEvaluator} to use when calculating values between keyframes. The type is based
+ * on the type of Keyframe created.
+ *
+ * @return The type of the value stored in the Keyframe.
+ */
+ public Class getType() {
+ return mValueType;
+ }
+
+ @Override
+ public abstract Keyframe clone();
+
+ /**
+ * This internal subclass is used for all types which are not int or float.
+ */
+ static class ObjectKeyframe extends Keyframe {
+
+ /**
+ * The value of the animation at the time mFraction.
+ */
+ Object mValue;
+
+ ObjectKeyframe(float fraction, Object value) {
+ mFraction = fraction;
+ mValue = value;
+ mHasValue = (value != null);
+ mValueType = mHasValue ? value.getClass() : Object.class;
+ }
+
+ public Object getValue() {
+ return mValue;
+ }
+
+ public void setValue(Object value) {
+ mValue = value;
+ mHasValue = (value != null);
+ }
+
+ @Override
+ public ObjectKeyframe clone() {
+ ObjectKeyframe kfClone = new ObjectKeyframe(getFraction(), mValue);
+ kfClone.setInterpolator(getInterpolator());
+ return kfClone;
+ }
+ }
+
+ /**
+ * Internal subclass used when the keyframe value is of type int.
+ */
+ static class IntKeyframe extends Keyframe {
+
+ /**
+ * The value of the animation at the time mFraction.
+ */
+ int mValue;
+
+ IntKeyframe(float fraction, int value) {
+ mFraction = fraction;
+ mValue = value;
+ mValueType = int.class;
+ mHasValue = true;
+ }
+
+ IntKeyframe(float fraction) {
+ mFraction = fraction;
+ mValueType = int.class;
+ }
+
+ public int getIntValue() {
+ return mValue;
+ }
+
+ public Object getValue() {
+ return mValue;
+ }
+
+ public void setValue(Object value) {
+ if (value != null && value.getClass() == Integer.class) {
+ mValue = ((Integer)value).intValue();
+ mHasValue = true;
+ }
+ }
+
+ @Override
+ public IntKeyframe clone() {
+ IntKeyframe kfClone = new IntKeyframe(getFraction(), mValue);
+ kfClone.setInterpolator(getInterpolator());
+ return kfClone;
+ }
+ }
+
+ /**
+ * Internal subclass used when the keyframe value is of type float.
+ */
+ static class FloatKeyframe extends Keyframe {
+ /**
+ * The value of the animation at the time mFraction.
+ */
+ float mValue;
+
+ FloatKeyframe(float fraction, float value) {
+ mFraction = fraction;
+ mValue = value;
+ mValueType = float.class;
+ mHasValue = true;
+ }
+
+ FloatKeyframe(float fraction) {
+ mFraction = fraction;
+ mValueType = float.class;
+ }
+
+ public float getFloatValue() {
+ return mValue;
+ }
+
+ public Object getValue() {
+ return mValue;
+ }
+
+ public void setValue(Object value) {
+ if (value != null && value.getClass() == Float.class) {
+ mValue = ((Float)value).floatValue();
+ mHasValue = true;
+ }
+ }
+
+ @Override
+ public FloatKeyframe clone() {
+ FloatKeyframe kfClone = new FloatKeyframe(getFraction(), mValue);
+ kfClone.setInterpolator(getInterpolator());
+ return kfClone;
+ }
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/animation/KeyframeSet.java b/app/src/main/java/com/nineoldandroids/animation/KeyframeSet.java
new file mode 100644
index 000000000..5694d4c3d
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/KeyframeSet.java
@@ -0,0 +1,227 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import android.view.animation.Interpolator;
+
+import com.nineoldandroids.animation.Keyframe.FloatKeyframe;
+import com.nineoldandroids.animation.Keyframe.IntKeyframe;
+import com.nineoldandroids.animation.Keyframe.ObjectKeyframe;
+
+/**
+ * This class holds a collection of Keyframe objects and is called by ValueAnimator to calculate
+ * values between those keyframes for a given animation. The class internal to the animation
+ * package because it is an implementation detail of how Keyframes are stored and used.
+ */
+class KeyframeSet {
+
+ int mNumKeyframes;
+
+ Keyframe mFirstKeyframe;
+ Keyframe mLastKeyframe;
+ /*Time*/Interpolator mInterpolator; // only used in the 2-keyframe case
+ ArrayList mKeyframes; // only used when there are not 2 keyframes
+ TypeEvaluator mEvaluator;
+
+
+ public KeyframeSet(Keyframe... keyframes) {
+ mNumKeyframes = keyframes.length;
+ mKeyframes = new ArrayList();
+ mKeyframes.addAll(Arrays.asList(keyframes));
+ mFirstKeyframe = mKeyframes.get(0);
+ mLastKeyframe = mKeyframes.get(mNumKeyframes - 1);
+ mInterpolator = mLastKeyframe.getInterpolator();
+ }
+
+ public static KeyframeSet ofInt(int... values) {
+ int numKeyframes = values.length;
+ IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)];
+ if (numKeyframes == 1) {
+ keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f);
+ keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]);
+ } else {
+ keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]);
+ for (int i = 1; i < numKeyframes; ++i) {
+ keyframes[i] = (IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]);
+ }
+ }
+ return new IntKeyframeSet(keyframes);
+ }
+
+ public static KeyframeSet ofFloat(float... values) {
+ int numKeyframes = values.length;
+ FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
+ if (numKeyframes == 1) {
+ keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
+ keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
+ } else {
+ keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
+ for (int i = 1; i < numKeyframes; ++i) {
+ keyframes[i] = (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
+ }
+ }
+ return new FloatKeyframeSet(keyframes);
+ }
+
+ public static KeyframeSet ofKeyframe(Keyframe... keyframes) {
+ // if all keyframes of same primitive type, create the appropriate KeyframeSet
+ int numKeyframes = keyframes.length;
+ boolean hasFloat = false;
+ boolean hasInt = false;
+ boolean hasOther = false;
+ for (int i = 0; i < numKeyframes; ++i) {
+ if (keyframes[i] instanceof FloatKeyframe) {
+ hasFloat = true;
+ } else if (keyframes[i] instanceof IntKeyframe) {
+ hasInt = true;
+ } else {
+ hasOther = true;
+ }
+ }
+ if (hasFloat && !hasInt && !hasOther) {
+ FloatKeyframe floatKeyframes[] = new FloatKeyframe[numKeyframes];
+ for (int i = 0; i < numKeyframes; ++i) {
+ floatKeyframes[i] = (FloatKeyframe) keyframes[i];
+ }
+ return new FloatKeyframeSet(floatKeyframes);
+ } else if (hasInt && !hasFloat && !hasOther) {
+ IntKeyframe intKeyframes[] = new IntKeyframe[numKeyframes];
+ for (int i = 0; i < numKeyframes; ++i) {
+ intKeyframes[i] = (IntKeyframe) keyframes[i];
+ }
+ return new IntKeyframeSet(intKeyframes);
+ } else {
+ return new KeyframeSet(keyframes);
+ }
+ }
+
+ public static KeyframeSet ofObject(Object... values) {
+ int numKeyframes = values.length;
+ ObjectKeyframe keyframes[] = new ObjectKeyframe[Math.max(numKeyframes,2)];
+ if (numKeyframes == 1) {
+ keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f);
+ keyframes[1] = (ObjectKeyframe) Keyframe.ofObject(1f, values[0]);
+ } else {
+ keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f, values[0]);
+ for (int i = 1; i < numKeyframes; ++i) {
+ keyframes[i] = (ObjectKeyframe) Keyframe.ofObject((float) i / (numKeyframes - 1), values[i]);
+ }
+ }
+ return new KeyframeSet(keyframes);
+ }
+
+ /**
+ * Sets the TypeEvaluator to be used when calculating animated values. This object
+ * is required only for KeyframeSets that are not either IntKeyframeSet or FloatKeyframeSet,
+ * both of which assume their own evaluator to speed up calculations with those primitive
+ * types.
+ *
+ * @param evaluator The TypeEvaluator to be used to calculate animated values.
+ */
+ public void setEvaluator(TypeEvaluator evaluator) {
+ mEvaluator = evaluator;
+ }
+
+ @Override
+ public KeyframeSet clone() {
+ ArrayList keyframes = mKeyframes;
+ int numKeyframes = mKeyframes.size();
+ Keyframe[] newKeyframes = new Keyframe[numKeyframes];
+ for (int i = 0; i < numKeyframes; ++i) {
+ newKeyframes[i] = keyframes.get(i).clone();
+ }
+ KeyframeSet newSet = new KeyframeSet(newKeyframes);
+ return newSet;
+ }
+
+ /**
+ * Gets the animated value, given the elapsed fraction of the animation (interpolated by the
+ * animation's interpolator) and the evaluator used to calculate in-between values. This
+ * function maps the input fraction to the appropriate keyframe interval and a fraction
+ * between them and returns the interpolated value. Note that the input fraction may fall
+ * outside the [0-1] bounds, if the animation's interpolator made that happen (e.g., a
+ * spring interpolation that might send the fraction past 1.0). We handle this situation by
+ * just using the two keyframes at the appropriate end when the value is outside those bounds.
+ *
+ * @param fraction The elapsed fraction of the animation
+ * @return The animated value.
+ */
+ public Object getValue(float fraction) {
+
+ // Special-case optimization for the common case of only two keyframes
+ if (mNumKeyframes == 2) {
+ if (mInterpolator != null) {
+ fraction = mInterpolator.getInterpolation(fraction);
+ }
+ return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(),
+ mLastKeyframe.getValue());
+ }
+ if (fraction <= 0f) {
+ final Keyframe nextKeyframe = mKeyframes.get(1);
+ final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ final float prevFraction = mFirstKeyframe.getFraction();
+ float intervalFraction = (fraction - prevFraction) /
+ (nextKeyframe.getFraction() - prevFraction);
+ return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(),
+ nextKeyframe.getValue());
+ } else if (fraction >= 1f) {
+ final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2);
+ final /*Time*/Interpolator interpolator = mLastKeyframe.getInterpolator();
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ final float prevFraction = prevKeyframe.getFraction();
+ float intervalFraction = (fraction - prevFraction) /
+ (mLastKeyframe.getFraction() - prevFraction);
+ return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
+ mLastKeyframe.getValue());
+ }
+ Keyframe prevKeyframe = mFirstKeyframe;
+ for (int i = 1; i < mNumKeyframes; ++i) {
+ Keyframe nextKeyframe = mKeyframes.get(i);
+ if (fraction < nextKeyframe.getFraction()) {
+ final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ final float prevFraction = prevKeyframe.getFraction();
+ float intervalFraction = (fraction - prevFraction) /
+ (nextKeyframe.getFraction() - prevFraction);
+ return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
+ nextKeyframe.getValue());
+ }
+ prevKeyframe = nextKeyframe;
+ }
+ // shouldn't reach here
+ return mLastKeyframe.getValue();
+ }
+
+ @Override
+ public String toString() {
+ String returnVal = " ";
+ for (int i = 0; i < mNumKeyframes; ++i) {
+ returnVal += mKeyframes.get(i).getValue() + " ";
+ }
+ return returnVal;
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/animation/ObjectAnimator.java b/app/src/main/java/com/nineoldandroids/animation/ObjectAnimator.java
new file mode 100644
index 000000000..2dc2aa0a4
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/ObjectAnimator.java
@@ -0,0 +1,583 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+import android.util.Log;
+import android.view.View;
+
+import com.nineoldandroids.util.Property;
+import com.nineoldandroids.view.animation.AnimatorProxy;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This subclass of {@link com.nineoldandroids.animation.ValueAnimator} provides support for animating properties on target objects.
+ * The constructors of this class take parameters to define the target object that will be animated
+ * as well as the name of the property that will be animated. Appropriate set/get functions
+ * are then determined internally and the animation will call these functions as necessary to
+ * animate the property.
+ *
+ * @see #setPropertyName(String)
+ *
+ */
+public final class ObjectAnimator extends ValueAnimator {
+ private static final boolean DBG = false;
+ private static final Map PROXY_PROPERTIES = new HashMap();
+
+ static {
+ PROXY_PROPERTIES.put("alpha", PreHoneycombCompat.ALPHA);
+ PROXY_PROPERTIES.put("pivotX", PreHoneycombCompat.PIVOT_X);
+ PROXY_PROPERTIES.put("pivotY", PreHoneycombCompat.PIVOT_Y);
+ PROXY_PROPERTIES.put("translationX", PreHoneycombCompat.TRANSLATION_X);
+ PROXY_PROPERTIES.put("translationY", PreHoneycombCompat.TRANSLATION_Y);
+ PROXY_PROPERTIES.put("rotation", PreHoneycombCompat.ROTATION);
+ PROXY_PROPERTIES.put("rotationX", PreHoneycombCompat.ROTATION_X);
+ PROXY_PROPERTIES.put("rotationY", PreHoneycombCompat.ROTATION_Y);
+ PROXY_PROPERTIES.put("scaleX", PreHoneycombCompat.SCALE_X);
+ PROXY_PROPERTIES.put("scaleY", PreHoneycombCompat.SCALE_Y);
+ PROXY_PROPERTIES.put("scrollX", PreHoneycombCompat.SCROLL_X);
+ PROXY_PROPERTIES.put("scrollY", PreHoneycombCompat.SCROLL_Y);
+ PROXY_PROPERTIES.put("x", PreHoneycombCompat.X);
+ PROXY_PROPERTIES.put("y", PreHoneycombCompat.Y);
+ }
+
+ // The target object on which the property exists, set in the constructor
+ private Object mTarget;
+
+ private String mPropertyName;
+
+ private Property mProperty;
+
+ private boolean mAutoCancel = false;
+
+ /**
+ * Sets the name of the property that will be animated. This name is used to derive
+ * a setter function that will be called to set animated values.
+ * For example, a property name of foo will result
+ * in a call to the function setFoo() on the target object. If either
+ * valueFrom or valueTo is null, then a getter function will
+ * also be derived and called.
+ *
+ * For best performance of the mechanism that calls the setter function determined by the
+ * name of the property being animated, use float or int typed values,
+ * and make the setter function for those properties have a void return value. This
+ * will cause the code to take an optimized path for these constrained circumstances. Other
+ * property types and return types will work, but will have more overhead in processing
+ * the requests due to normal reflection mechanisms.
+ *
+ * Note that the setter function derived from this property name
+ * must take the same parameter type as the
+ * valueFrom and valueTo properties, otherwise the call to
+ * the setter function will fail.
+ *
+ * If this ObjectAnimator has been set up to animate several properties together,
+ * using more than one PropertyValuesHolder objects, then setting the propertyName simply
+ * sets the propertyName in the first of those PropertyValuesHolder objects.
+ *
+ * @param propertyName The name of the property being animated. Should not be null.
+ */
+ public void setPropertyName(String propertyName) {
+ // mValues could be null if this is being constructed piecemeal. Just record the
+ // propertyName to be used later when setValues() is called if so.
+ if (mValues != null) {
+ PropertyValuesHolder valuesHolder = mValues[0];
+ String oldName = valuesHolder.getPropertyName();
+ valuesHolder.setPropertyName(propertyName);
+ mValuesMap.remove(oldName);
+ mValuesMap.put(propertyName, valuesHolder);
+ }
+ mPropertyName = propertyName;
+ // New property/values/target should cause re-initialization prior to starting
+ mInitialized = false;
+ }
+
+ /**
+ * Sets the property that will be animated. Property objects will take precedence over
+ * properties specified by the {@link #setPropertyName(String)} method. Animations should
+ * be set up to use one or the other, not both.
+ *
+ * @param property The property being animated. Should not be null.
+ */
+ public void setProperty(Property property) {
+ // mValues could be null if this is being constructed piecemeal. Just record the
+ // propertyName to be used later when setValues() is called if so.
+ if (mValues != null) {
+ PropertyValuesHolder valuesHolder = mValues[0];
+ String oldName = valuesHolder.getPropertyName();
+ valuesHolder.setProperty(property);
+ mValuesMap.remove(oldName);
+ mValuesMap.put(mPropertyName, valuesHolder);
+ }
+ if (mProperty != null) {
+ mPropertyName = property.getName();
+ }
+ mProperty = property;
+ // New property/values/target should cause re-initialization prior to starting
+ mInitialized = false;
+ }
+
+ /**
+ * Gets the name of the property that will be animated. This name will be used to derive
+ * a setter function that will be called to set animated values.
+ * For example, a property name of foo will result
+ * in a call to the function setFoo() on the target object. If either
+ * valueFrom or valueTo is null, then a getter function will
+ * also be derived and called.
+ */
+ public String getPropertyName() {
+ return mPropertyName;
+ }
+
+ /**
+ * Creates a new ObjectAnimator object. This default constructor is primarily for
+ * use internally; the other constructors which take parameters are more generally
+ * useful.
+ */
+ public ObjectAnimator() {
+ }
+
+ /**
+ * Private utility constructor that initializes the target object and name of the
+ * property being animated.
+ *
+ * @param target The object whose property is to be animated. This object should
+ * have a public method on it called setName(), where name is
+ * the value of the propertyName parameter.
+ * @param propertyName The name of the property being animated.
+ */
+ private ObjectAnimator(Object target, String propertyName) {
+ mTarget = target;
+ setPropertyName(propertyName);
+ }
+
+ /**
+ * Private utility constructor that initializes the target object and property being animated.
+ *
+ * @param target The object whose property is to be animated.
+ * @param property The property being animated.
+ */
+ private ObjectAnimator(T target, Property property) {
+ mTarget = target;
+ setProperty(property);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between int values. A single
+ * value implies that that value is the one being animated to. Two values imply a starting
+ * and ending values. More than two values imply a starting value, values to animate through
+ * along the way, and an ending value (these values will be distributed evenly across
+ * the duration of the animation).
+ *
+ * @param target The object whose property is to be animated. This object should
+ * have a public method on it called setName(), where name is
+ * the value of the propertyName parameter.
+ * @param propertyName The name of the property being animated.
+ * @param values A set of values that the animation will animate between over time.
+ * @return An ObjectAnimator object that is set up to animate between the given values.
+ */
+ public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
+ ObjectAnimator anim = new ObjectAnimator(target, propertyName);
+ anim.setIntValues(values);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between int values. A single
+ * value implies that that value is the one being animated to. Two values imply a starting
+ * and ending values. More than two values imply a starting value, values to animate through
+ * along the way, and an ending value (these values will be distributed evenly across
+ * the duration of the animation).
+ *
+ * @param target The object whose property is to be animated.
+ * @param property The property being animated.
+ * @param values A set of values that the animation will animate between over time.
+ * @return An ObjectAnimator object that is set up to animate between the given values.
+ */
+ public static ObjectAnimator ofInt(T target, Property property, int... values) {
+ ObjectAnimator anim = new ObjectAnimator(target, property);
+ anim.setIntValues(values);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between float values. A single
+ * value implies that that value is the one being animated to. Two values imply a starting
+ * and ending values. More than two values imply a starting value, values to animate through
+ * along the way, and an ending value (these values will be distributed evenly across
+ * the duration of the animation).
+ *
+ * @param target The object whose property is to be animated. This object should
+ * have a public method on it called setName(), where name is
+ * the value of the propertyName parameter.
+ * @param propertyName The name of the property being animated.
+ * @param values A set of values that the animation will animate between over time.
+ * @return An ObjectAnimator object that is set up to animate between the given values.
+ */
+ public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
+ ObjectAnimator anim = new ObjectAnimator(target, propertyName);
+ anim.setFloatValues(values);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between float values. A single
+ * value implies that that value is the one being animated to. Two values imply a starting
+ * and ending values. More than two values imply a starting value, values to animate through
+ * along the way, and an ending value (these values will be distributed evenly across
+ * the duration of the animation).
+ *
+ * @param target The object whose property is to be animated.
+ * @param property The property being animated.
+ * @param values A set of values that the animation will animate between over time.
+ * @return An ObjectAnimator object that is set up to animate between the given values.
+ */
+ public static ObjectAnimator ofFloat(T target, Property property,
+ float... values) {
+ ObjectAnimator anim = new ObjectAnimator(target, property);
+ anim.setFloatValues(values);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between Object values. A single
+ * value implies that that value is the one being animated to. Two values imply a starting
+ * and ending values. More than two values imply a starting value, values to animate through
+ * along the way, and an ending value (these values will be distributed evenly across
+ * the duration of the animation).
+ *
+ * @param target The object whose property is to be animated. This object should
+ * have a public method on it called setName(), where name is
+ * the value of the propertyName parameter.
+ * @param propertyName The name of the property being animated.
+ * @param evaluator A TypeEvaluator that will be called on each animation frame to
+ * provide the necessary interpolation between the Object values to derive the animated
+ * value.
+ * @param values A set of values that the animation will animate between over time.
+ * @return An ObjectAnimator object that is set up to animate between the given values.
+ */
+ public static ObjectAnimator ofObject(Object target, String propertyName,
+ TypeEvaluator evaluator, Object... values) {
+ ObjectAnimator anim = new ObjectAnimator(target, propertyName);
+ anim.setObjectValues(values);
+ anim.setEvaluator(evaluator);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between Object values. A single
+ * value implies that that value is the one being animated to. Two values imply a starting
+ * and ending values. More than two values imply a starting value, values to animate through
+ * along the way, and an ending value (these values will be distributed evenly across
+ * the duration of the animation).
+ *
+ * @param target The object whose property is to be animated.
+ * @param property The property being animated.
+ * @param evaluator A TypeEvaluator that will be called on each animation frame to
+ * provide the necessary interpolation between the Object values to derive the animated
+ * value.
+ * @param values A set of values that the animation will animate between over time.
+ * @return An ObjectAnimator object that is set up to animate between the given values.
+ */
+ public static ObjectAnimator ofObject(T target, Property property,
+ TypeEvaluator evaluator, V... values) {
+ ObjectAnimator anim = new ObjectAnimator(target, property);
+ anim.setObjectValues(values);
+ anim.setEvaluator(evaluator);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between the sets of values specified
+ * in PropertyValueHolder objects. This variant should be used when animating
+ * several properties at once with the same ObjectAnimator, since PropertyValuesHolder allows
+ * you to associate a set of animation values with a property name.
+ *
+ * @param target The object whose property is to be animated. Depending on how the
+ * PropertyValuesObjects were constructed, the target object should either have the {@link
+ * android.util.Property} objects used to construct the PropertyValuesHolder objects or (if the
+ * PropertyValuesHOlder objects were created with property names) the target object should have
+ * public methods on it called setName(), where name is the name of
+ * the property passed in as the propertyName parameter for each of the
+ * PropertyValuesHolder objects.
+ * @param values A set of PropertyValuesHolder objects whose values will be animated between
+ * over time.
+ * @return An ObjectAnimator object that is set up to animate between the given values.
+ */
+ public static ObjectAnimator ofPropertyValuesHolder(Object target,
+ PropertyValuesHolder... values) {
+ ObjectAnimator anim = new ObjectAnimator();
+ anim.mTarget = target;
+ anim.setValues(values);
+ return anim;
+ }
+
+ @Override
+ public void setIntValues(int... values) {
+ if (mValues == null || mValues.length == 0) {
+ // No values yet - this animator is being constructed piecemeal. Init the values with
+ // whatever the current propertyName is
+ if (mProperty != null) {
+ setValues(PropertyValuesHolder.ofInt(mProperty, values));
+ } else {
+ setValues(PropertyValuesHolder.ofInt(mPropertyName, values));
+ }
+ } else {
+ super.setIntValues(values);
+ }
+ }
+
+ @Override
+ public void setFloatValues(float... values) {
+ if (mValues == null || mValues.length == 0) {
+ // No values yet - this animator is being constructed piecemeal. Init the values with
+ // whatever the current propertyName is
+ if (mProperty != null) {
+ setValues(PropertyValuesHolder.ofFloat(mProperty, values));
+ } else {
+ setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
+ }
+ } else {
+ super.setFloatValues(values);
+ }
+ }
+
+ @Override
+ public void setObjectValues(Object... values) {
+ if (mValues == null || mValues.length == 0) {
+ // No values yet - this animator is being constructed piecemeal. Init the values with
+ // whatever the current propertyName is
+ if (mProperty != null) {
+ setValues(PropertyValuesHolder.ofObject(mProperty, (TypeEvaluator)null, values));
+ } else {
+ setValues(PropertyValuesHolder.ofObject(mPropertyName, (TypeEvaluator)null, values));
+ }
+ } else {
+ super.setObjectValues(values);
+ }
+ }
+
+
+ /**
+ * autoCancel controls whether an ObjectAnimator will be canceled automatically
+ * when any other ObjectAnimator with the same target and properties is started.
+ * Setting this flag may make it easier to run different animators on the same target
+ * object without having to keep track of whether there are conflicting animators that
+ * need to be manually canceled. Canceling animators must have the same exact set of
+ * target properties, in the same order.
+ *
+ * @param cancel Whether future ObjectAnimators with the same target and properties
+ * as this ObjectAnimator will cause this ObjectAnimator to be canceled.
+ */
+ public void setAutoCancel(boolean cancel) {
+ mAutoCancel = cancel;
+ }
+
+ private boolean hasSameTargetAndProperties(Animator anim) {
+ if (anim instanceof ObjectAnimator) {
+ PropertyValuesHolder[] theirValues = ((ObjectAnimator) anim).getValues();
+ if (((ObjectAnimator) anim).getTarget() == getTarget() &&
+ mValues.length == theirValues.length) {
+ for (int i = 0; i < mValues.length; ++i) {
+ PropertyValuesHolder pvhMine = mValues[i];
+ PropertyValuesHolder pvhTheirs = theirValues[i];
+ if (pvhMine.getPropertyName() == null ||
+ !pvhMine.getPropertyName().equals(pvhTheirs.getPropertyName())) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void start() {
+ // See if any of the current active/pending animators need to be canceled
+ AnimationHandler handler = sAnimationHandler.get();
+ if (handler != null) {
+ int numAnims = sAnimations.get().size();
+ for (int i = numAnims - 1; i >= 0; i--) {
+ if (sAnimations.get().get(i) instanceof ObjectAnimator) {
+ ObjectAnimator anim = (ObjectAnimator) sAnimations.get().get(i);
+ if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
+ anim.cancel();
+ }
+ }
+ }
+ numAnims = sPendingAnimations.get().size();
+ for (int i = numAnims - 1; i >= 0; i--) {
+ if (sPendingAnimations.get().get(i) instanceof ObjectAnimator) {
+ ObjectAnimator anim = (ObjectAnimator) sPendingAnimations.get().get(i);
+ if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
+ anim.cancel();
+ }
+ }
+ }
+ numAnims = sDelayedAnims.get().size();
+ for (int i = numAnims - 1; i >= 0; i--) {
+ if (sDelayedAnims.get().get(i) instanceof ObjectAnimator) {
+ ObjectAnimator anim = (ObjectAnimator) sDelayedAnims.get().get(i);
+ if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
+ anim.cancel();
+ }
+ }
+ }
+ }
+ if (DBG) {
+ Log.d("ObjectAnimator", "Anim target, duration: " + mTarget + ", " + getDuration());
+ for (int i = 0; i < mValues.length; ++i) {
+ PropertyValuesHolder pvh = mValues[i];
+ ArrayList keyframes = pvh.mKeyframeSet.mKeyframes;
+ Log.d("ObjectAnimator", " Values[" + i + "]: " +
+ pvh.getPropertyName() + ", " + keyframes.get(0).getValue() + ", " +
+ keyframes.get(pvh.mKeyframeSet.mNumKeyframes - 1).getValue());
+ }
+ }
+ super.start();
+ }
+
+ /**
+ * This function is called immediately before processing the first animation
+ * frame of an animation. If there is a nonzero startDelay, the
+ * function is called after that delay ends.
+ * It takes care of the final initialization steps for the
+ * animation. This includes setting mEvaluator, if the user has not yet
+ * set it up, and the setter/getter methods, if the user did not supply
+ * them.
+ *
+ * Overriders of this method should call the superclass method to cause
+ * internal mechanisms to be set up correctly.
+ */
+ @Override
+ void initAnimation() {
+ if (!mInitialized) {
+ // mValueType may change due to setter/getter setup; do this before calling super.init(),
+ // which uses mValueType to set up the default type evaluator.
+ if ((mProperty == null) && AnimatorProxy.NEEDS_PROXY && (mTarget instanceof View) && PROXY_PROPERTIES.containsKey(mPropertyName)) {
+ setProperty(PROXY_PROPERTIES.get(mPropertyName));
+ }
+ int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].setupSetterAndGetter(mTarget);
+ }
+ super.initAnimation();
+ }
+ }
+
+ /**
+ * Sets the length of the animation. The default duration is 300 milliseconds.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @return ObjectAnimator The object called with setDuration(). This return
+ * value makes it easier to compose statements together that construct and then set the
+ * duration, as in
+ * ObjectAnimator.ofInt(target, propertyName, 0, 10).setDuration(500).start().
+ */
+ @Override
+ public ObjectAnimator setDuration(long duration) {
+ super.setDuration(duration);
+ return this;
+ }
+
+
+ /**
+ * The target object whose property will be animated by this animation
+ *
+ * @return The object being animated
+ */
+ public Object getTarget() {
+ return mTarget;
+ }
+
+ /**
+ * Sets the target object whose property will be animated by this animation
+ *
+ * @param target The object being animated
+ */
+ @Override
+ public void setTarget(Object target) {
+ if (mTarget != target) {
+ final Object oldTarget = mTarget;
+ mTarget = target;
+ if (oldTarget != null && target != null && oldTarget.getClass() == target.getClass()) {
+ return;
+ }
+ // New target type should cause re-initialization prior to starting
+ mInitialized = false;
+ }
+ }
+
+ @Override
+ public void setupStartValues() {
+ initAnimation();
+ int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].setupStartValue(mTarget);
+ }
+ }
+
+ @Override
+ public void setupEndValues() {
+ initAnimation();
+ int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].setupEndValue(mTarget);
+ }
+ }
+
+ /**
+ * This method is called with the elapsed fraction of the animation during every
+ * animation frame. This function turns the elapsed fraction into an interpolated fraction
+ * and then into an animated value (from the evaluator. The function is called mostly during
+ * animation updates, but it is also called when the end()
+ * function is called, to set the final value on the property.
+ *
+ * Overrides of this method must call the superclass to perform the calculation
+ * of the animated value.
+ *
+ * @param fraction The elapsed fraction of the animation.
+ */
+ @Override
+ void animateValue(float fraction) {
+ super.animateValue(fraction);
+ int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].setAnimatedValue(mTarget);
+ }
+ }
+
+ @Override
+ public ObjectAnimator clone() {
+ final ObjectAnimator anim = (ObjectAnimator) super.clone();
+ return anim;
+ }
+
+ @Override
+ public String toString() {
+ String returnVal = "ObjectAnimator@" + Integer.toHexString(hashCode()) + ", target " +
+ mTarget;
+ if (mValues != null) {
+ for (int i = 0; i < mValues.length; ++i) {
+ returnVal += "\n " + mValues[i].toString();
+ }
+ }
+ return returnVal;
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/animation/PreHoneycombCompat.java b/app/src/main/java/com/nineoldandroids/animation/PreHoneycombCompat.java
new file mode 100644
index 000000000..8a68d82c0
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/PreHoneycombCompat.java
@@ -0,0 +1,168 @@
+package com.nineoldandroids.animation;
+
+import android.view.View;
+import com.nineoldandroids.util.FloatProperty;
+import com.nineoldandroids.util.IntProperty;
+import com.nineoldandroids.util.Property;
+import com.nineoldandroids.view.animation.AnimatorProxy;
+
+final class PreHoneycombCompat {
+ static Property ALPHA = new FloatProperty("alpha") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setAlpha(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getAlpha();
+ }
+ };
+ static Property PIVOT_X = new FloatProperty("pivotX") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setPivotX(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getPivotX();
+ }
+ };
+ static Property PIVOT_Y = new FloatProperty("pivotY") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setPivotY(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getPivotY();
+ }
+ };
+ static Property TRANSLATION_X = new FloatProperty("translationX") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setTranslationX(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getTranslationX();
+ }
+ };
+ static Property TRANSLATION_Y = new FloatProperty("translationY") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setTranslationY(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getTranslationY();
+ }
+ };
+ static Property ROTATION = new FloatProperty("rotation") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setRotation(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getRotation();
+ }
+ };
+ static Property ROTATION_X = new FloatProperty("rotationX") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setRotationX(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getRotationX();
+ }
+ };
+ static Property ROTATION_Y = new FloatProperty("rotationY") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setRotationY(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getRotationY();
+ }
+ };
+ static Property SCALE_X = new FloatProperty("scaleX") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setScaleX(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getScaleX();
+ }
+ };
+ static Property SCALE_Y = new FloatProperty("scaleY") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setScaleY(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getScaleY();
+ }
+ };
+ static Property SCROLL_X = new IntProperty("scrollX") {
+ @Override
+ public void setValue(View object, int value) {
+ AnimatorProxy.wrap(object).setScrollX(value);
+ }
+
+ @Override
+ public Integer get(View object) {
+ return AnimatorProxy.wrap(object).getScrollX();
+ }
+ };
+ static Property SCROLL_Y = new IntProperty("scrollY") {
+ @Override
+ public void setValue(View object, int value) {
+ AnimatorProxy.wrap(object).setScrollY(value);
+ }
+
+ @Override
+ public Integer get(View object) {
+ return AnimatorProxy.wrap(object).getScrollY();
+ }
+ };
+ static Property X = new FloatProperty("x") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setX(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getX();
+ }
+ };
+ static Property Y = new FloatProperty("y") {
+ @Override
+ public void setValue(View object, float value) {
+ AnimatorProxy.wrap(object).setY(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return AnimatorProxy.wrap(object).getY();
+ }
+ };
+
+
+ //No instances
+ private PreHoneycombCompat() {}
+}
diff --git a/app/src/main/java/com/nineoldandroids/animation/PropertyValuesHolder.java b/app/src/main/java/com/nineoldandroids/animation/PropertyValuesHolder.java
new file mode 100644
index 000000000..a78e6b5e5
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/PropertyValuesHolder.java
@@ -0,0 +1,1030 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.nineoldandroids.util.FloatProperty;
+import com.nineoldandroids.util.IntProperty;
+import com.nineoldandroids.util.Property;
+
+/**
+ * This class holds information about a property and the values that that property
+ * should take on during an animation. PropertyValuesHolder objects can be used to create
+ * animations with ValueAnimator or ObjectAnimator that operate on several different properties
+ * in parallel.
+ */
+public class PropertyValuesHolder implements Cloneable {
+
+ /**
+ * The name of the property associated with the values. This need not be a real property,
+ * unless this object is being used with ObjectAnimator. But this is the name by which
+ * aniamted values are looked up with getAnimatedValue(String) in ValueAnimator.
+ */
+ String mPropertyName;
+
+ /**
+ * @hide
+ */
+ protected Property mProperty;
+
+ /**
+ * The setter function, if needed. ObjectAnimator hands off this functionality to
+ * PropertyValuesHolder, since it holds all of the per-property information. This
+ * property is automatically
+ * derived when the animation starts in setupSetterAndGetter() if using ObjectAnimator.
+ */
+ Method mSetter = null;
+
+ /**
+ * The getter function, if needed. ObjectAnimator hands off this functionality to
+ * PropertyValuesHolder, since it holds all of the per-property information. This
+ * property is automatically
+ * derived when the animation starts in setupSetterAndGetter() if using ObjectAnimator.
+ * The getter is only derived and used if one of the values is null.
+ */
+ private Method mGetter = null;
+
+ /**
+ * The type of values supplied. This information is used both in deriving the setter/getter
+ * functions and in deriving the type of TypeEvaluator.
+ */
+ Class mValueType;
+
+ /**
+ * The set of keyframes (time/value pairs) that define this animation.
+ */
+ KeyframeSet mKeyframeSet = null;
+
+
+ // type evaluators for the primitive types handled by this implementation
+ private static final TypeEvaluator sIntEvaluator = new IntEvaluator();
+ private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator();
+
+ // We try several different types when searching for appropriate setter/getter functions.
+ // The caller may have supplied values in a type that does not match the setter/getter
+ // functions (such as the integers 0 and 1 to represent floating point values for alpha).
+ // Also, the use of generics in constructors means that we end up with the Object versions
+ // of primitive types (Float vs. float). But most likely, the setter/getter functions
+ // will take primitive types instead.
+ // So we supply an ordered array of other types to try before giving up.
+ private static Class[] FLOAT_VARIANTS = {float.class, Float.class, double.class, int.class,
+ Double.class, Integer.class};
+ private static Class[] INTEGER_VARIANTS = {int.class, Integer.class, float.class, double.class,
+ Float.class, Double.class};
+ private static Class[] DOUBLE_VARIANTS = {double.class, Double.class, float.class, int.class,
+ Float.class, Integer.class};
+
+ // These maps hold all property entries for a particular class. This map
+ // is used to speed up property/setter/getter lookups for a given class/property
+ // combination. No need to use reflection on the combination more than once.
+ private static final HashMap> sSetterPropertyMap =
+ new HashMap>();
+ private static final HashMap> sGetterPropertyMap =
+ new HashMap>();
+
+ // This lock is used to ensure that only one thread is accessing the property maps
+ // at a time.
+ final ReentrantReadWriteLock mPropertyMapLock = new ReentrantReadWriteLock();
+
+ // Used to pass single value to varargs parameter in setter invocation
+ final Object[] mTmpValueArray = new Object[1];
+
+ /**
+ * The type evaluator used to calculate the animated values. This evaluator is determined
+ * automatically based on the type of the start/end objects passed into the constructor,
+ * but the system only knows about the primitive types int and float. Any other
+ * type will need to set the evaluator to a custom evaluator for that type.
+ */
+ private TypeEvaluator mEvaluator;
+
+ /**
+ * The value most recently calculated by calculateValue(). This is set during
+ * that function and might be retrieved later either by ValueAnimator.animatedValue() or
+ * by the property-setting logic in ObjectAnimator.animatedValue().
+ */
+ private Object mAnimatedValue;
+
+ /**
+ * Internal utility constructor, used by the factory methods to set the property name.
+ * @param propertyName The name of the property for this holder.
+ */
+ private PropertyValuesHolder(String propertyName) {
+ mPropertyName = propertyName;
+ }
+
+ /**
+ * Internal utility constructor, used by the factory methods to set the property.
+ * @param property The property for this holder.
+ */
+ private PropertyValuesHolder(Property property) {
+ mProperty = property;
+ if (property != null) {
+ mPropertyName = property.getName();
+ }
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property name and
+ * set of int values.
+ * @param propertyName The name of the property being animated.
+ * @param values The values that the named property will animate between.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ */
+ public static PropertyValuesHolder ofInt(String propertyName, int... values) {
+ return new IntPropertyValuesHolder(propertyName, values);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property and
+ * set of int values.
+ * @param property The property being animated. Should not be null.
+ * @param values The values that the property will animate between.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ */
+ public static PropertyValuesHolder ofInt(Property, Integer> property, int... values) {
+ return new IntPropertyValuesHolder(property, values);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property name and
+ * set of float values.
+ * @param propertyName The name of the property being animated.
+ * @param values The values that the named property will animate between.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ */
+ public static PropertyValuesHolder ofFloat(String propertyName, float... values) {
+ return new FloatPropertyValuesHolder(propertyName, values);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property and
+ * set of float values.
+ * @param property The property being animated. Should not be null.
+ * @param values The values that the property will animate between.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ */
+ public static PropertyValuesHolder ofFloat(Property, Float> property, float... values) {
+ return new FloatPropertyValuesHolder(property, values);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property name and
+ * set of Object values. This variant also takes a TypeEvaluator because the system
+ * cannot automatically interpolate between objects of unknown type.
+ *
+ * @param propertyName The name of the property being animated.
+ * @param evaluator A TypeEvaluator that will be called on each animation frame to
+ * provide the necessary interpolation between the Object values to derive the animated
+ * value.
+ * @param values The values that the named property will animate between.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ */
+ public static PropertyValuesHolder ofObject(String propertyName, TypeEvaluator evaluator,
+ Object... values) {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
+ pvh.setObjectValues(values);
+ pvh.setEvaluator(evaluator);
+ return pvh;
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property and
+ * set of Object values. This variant also takes a TypeEvaluator because the system
+ * cannot automatically interpolate between objects of unknown type.
+ *
+ * @param property The property being animated. Should not be null.
+ * @param evaluator A TypeEvaluator that will be called on each animation frame to
+ * provide the necessary interpolation between the Object values to derive the animated
+ * value.
+ * @param values The values that the property will animate between.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ */
+ public static PropertyValuesHolder ofObject(Property property,
+ TypeEvaluator evaluator, V... values) {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(property);
+ pvh.setObjectValues(values);
+ pvh.setEvaluator(evaluator);
+ return pvh;
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder object with the specified property name and set
+ * of values. These values can be of any type, but the type should be consistent so that
+ * an appropriate {@link android.animation.TypeEvaluator} can be found that matches
+ * the common type.
+ * If there is only one value, it is assumed to be the end value of an animation,
+ * and an initial value will be derived, if possible, by calling a getter function
+ * on the object. Also, if any value is null, the value will be filled in when the animation
+ * starts in the same way. This mechanism of automatically getting null values only works
+ * if the PropertyValuesHolder object is used in conjunction
+ * {@link com.nineoldandroids.animation.ObjectAnimator}, and with a getter function
+ * derived automatically from propertyName, since otherwise PropertyValuesHolder has
+ * no way of determining what the value should be.
+ * @param propertyName The name of the property associated with this set of values. This
+ * can be the actual property name to be used when using a ObjectAnimator object, or
+ * just a name used to get animated values, such as if this object is used with an
+ * ValueAnimator object.
+ * @param values The set of values to animate between.
+ */
+ public static PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values) {
+ KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
+ if (keyframeSet instanceof IntKeyframeSet) {
+ return new IntPropertyValuesHolder(propertyName, (IntKeyframeSet) keyframeSet);
+ } else if (keyframeSet instanceof FloatKeyframeSet) {
+ return new FloatPropertyValuesHolder(propertyName, (FloatKeyframeSet) keyframeSet);
+ }
+ else {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
+ pvh.mKeyframeSet = keyframeSet;
+ pvh.mValueType = ((Keyframe)values[0]).getType();
+ return pvh;
+ }
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder object with the specified property and set
+ * of values. These values can be of any type, but the type should be consistent so that
+ * an appropriate {@link android.animation.TypeEvaluator} can be found that matches
+ * the common type.
+ *
If there is only one value, it is assumed to be the end value of an animation,
+ * and an initial value will be derived, if possible, by calling the property's
+ * {@link android.util.Property#get(Object)} function.
+ * Also, if any value is null, the value will be filled in when the animation
+ * starts in the same way. This mechanism of automatically getting null values only works
+ * if the PropertyValuesHolder object is used in conjunction with
+ * {@link com.nineoldandroids.animation.ObjectAnimator}, since otherwise PropertyValuesHolder has
+ * no way of determining what the value should be.
+ * @param property The property associated with this set of values. Should not be null.
+ * @param values The set of values to animate between.
+ */
+ public static PropertyValuesHolder ofKeyframe(Property property, Keyframe... values) {
+ KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
+ if (keyframeSet instanceof IntKeyframeSet) {
+ return new IntPropertyValuesHolder(property, (IntKeyframeSet) keyframeSet);
+ } else if (keyframeSet instanceof FloatKeyframeSet) {
+ return new FloatPropertyValuesHolder(property, (FloatKeyframeSet) keyframeSet);
+ }
+ else {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(property);
+ pvh.mKeyframeSet = keyframeSet;
+ pvh.mValueType = ((Keyframe)values[0]).getType();
+ return pvh;
+ }
+ }
+
+ /**
+ * Set the animated values for this object to this set of ints.
+ * If there is only one value, it is assumed to be the end value of an animation,
+ * and an initial value will be derived, if possible, by calling a getter function
+ * on the object. Also, if any value is null, the value will be filled in when the animation
+ * starts in the same way. This mechanism of automatically getting null values only works
+ * if the PropertyValuesHolder object is used in conjunction
+ * {@link com.nineoldandroids.animation.ObjectAnimator}, and with a getter function
+ * derived automatically from propertyName, since otherwise PropertyValuesHolder has
+ * no way of determining what the value should be.
+ *
+ * @param values One or more values that the animation will animate between.
+ */
+ public void setIntValues(int... values) {
+ mValueType = int.class;
+ mKeyframeSet = KeyframeSet.ofInt(values);
+ }
+
+ /**
+ * Set the animated values for this object to this set of floats.
+ * If there is only one value, it is assumed to be the end value of an animation,
+ * and an initial value will be derived, if possible, by calling a getter function
+ * on the object. Also, if any value is null, the value will be filled in when the animation
+ * starts in the same way. This mechanism of automatically getting null values only works
+ * if the PropertyValuesHolder object is used in conjunction
+ * {@link com.nineoldandroids.animation.ObjectAnimator}, and with a getter function
+ * derived automatically from propertyName, since otherwise PropertyValuesHolder has
+ * no way of determining what the value should be.
+ *
+ * @param values One or more values that the animation will animate between.
+ */
+ public void setFloatValues(float... values) {
+ mValueType = float.class;
+ mKeyframeSet = KeyframeSet.ofFloat(values);
+ }
+
+ /**
+ * Set the animated values for this object to this set of Keyframes.
+ *
+ * @param values One or more values that the animation will animate between.
+ */
+ public void setKeyframes(Keyframe... values) {
+ int numKeyframes = values.length;
+ Keyframe keyframes[] = new Keyframe[Math.max(numKeyframes,2)];
+ mValueType = ((Keyframe)values[0]).getType();
+ for (int i = 0; i < numKeyframes; ++i) {
+ keyframes[i] = (Keyframe)values[i];
+ }
+ mKeyframeSet = new KeyframeSet(keyframes);
+ }
+
+ /**
+ * Set the animated values for this object to this set of Objects.
+ * If there is only one value, it is assumed to be the end value of an animation,
+ * and an initial value will be derived, if possible, by calling a getter function
+ * on the object. Also, if any value is null, the value will be filled in when the animation
+ * starts in the same way. This mechanism of automatically getting null values only works
+ * if the PropertyValuesHolder object is used in conjunction
+ * {@link com.nineoldandroids.animation.ObjectAnimator}, and with a getter function
+ * derived automatically from propertyName, since otherwise PropertyValuesHolder has
+ * no way of determining what the value should be.
+ *
+ * @param values One or more values that the animation will animate between.
+ */
+ public void setObjectValues(Object... values) {
+ mValueType = values[0].getClass();
+ mKeyframeSet = KeyframeSet.ofObject(values);
+ }
+
+ /**
+ * Determine the setter or getter function using the JavaBeans convention of setFoo or
+ * getFoo for a property named 'foo'. This function figures out what the name of the
+ * function should be and uses reflection to find the Method with that name on the
+ * target object.
+ *
+ * @param targetClass The class to search for the method
+ * @param prefix "set" or "get", depending on whether we need a setter or getter.
+ * @param valueType The type of the parameter (in the case of a setter). This type
+ * is derived from the values set on this PropertyValuesHolder. This type is used as
+ * a first guess at the parameter type, but we check for methods with several different
+ * types to avoid problems with slight mis-matches between supplied values and actual
+ * value types used on the setter.
+ * @return Method the method associated with mPropertyName.
+ */
+ private Method getPropertyFunction(Class targetClass, String prefix, Class valueType) {
+ // TODO: faster implementation...
+ Method returnVal = null;
+ String methodName = getMethodName(prefix, mPropertyName);
+ Class args[] = null;
+ if (valueType == null) {
+ try {
+ returnVal = targetClass.getMethod(methodName, args);
+ } catch (NoSuchMethodException e) {
+ /* The native implementation uses JNI to do reflection, which allows access to private methods.
+ * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback.
+ */
+ try {
+ returnVal = targetClass.getDeclaredMethod(methodName, args);
+ returnVal.setAccessible(true);
+ } catch (NoSuchMethodException e2) {
+ Log.e("PropertyValuesHolder",
+ "Couldn't find no-arg method for property " + mPropertyName + ": " + e);
+ }
+ }
+ } else {
+ args = new Class[1];
+ Class typeVariants[];
+ if (mValueType.equals(Float.class)) {
+ typeVariants = FLOAT_VARIANTS;
+ } else if (mValueType.equals(Integer.class)) {
+ typeVariants = INTEGER_VARIANTS;
+ } else if (mValueType.equals(Double.class)) {
+ typeVariants = DOUBLE_VARIANTS;
+ } else {
+ typeVariants = new Class[1];
+ typeVariants[0] = mValueType;
+ }
+ for (Class typeVariant : typeVariants) {
+ args[0] = typeVariant;
+ try {
+ returnVal = targetClass.getMethod(methodName, args);
+ // change the value type to suit
+ mValueType = typeVariant;
+ return returnVal;
+ } catch (NoSuchMethodException e) {
+ /* The native implementation uses JNI to do reflection, which allows access to private methods.
+ * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback.
+ */
+ try {
+ returnVal = targetClass.getDeclaredMethod(methodName, args);
+ returnVal.setAccessible(true);
+ // change the value type to suit
+ mValueType = typeVariant;
+ return returnVal;
+ } catch (NoSuchMethodException e2) {
+ // Swallow the error and keep trying other variants
+ }
+ }
+ }
+ // If we got here, then no appropriate function was found
+ Log.e("PropertyValuesHolder",
+ "Couldn't find setter/getter for property " + mPropertyName +
+ " with value type "+ mValueType);
+ }
+
+ return returnVal;
+ }
+
+
+ /**
+ * Returns the setter or getter requested. This utility function checks whether the
+ * requested method exists in the propertyMapMap cache. If not, it calls another
+ * utility function to request the Method from the targetClass directly.
+ * @param targetClass The Class on which the requested method should exist.
+ * @param propertyMapMap The cache of setters/getters derived so far.
+ * @param prefix "set" or "get", for the setter or getter.
+ * @param valueType The type of parameter passed into the method (null for getter).
+ * @return Method the method associated with mPropertyName.
+ */
+ private Method setupSetterOrGetter(Class targetClass,
+ HashMap> propertyMapMap,
+ String prefix, Class valueType) {
+ Method setterOrGetter = null;
+ try {
+ // Have to lock property map prior to reading it, to guard against
+ // another thread putting something in there after we've checked it
+ // but before we've added an entry to it
+ mPropertyMapLock.writeLock().lock();
+ HashMap propertyMap = propertyMapMap.get(targetClass);
+ if (propertyMap != null) {
+ setterOrGetter = propertyMap.get(mPropertyName);
+ }
+ if (setterOrGetter == null) {
+ setterOrGetter = getPropertyFunction(targetClass, prefix, valueType);
+ if (propertyMap == null) {
+ propertyMap = new HashMap();
+ propertyMapMap.put(targetClass, propertyMap);
+ }
+ propertyMap.put(mPropertyName, setterOrGetter);
+ }
+ } finally {
+ mPropertyMapLock.writeLock().unlock();
+ }
+ return setterOrGetter;
+ }
+
+ /**
+ * Utility function to get the setter from targetClass
+ * @param targetClass The Class on which the requested method should exist.
+ */
+ void setupSetter(Class targetClass) {
+ mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", mValueType);
+ }
+
+ /**
+ * Utility function to get the getter from targetClass
+ */
+ private void setupGetter(Class targetClass) {
+ mGetter = setupSetterOrGetter(targetClass, sGetterPropertyMap, "get", null);
+ }
+
+ /**
+ * Internal function (called from ObjectAnimator) to set up the setter and getter
+ * prior to running the animation. If the setter has not been manually set for this
+ * object, it will be derived automatically given the property name, target object, and
+ * types of values supplied. If no getter has been set, it will be supplied iff any of the
+ * supplied values was null. If there is a null value, then the getter (supplied or derived)
+ * will be called to set those null values to the current value of the property
+ * on the target object.
+ * @param target The object on which the setter (and possibly getter) exist.
+ */
+ void setupSetterAndGetter(Object target) {
+ if (mProperty != null) {
+ // check to make sure that mProperty is on the class of target
+ try {
+ Object testValue = mProperty.get(target);
+ for (Keyframe kf : mKeyframeSet.mKeyframes) {
+ if (!kf.hasValue()) {
+ kf.setValue(mProperty.get(target));
+ }
+ }
+ return;
+ } catch (ClassCastException e) {
+ Log.e("PropertyValuesHolder","No such property (" + mProperty.getName() +
+ ") on target object " + target + ". Trying reflection instead");
+ mProperty = null;
+ }
+ }
+ Class targetClass = target.getClass();
+ if (mSetter == null) {
+ setupSetter(targetClass);
+ }
+ for (Keyframe kf : mKeyframeSet.mKeyframes) {
+ if (!kf.hasValue()) {
+ if (mGetter == null) {
+ setupGetter(targetClass);
+ }
+ try {
+ kf.setValue(mGetter.invoke(target));
+ } catch (InvocationTargetException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ } catch (IllegalAccessException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ }
+ }
+ }
+ }
+
+ /**
+ * Utility function to set the value stored in a particular Keyframe. The value used is
+ * whatever the value is for the property name specified in the keyframe on the target object.
+ *
+ * @param target The target object from which the current value should be extracted.
+ * @param kf The keyframe which holds the property name and value.
+ */
+ private void setupValue(Object target, Keyframe kf) {
+ if (mProperty != null) {
+ kf.setValue(mProperty.get(target));
+ }
+ try {
+ if (mGetter == null) {
+ Class targetClass = target.getClass();
+ setupGetter(targetClass);
+ }
+ kf.setValue(mGetter.invoke(target));
+ } catch (InvocationTargetException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ } catch (IllegalAccessException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ }
+ }
+
+ /**
+ * This function is called by ObjectAnimator when setting the start values for an animation.
+ * The start values are set according to the current values in the target object. The
+ * property whose value is extracted is whatever is specified by the propertyName of this
+ * PropertyValuesHolder object.
+ *
+ * @param target The object which holds the start values that should be set.
+ */
+ void setupStartValue(Object target) {
+ setupValue(target, mKeyframeSet.mKeyframes.get(0));
+ }
+
+ /**
+ * This function is called by ObjectAnimator when setting the end values for an animation.
+ * The end values are set according to the current values in the target object. The
+ * property whose value is extracted is whatever is specified by the propertyName of this
+ * PropertyValuesHolder object.
+ *
+ * @param target The object which holds the start values that should be set.
+ */
+ void setupEndValue(Object target) {
+ setupValue(target, mKeyframeSet.mKeyframes.get(mKeyframeSet.mKeyframes.size() - 1));
+ }
+
+ @Override
+ public PropertyValuesHolder clone() {
+ try {
+ PropertyValuesHolder newPVH = (PropertyValuesHolder) super.clone();
+ newPVH.mPropertyName = mPropertyName;
+ newPVH.mProperty = mProperty;
+ newPVH.mKeyframeSet = mKeyframeSet.clone();
+ newPVH.mEvaluator = mEvaluator;
+ return newPVH;
+ } catch (CloneNotSupportedException e) {
+ // won't reach here
+ return null;
+ }
+ }
+
+ /**
+ * Internal function to set the value on the target object, using the setter set up
+ * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator
+ * to handle turning the value calculated by ValueAnimator into a value set on the object
+ * according to the name of the property.
+ * @param target The target object on which the value is set
+ */
+ void setAnimatedValue(Object target) {
+ if (mProperty != null) {
+ mProperty.set(target, getAnimatedValue());
+ }
+ if (mSetter != null) {
+ try {
+ mTmpValueArray[0] = getAnimatedValue();
+ mSetter.invoke(target, mTmpValueArray);
+ } catch (InvocationTargetException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ } catch (IllegalAccessException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ }
+ }
+ }
+
+ /**
+ * Internal function, called by ValueAnimator, to set up the TypeEvaluator that will be used
+ * to calculate animated values.
+ */
+ void init() {
+ if (mEvaluator == null) {
+ // We already handle int and float automatically, but not their Object
+ // equivalents
+ mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
+ (mValueType == Float.class) ? sFloatEvaluator :
+ null;
+ }
+ if (mEvaluator != null) {
+ // KeyframeSet knows how to evaluate the common types - only give it a custom
+ // evaluator if one has been set on this class
+ mKeyframeSet.setEvaluator(mEvaluator);
+ }
+ }
+
+ /**
+ * The TypeEvaluator will the automatically determined based on the type of values
+ * supplied to PropertyValuesHolder. The evaluator can be manually set, however, if so
+ * desired. This may be important in cases where either the type of the values supplied
+ * do not match the way that they should be interpolated between, or if the values
+ * are of a custom type or one not currently understood by the animation system. Currently,
+ * only values of type float and int (and their Object equivalents: Float
+ * and Integer) are correctly interpolated; all other types require setting a TypeEvaluator.
+ * @param evaluator
+ */
+ public void setEvaluator(TypeEvaluator evaluator) {
+ mEvaluator = evaluator;
+ mKeyframeSet.setEvaluator(evaluator);
+ }
+
+ /**
+ * Function used to calculate the value according to the evaluator set up for
+ * this PropertyValuesHolder object. This function is called by ValueAnimator.animateValue().
+ *
+ * @param fraction The elapsed, interpolated fraction of the animation.
+ */
+ void calculateValue(float fraction) {
+ mAnimatedValue = mKeyframeSet.getValue(fraction);
+ }
+
+ /**
+ * Sets the name of the property that will be animated. This name is used to derive
+ * a setter function that will be called to set animated values.
+ * For example, a property name of foo will result
+ * in a call to the function setFoo() on the target object. If either
+ * valueFrom or valueTo is null, then a getter function will
+ * also be derived and called.
+ *
+ * Note that the setter function derived from this property name
+ * must take the same parameter type as the
+ * valueFrom and valueTo properties, otherwise the call to
+ * the setter function will fail.
+ *
+ * @param propertyName The name of the property being animated.
+ */
+ public void setPropertyName(String propertyName) {
+ mPropertyName = propertyName;
+ }
+
+ /**
+ * Sets the property that will be animated.
+ *
+ * Note that if this PropertyValuesHolder object is used with ObjectAnimator, the property
+ * must exist on the target object specified in that ObjectAnimator.
+ *
+ * @param property The property being animated.
+ */
+ public void setProperty(Property property) {
+ mProperty = property;
+ }
+
+ /**
+ * Gets the name of the property that will be animated. This name will be used to derive
+ * a setter function that will be called to set animated values.
+ * For example, a property name of foo will result
+ * in a call to the function setFoo() on the target object. If either
+ * valueFrom or valueTo is null, then a getter function will
+ * also be derived and called.
+ */
+ public String getPropertyName() {
+ return mPropertyName;
+ }
+
+ /**
+ * Internal function, called by ValueAnimator and ObjectAnimator, to retrieve the value
+ * most recently calculated in calculateValue().
+ * @return
+ */
+ Object getAnimatedValue() {
+ return mAnimatedValue;
+ }
+
+ @Override
+ public String toString() {
+ return mPropertyName + ": " + mKeyframeSet.toString();
+ }
+
+ /**
+ * Utility method to derive a setter/getter method name from a property name, where the
+ * prefix is typically "set" or "get" and the first letter of the property name is
+ * capitalized.
+ *
+ * @param prefix The precursor to the method name, before the property name begins, typically
+ * "set" or "get".
+ * @param propertyName The name of the property that represents the bulk of the method name
+ * after the prefix. The first letter of this word will be capitalized in the resulting
+ * method name.
+ * @return String the property name converted to a method name according to the conventions
+ * specified above.
+ */
+ static String getMethodName(String prefix, String propertyName) {
+ if (propertyName == null || propertyName.length() == 0) {
+ // shouldn't get here
+ return prefix;
+ }
+ char firstLetter = Character.toUpperCase(propertyName.charAt(0));
+ String theRest = propertyName.substring(1);
+ return prefix + firstLetter + theRest;
+ }
+
+ static class IntPropertyValuesHolder extends PropertyValuesHolder {
+
+ // Cache JNI functions to avoid looking them up twice
+ //private static final HashMap> sJNISetterPropertyMap =
+ // new HashMap>();
+ //int mJniSetter;
+ private IntProperty mIntProperty;
+
+ IntKeyframeSet mIntKeyframeSet;
+ int mIntAnimatedValue;
+
+ public IntPropertyValuesHolder(String propertyName, IntKeyframeSet keyframeSet) {
+ super(propertyName);
+ mValueType = int.class;
+ mKeyframeSet = keyframeSet;
+ mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet;
+ }
+
+ public IntPropertyValuesHolder(Property property, IntKeyframeSet keyframeSet) {
+ super(property);
+ mValueType = int.class;
+ mKeyframeSet = keyframeSet;
+ mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet;
+ if (property instanceof IntProperty) {
+ mIntProperty = (IntProperty) mProperty;
+ }
+ }
+
+ public IntPropertyValuesHolder(String propertyName, int... values) {
+ super(propertyName);
+ setIntValues(values);
+ }
+
+ public IntPropertyValuesHolder(Property property, int... values) {
+ super(property);
+ setIntValues(values);
+ if (property instanceof IntProperty) {
+ mIntProperty = (IntProperty) mProperty;
+ }
+ }
+
+ @Override
+ public void setIntValues(int... values) {
+ super.setIntValues(values);
+ mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet;
+ }
+
+ @Override
+ void calculateValue(float fraction) {
+ mIntAnimatedValue = mIntKeyframeSet.getIntValue(fraction);
+ }
+
+ @Override
+ Object getAnimatedValue() {
+ return mIntAnimatedValue;
+ }
+
+ @Override
+ public IntPropertyValuesHolder clone() {
+ IntPropertyValuesHolder newPVH = (IntPropertyValuesHolder) super.clone();
+ newPVH.mIntKeyframeSet = (IntKeyframeSet) newPVH.mKeyframeSet;
+ return newPVH;
+ }
+
+ /**
+ * Internal function to set the value on the target object, using the setter set up
+ * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator
+ * to handle turning the value calculated by ValueAnimator into a value set on the object
+ * according to the name of the property.
+ * @param target The target object on which the value is set
+ */
+ @Override
+ void setAnimatedValue(Object target) {
+ if (mIntProperty != null) {
+ mIntProperty.setValue(target, mIntAnimatedValue);
+ return;
+ }
+ if (mProperty != null) {
+ mProperty.set(target, mIntAnimatedValue);
+ return;
+ }
+ //if (mJniSetter != 0) {
+ // nCallIntMethod(target, mJniSetter, mIntAnimatedValue);
+ // return;
+ //}
+ if (mSetter != null) {
+ try {
+ mTmpValueArray[0] = mIntAnimatedValue;
+ mSetter.invoke(target, mTmpValueArray);
+ } catch (InvocationTargetException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ } catch (IllegalAccessException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ }
+ }
+ }
+
+ @Override
+ void setupSetter(Class targetClass) {
+ if (mProperty != null) {
+ return;
+ }
+ // Check new static hashmap for setter method
+ //try {
+ // mPropertyMapLock.writeLock().lock();
+ // HashMap propertyMap = sJNISetterPropertyMap.get(targetClass);
+ // if (propertyMap != null) {
+ // Integer mJniSetterInteger = propertyMap.get(mPropertyName);
+ // if (mJniSetterInteger != null) {
+ // mJniSetter = mJniSetterInteger;
+ // }
+ // }
+ // if (mJniSetter == 0) {
+ // String methodName = getMethodName("set", mPropertyName);
+ // mJniSetter = nGetIntMethod(targetClass, methodName);
+ // if (mJniSetter != 0) {
+ // if (propertyMap == null) {
+ // propertyMap = new HashMap();
+ // sJNISetterPropertyMap.put(targetClass, propertyMap);
+ // }
+ // propertyMap.put(mPropertyName, mJniSetter);
+ // }
+ // }
+ //} catch (NoSuchMethodError e) {
+ // Log.d("PropertyValuesHolder",
+ // "Can't find native method using JNI, use reflection" + e);
+ //} finally {
+ // mPropertyMapLock.writeLock().unlock();
+ //}
+ //if (mJniSetter == 0) {
+ // Couldn't find method through fast JNI approach - just use reflection
+ super.setupSetter(targetClass);
+ //}
+ }
+ }
+
+ static class FloatPropertyValuesHolder extends PropertyValuesHolder {
+
+ // Cache JNI functions to avoid looking them up twice
+ //private static final HashMap> sJNISetterPropertyMap =
+ // new HashMap>();
+ //int mJniSetter;
+ private FloatProperty mFloatProperty;
+
+ FloatKeyframeSet mFloatKeyframeSet;
+ float mFloatAnimatedValue;
+
+ public FloatPropertyValuesHolder(String propertyName, FloatKeyframeSet keyframeSet) {
+ super(propertyName);
+ mValueType = float.class;
+ mKeyframeSet = keyframeSet;
+ mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet;
+ }
+
+ public FloatPropertyValuesHolder(Property property, FloatKeyframeSet keyframeSet) {
+ super(property);
+ mValueType = float.class;
+ mKeyframeSet = keyframeSet;
+ mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet;
+ if (property instanceof FloatProperty) {
+ mFloatProperty = (FloatProperty) mProperty;
+ }
+ }
+
+ public FloatPropertyValuesHolder(String propertyName, float... values) {
+ super(propertyName);
+ setFloatValues(values);
+ }
+
+ public FloatPropertyValuesHolder(Property property, float... values) {
+ super(property);
+ setFloatValues(values);
+ if (property instanceof FloatProperty) {
+ mFloatProperty = (FloatProperty) mProperty;
+ }
+ }
+
+ @Override
+ public void setFloatValues(float... values) {
+ super.setFloatValues(values);
+ mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet;
+ }
+
+ @Override
+ void calculateValue(float fraction) {
+ mFloatAnimatedValue = mFloatKeyframeSet.getFloatValue(fraction);
+ }
+
+ @Override
+ Object getAnimatedValue() {
+ return mFloatAnimatedValue;
+ }
+
+ @Override
+ public FloatPropertyValuesHolder clone() {
+ FloatPropertyValuesHolder newPVH = (FloatPropertyValuesHolder) super.clone();
+ newPVH.mFloatKeyframeSet = (FloatKeyframeSet) newPVH.mKeyframeSet;
+ return newPVH;
+ }
+
+ /**
+ * Internal function to set the value on the target object, using the setter set up
+ * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator
+ * to handle turning the value calculated by ValueAnimator into a value set on the object
+ * according to the name of the property.
+ * @param target The target object on which the value is set
+ */
+ @Override
+ void setAnimatedValue(Object target) {
+ if (mFloatProperty != null) {
+ mFloatProperty.setValue(target, mFloatAnimatedValue);
+ return;
+ }
+ if (mProperty != null) {
+ mProperty.set(target, mFloatAnimatedValue);
+ return;
+ }
+ //if (mJniSetter != 0) {
+ // nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue);
+ // return;
+ //}
+ if (mSetter != null) {
+ try {
+ mTmpValueArray[0] = mFloatAnimatedValue;
+ mSetter.invoke(target, mTmpValueArray);
+ } catch (InvocationTargetException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ } catch (IllegalAccessException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ }
+ }
+ }
+
+ @Override
+ void setupSetter(Class targetClass) {
+ if (mProperty != null) {
+ return;
+ }
+ // Check new static hashmap for setter method
+ //try {
+ // mPropertyMapLock.writeLock().lock();
+ // HashMap propertyMap = sJNISetterPropertyMap.get(targetClass);
+ // if (propertyMap != null) {
+ // Integer mJniSetterInteger = propertyMap.get(mPropertyName);
+ // if (mJniSetterInteger != null) {
+ // mJniSetter = mJniSetterInteger;
+ // }
+ // }
+ // if (mJniSetter == 0) {
+ // String methodName = getMethodName("set", mPropertyName);
+ // mJniSetter = nGetFloatMethod(targetClass, methodName);
+ // if (mJniSetter != 0) {
+ // if (propertyMap == null) {
+ // propertyMap = new HashMap();
+ // sJNISetterPropertyMap.put(targetClass, propertyMap);
+ // }
+ // propertyMap.put(mPropertyName, mJniSetter);
+ // }
+ // }
+ //} catch (NoSuchMethodError e) {
+ // Log.d("PropertyValuesHolder",
+ // "Can't find native method using JNI, use reflection" + e);
+ //} finally {
+ // mPropertyMapLock.writeLock().unlock();
+ //}
+ //if (mJniSetter == 0) {
+ // Couldn't find method through fast JNI approach - just use reflection
+ super.setupSetter(targetClass);
+ //}
+ }
+
+ }
+
+ //native static private int nGetIntMethod(Class targetClass, String methodName);
+ //native static private int nGetFloatMethod(Class targetClass, String methodName);
+ //native static private void nCallIntMethod(Object target, int methodID, int arg);
+ //native static private void nCallFloatMethod(Object target, int methodID, float arg);
+}
diff --git a/app/src/main/java/com/nineoldandroids/animation/TimeAnimator.java b/app/src/main/java/com/nineoldandroids/animation/TimeAnimator.java
new file mode 100644
index 000000000..7c5006311
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/TimeAnimator.java
@@ -0,0 +1,78 @@
+package com.nineoldandroids.animation;
+
+/**
+ * This class provides a simple callback mechanism to listeners that is synchronized with other
+ * animators in the system. There is no duration, interpolation, or object value-setting
+ * with this Animator. Instead, it is simply started and proceeds to send out events on every
+ * animation frame to its TimeListener (if set), with information about this animator,
+ * the total elapsed time, and the time since the last animation frame.
+ *
+ * @hide
+ */
+public class TimeAnimator extends ValueAnimator {
+
+ private TimeListener mListener;
+ private long mPreviousTime = -1;
+
+ @Override
+ boolean animationFrame(long currentTime) {
+ if (mPlayingState == STOPPED) {
+ mPlayingState = RUNNING;
+ if (mSeekTime < 0) {
+ mStartTime = currentTime;
+ } else {
+ mStartTime = currentTime - mSeekTime;
+ // Now that we're playing, reset the seek time
+ mSeekTime = -1;
+ }
+ }
+ if (mListener != null) {
+ long totalTime = currentTime - mStartTime;
+ long deltaTime = (mPreviousTime < 0) ? 0 : (currentTime - mPreviousTime);
+ mPreviousTime = currentTime;
+ mListener.onTimeUpdate(this, totalTime, deltaTime);
+ }
+ return false;
+ }
+
+ /**
+ * Sets a listener that is sent update events throughout the life of
+ * an animation.
+ *
+ * @param listener the listener to be set.
+ */
+ public void setTimeListener(TimeListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ void animateValue(float fraction) {
+ // Noop
+ }
+
+ @Override
+ void initAnimation() {
+ // noop
+ }
+
+ /**
+ * Implementors of this interface can set themselves as update listeners
+ * to a TimeAnimator instance to receive callbacks on every animation
+ * frame to receive the total time since the animator started and the delta time
+ * since the last frame. The first time the listener is called, totalTime and
+ * deltaTime should both be zero.
+ *
+ * @hide
+ */
+ public static interface TimeListener {
+ /**
+ * Notifies listeners of the occurrence of another frame of the animation,
+ * along with information about the elapsed time.
+ *
+ * @param animation The animator sending out the notification.
+ * @param totalTime The
+ */
+ void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime);
+
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/animation/TypeEvaluator.java b/app/src/main/java/com/nineoldandroids/animation/TypeEvaluator.java
new file mode 100644
index 000000000..7393784ea
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/TypeEvaluator.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+/**
+ * Interface for use with the {@link ValueAnimator#setEvaluator(com.nineoldandroids.animation.TypeEvaluator)} function. Evaluators
+ * allow developers to create animations on arbitrary property types, by allowing them to supply
+ * custom evaulators for types that are not automatically understood and used by the animation
+ * system.
+ *
+ * @see ValueAnimator#setEvaluator(com.nineoldandroids.animation.TypeEvaluator)
+ */
+public interface TypeEvaluator {
+
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * fraction representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0),
+ * where x0 is startValue, x1 is endValue,
+ * and t is fraction.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value.
+ * @param endValue The end value.
+ * @return A linear interpolation between the start and end values, given the
+ * fraction parameter.
+ */
+ public T evaluate(float fraction, T startValue, T endValue);
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nineoldandroids/animation/ValueAnimator.java b/app/src/main/java/com/nineoldandroids/animation/ValueAnimator.java
new file mode 100644
index 000000000..8d47bd3f8
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/animation/ValueAnimator.java
@@ -0,0 +1,1265 @@
+/*
+ * 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 com.nineoldandroids.animation;
+
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.AndroidRuntimeException;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * This class provides a simple timing engine for running animations
+ * which calculate animated values and set them on target objects.
+ *
+ * There is a single timing pulse that all animations use. It runs in a
+ * custom handler to ensure that property changes happen on the UI thread.
+ *
+ * By default, ValueAnimator uses non-linear time interpolation, via the
+ * {@link AccelerateDecelerateInterpolator} class, which accelerates into and decelerates
+ * out of an animation. This behavior can be changed by calling
+ * {@link ValueAnimator#setInterpolator(Interpolator)}.
+ */
+public class ValueAnimator extends Animator {
+
+ /**
+ * Internal constants
+ */
+
+ /*
+ * The default amount of time in ms between animation frames
+ */
+ private static final long DEFAULT_FRAME_DELAY = 10;
+
+ /**
+ * Messages sent to timing handler: START is sent when an animation first begins, FRAME is sent
+ * by the handler to itself to process the next animation frame
+ */
+ static final int ANIMATION_START = 0;
+ static final int ANIMATION_FRAME = 1;
+
+ /**
+ * Values used with internal variable mPlayingState to indicate the current state of an
+ * animation.
+ */
+ static final int STOPPED = 0; // Not yet playing
+ static final int RUNNING = 1; // Playing normally
+ static final int SEEKED = 2; // Seeked to some time value
+
+ /**
+ * Internal variables
+ * NOTE: This object implements the clone() method, making a deep copy of any referenced
+ * objects. As other non-trivial fields are added to this class, make sure to add logic
+ * to clone() to make deep copies of them.
+ */
+
+ // The first time that the animation's animateFrame() method is called. This time is used to
+ // determine elapsed time (and therefore the elapsed fraction) in subsequent calls
+ // to animateFrame()
+ long mStartTime;
+
+ /**
+ * Set when setCurrentPlayTime() is called. If negative, animation is not currently seeked
+ * to a value.
+ */
+ long mSeekTime = -1;
+
+ // TODO: We access the following ThreadLocal variables often, some of them on every update.
+ // If ThreadLocal access is significantly expensive, we may want to put all of these
+ // fields into a structure sot hat we just access ThreadLocal once to get the reference
+ // to that structure, then access the structure directly for each field.
+
+ // The static sAnimationHandler processes the internal timing loop on which all animations
+ // are based
+ protected static ThreadLocal sAnimationHandler =
+ new ThreadLocal();
+
+ // The per-thread list of all active animations
+ protected static final ThreadLocal> sAnimations =
+ new ThreadLocal>() {
+ @Override
+ protected ArrayList initialValue() {
+ return new ArrayList();
+ }
+ };
+
+ // The per-thread set of animations to be started on the next animation frame
+ protected static final ThreadLocal> sPendingAnimations =
+ new ThreadLocal>() {
+ @Override
+ protected ArrayList initialValue() {
+ return new ArrayList();
+ }
+ };
+
+ /**
+ * Internal per-thread collections used to avoid set collisions as animations start and end
+ * while being processed.
+ */
+ protected static final ThreadLocal> sDelayedAnims =
+ new ThreadLocal>() {
+ @Override
+ protected ArrayList initialValue() {
+ return new ArrayList();
+ }
+ };
+
+ protected static final ThreadLocal> sEndingAnims =
+ new ThreadLocal>() {
+ @Override
+ protected ArrayList initialValue() {
+ return new ArrayList();
+ }
+ };
+
+ protected static final ThreadLocal> sReadyAnims =
+ new ThreadLocal>() {
+ @Override
+ protected ArrayList initialValue() {
+ return new ArrayList();
+ }
+ };
+
+ // The time interpolator to be used if none is set on the animation
+ private static final android.view.animation.Interpolator sDefaultInterpolator =
+ new AccelerateDecelerateInterpolator();
+
+ // type evaluators for the primitive types handled by this implementation
+ private static final TypeEvaluator sIntEvaluator = new IntEvaluator();
+ private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator();
+
+ /**
+ * Used to indicate whether the animation is currently playing in reverse. This causes the
+ * elapsed fraction to be inverted to calculate the appropriate values.
+ */
+ private boolean mPlayingBackwards = false;
+
+ /**
+ * This variable tracks the current iteration that is playing. When mCurrentIteration exceeds the
+ * repeatCount (if repeatCount!=INFINITE), the animation ends
+ */
+ private int mCurrentIteration = 0;
+
+ /**
+ * Tracks current elapsed/eased fraction, for querying in getAnimatedFraction().
+ */
+ private float mCurrentFraction = 0f;
+
+ /**
+ * Tracks whether a startDelay'd animation has begun playing through the startDelay.
+ */
+ private boolean mStartedDelay = false;
+
+ /**
+ * Tracks the time at which the animation began playing through its startDelay. This is
+ * different from the mStartTime variable, which is used to track when the animation became
+ * active (which is when the startDelay expired and the animation was added to the active
+ * animations list).
+ */
+ private long mDelayStartTime;
+
+ /**
+ * Flag that represents the current state of the animation. Used to figure out when to start
+ * an animation (if state == STOPPED). Also used to end an animation that
+ * has been cancel()'d or end()'d since the last animation frame. Possible values are
+ * STOPPED, RUNNING, SEEKED.
+ */
+ int mPlayingState = STOPPED;
+
+ /**
+ * Additional playing state to indicate whether an animator has been start()'d. There is
+ * some lag between a call to start() and the first animation frame. We should still note
+ * that the animation has been started, even if it's first animation frame has not yet
+ * happened, and reflect that state in isRunning().
+ * Note that delayed animations are different: they are not started until their first
+ * animation frame, which occurs after their delay elapses.
+ */
+ private boolean mRunning = false;
+
+ /**
+ * Additional playing state to indicate whether an animator has been start()'d, whether or
+ * not there is a nonzero startDelay.
+ */
+ private boolean mStarted = false;
+
+ /**
+ * Flag that denotes whether the animation is set up and ready to go. Used to
+ * set up animation that has not yet been started.
+ */
+ boolean mInitialized = false;
+
+ //
+ // Backing variables
+ //
+
+ // How long the animation should last in ms
+ private long mDuration = 300;
+
+ // The amount of time in ms to delay starting the animation after start() is called
+ private long mStartDelay = 0;
+
+ // The number of milliseconds between animation frames
+ private static long sFrameDelay = DEFAULT_FRAME_DELAY;
+
+ // The number of times the animation will repeat. The default is 0, which means the animation
+ // will play only once
+ private int mRepeatCount = 0;
+
+ /**
+ * The type of repetition that will occur when repeatMode is nonzero. RESTART means the
+ * animation will start from the beginning on every new cycle. REVERSE means the animation
+ * will reverse directions on each iteration.
+ */
+ private int mRepeatMode = RESTART;
+
+ /**
+ * The time interpolator to be used. The elapsed fraction of the animation will be passed
+ * through this interpolator to calculate the interpolated fraction, which is then used to
+ * calculate the animated values.
+ */
+ private Interpolator mInterpolator = sDefaultInterpolator;
+
+ /**
+ * The set of listeners to be sent events through the life of an animation.
+ */
+ private ArrayList mUpdateListeners = null;
+
+ /**
+ * The property/value sets being animated.
+ */
+ PropertyValuesHolder[] mValues;
+
+ /**
+ * A hashmap of the PropertyValuesHolder objects. This map is used to lookup animated values
+ * by property name during calls to getAnimatedValue(String).
+ */
+ HashMap mValuesMap;
+
+ /**
+ * Public constants
+ */
+
+ /**
+ * When the animation reaches the end and repeatCount is INFINITE
+ * or a positive value, the animation restarts from the beginning.
+ */
+ public static final int RESTART = 1;
+ /**
+ * When the animation reaches the end and repeatCount is INFINITE
+ * or a positive value, the animation reverses direction on every iteration.
+ */
+ public static final int REVERSE = 2;
+ /**
+ * This value used used with the {@link #setRepeatCount(int)} property to repeat
+ * the animation indefinitely.
+ */
+ public static final int INFINITE = -1;
+
+ /**
+ * Creates a new ValueAnimator object. This default constructor is primarily for
+ * use internally; the factory methods which take parameters are more generally
+ * useful.
+ */
+ public ValueAnimator() {
+ }
+
+ /**
+ * Constructs and returns a ValueAnimator that animates between int values. A single
+ * value implies that that value is the one being animated to. However, this is not typically
+ * useful in a ValueAnimator object because there is no way for the object to determine the
+ * starting value for the animation (unlike ObjectAnimator, which can derive that value
+ * from the target object and property being animated). Therefore, there should typically
+ * be two or more values.
+ *
+ * @param values A set of values that the animation will animate between over time.
+ * @return A ValueAnimator object that is set up to animate between the given values.
+ */
+ public static ValueAnimator ofInt(int... values) {
+ ValueAnimator anim = new ValueAnimator();
+ anim.setIntValues(values);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns a ValueAnimator that animates between float values. A single
+ * value implies that that value is the one being animated to. However, this is not typically
+ * useful in a ValueAnimator object because there is no way for the object to determine the
+ * starting value for the animation (unlike ObjectAnimator, which can derive that value
+ * from the target object and property being animated). Therefore, there should typically
+ * be two or more values.
+ *
+ * @param values A set of values that the animation will animate between over time.
+ * @return A ValueAnimator object that is set up to animate between the given values.
+ */
+ public static ValueAnimator ofFloat(float... values) {
+ ValueAnimator anim = new ValueAnimator();
+ anim.setFloatValues(values);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns a ValueAnimator that animates between the values
+ * specified in the PropertyValuesHolder objects.
+ *
+ * @param values A set of PropertyValuesHolder objects whose values will be animated
+ * between over time.
+ * @return A ValueAnimator object that is set up to animate between the given values.
+ */
+ public static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder... values) {
+ ValueAnimator anim = new ValueAnimator();
+ anim.setValues(values);
+ return anim;
+ }
+ /**
+ * Constructs and returns a ValueAnimator that animates between Object values. A single
+ * value implies that that value is the one being animated to. However, this is not typically
+ * useful in a ValueAnimator object because there is no way for the object to determine the
+ * starting value for the animation (unlike ObjectAnimator, which can derive that value
+ * from the target object and property being animated). Therefore, there should typically
+ * be two or more values.
+ *
+ * Since ValueAnimator does not know how to animate between arbitrary Objects, this
+ * factory method also takes a TypeEvaluator object that the ValueAnimator will use
+ * to perform that interpolation.
+ *
+ * @param evaluator A TypeEvaluator that will be called on each animation frame to
+ * provide the ncessry interpolation between the Object values to derive the animated
+ * value.
+ * @param values A set of values that the animation will animate between over time.
+ * @return A ValueAnimator object that is set up to animate between the given values.
+ */
+ public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values) {
+ ValueAnimator anim = new ValueAnimator();
+ anim.setObjectValues(values);
+ anim.setEvaluator(evaluator);
+ return anim;
+ }
+
+ /**
+ * Sets int values that will be animated between. A single
+ * value implies that that value is the one being animated to. However, this is not typically
+ * useful in a ValueAnimator object because there is no way for the object to determine the
+ * starting value for the animation (unlike ObjectAnimator, which can derive that value
+ * from the target object and property being animated). Therefore, there should typically
+ * be two or more values.
+ *
+ *
If there are already multiple sets of values defined for this ValueAnimator via more
+ * than one PropertyValuesHolder object, this method will set the values for the first
+ * of those objects.
+ *
+ * @param values A set of values that the animation will animate between over time.
+ */
+ public void setIntValues(int... values) {
+ if (values == null || values.length == 0) {
+ return;
+ }
+ if (mValues == null || mValues.length == 0) {
+ setValues(new PropertyValuesHolder[]{PropertyValuesHolder.ofInt("", values)});
+ } else {
+ PropertyValuesHolder valuesHolder = mValues[0];
+ valuesHolder.setIntValues(values);
+ }
+ // New property/values/target should cause re-initialization prior to starting
+ mInitialized = false;
+ }
+
+ /**
+ * Sets float values that will be animated between. A single
+ * value implies that that value is the one being animated to. However, this is not typically
+ * useful in a ValueAnimator object because there is no way for the object to determine the
+ * starting value for the animation (unlike ObjectAnimator, which can derive that value
+ * from the target object and property being animated). Therefore, there should typically
+ * be two or more values.
+ *
+ * If there are already multiple sets of values defined for this ValueAnimator via more
+ * than one PropertyValuesHolder object, this method will set the values for the first
+ * of those objects.
+ *
+ * @param values A set of values that the animation will animate between over time.
+ */
+ public void setFloatValues(float... values) {
+ if (values == null || values.length == 0) {
+ return;
+ }
+ if (mValues == null || mValues.length == 0) {
+ setValues(new PropertyValuesHolder[]{PropertyValuesHolder.ofFloat("", values)});
+ } else {
+ PropertyValuesHolder valuesHolder = mValues[0];
+ valuesHolder.setFloatValues(values);
+ }
+ // New property/values/target should cause re-initialization prior to starting
+ mInitialized = false;
+ }
+
+ /**
+ * Sets the values to animate between for this animation. A single
+ * value implies that that value is the one being animated to. However, this is not typically
+ * useful in a ValueAnimator object because there is no way for the object to determine the
+ * starting value for the animation (unlike ObjectAnimator, which can derive that value
+ * from the target object and property being animated). Therefore, there should typically
+ * be two or more values.
+ *
+ * If there are already multiple sets of values defined for this ValueAnimator via more
+ * than one PropertyValuesHolder object, this method will set the values for the first
+ * of those objects.
+ *
+ * There should be a TypeEvaluator set on the ValueAnimator that knows how to interpolate
+ * between these value objects. ValueAnimator only knows how to interpolate between the
+ * primitive types specified in the other setValues() methods.
+ *
+ * @param values The set of values to animate between.
+ */
+ public void setObjectValues(Object... values) {
+ if (values == null || values.length == 0) {
+ return;
+ }
+ if (mValues == null || mValues.length == 0) {
+ setValues(new PropertyValuesHolder[]{PropertyValuesHolder.ofObject("",
+ (TypeEvaluator)null, values)});
+ } else {
+ PropertyValuesHolder valuesHolder = mValues[0];
+ valuesHolder.setObjectValues(values);
+ }
+ // New property/values/target should cause re-initialization prior to starting
+ mInitialized = false;
+ }
+
+ /**
+ * Sets the values, per property, being animated between. This function is called internally
+ * by the constructors of ValueAnimator that take a list of values. But an ValueAnimator can
+ * be constructed without values and this method can be called to set the values manually
+ * instead.
+ *
+ * @param values The set of values, per property, being animated between.
+ */
+ public void setValues(PropertyValuesHolder... values) {
+ int numValues = values.length;
+ mValues = values;
+ mValuesMap = new HashMap(numValues);
+ for (int i = 0; i < numValues; ++i) {
+ PropertyValuesHolder valuesHolder = (PropertyValuesHolder) values[i];
+ mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
+ }
+ // New property/values/target should cause re-initialization prior to starting
+ mInitialized = false;
+ }
+
+ /**
+ * Returns the values that this ValueAnimator animates between. These values are stored in
+ * PropertyValuesHolder objects, even if the ValueAnimator was created with a simple list
+ * of value objects instead.
+ *
+ * @return PropertyValuesHolder[] An array of PropertyValuesHolder objects which hold the
+ * values, per property, that define the animation.
+ */
+ public PropertyValuesHolder[] getValues() {
+ return mValues;
+ }
+
+ /**
+ * This function is called immediately before processing the first animation
+ * frame of an animation. If there is a nonzero startDelay, the
+ * function is called after that delay ends.
+ * It takes care of the final initialization steps for the
+ * animation.
+ *
+ * Overrides of this method should call the superclass method to ensure
+ * that internal mechanisms for the animation are set up correctly.
+ */
+ void initAnimation() {
+ if (!mInitialized) {
+ int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].init();
+ }
+ mInitialized = true;
+ }
+ }
+
+
+ /**
+ * Sets the length of the animation. The default duration is 300 milliseconds.
+ *
+ * @param duration The length of the animation, in milliseconds. This value cannot
+ * be negative.
+ * @return ValueAnimator The object called with setDuration(). This return
+ * value makes it easier to compose statements together that construct and then set the
+ * duration, as in ValueAnimator.ofInt(0, 10).setDuration(500).start().
+ */
+ public ValueAnimator setDuration(long duration) {
+ if (duration < 0) {
+ throw new IllegalArgumentException("Animators cannot have negative duration: " +
+ duration);
+ }
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Gets the length of the animation. The default duration is 300 milliseconds.
+ *
+ * @return The length of the animation, in milliseconds.
+ */
+ public long getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Sets the position of the animation to the specified point in time. This time should
+ * be between 0 and the total duration of the animation, including any repetition. If
+ * the animation has not yet been started, then it will not advance forward after it is
+ * set to this time; it will simply set the time to this value and perform any appropriate
+ * actions based on that time. If the animation is already running, then setCurrentPlayTime()
+ * will set the current playing time to this value and continue playing from that point.
+ *
+ * @param playTime The time, in milliseconds, to which the animation is advanced or rewound.
+ */
+ public void setCurrentPlayTime(long playTime) {
+ initAnimation();
+ long currentTime = AnimationUtils.currentAnimationTimeMillis();
+ if (mPlayingState != RUNNING) {
+ mSeekTime = playTime;
+ mPlayingState = SEEKED;
+ }
+ mStartTime = currentTime - playTime;
+ animationFrame(currentTime);
+ }
+
+ /**
+ * Gets the current position of the animation in time, which is equal to the current
+ * time minus the time that the animation started. An animation that is not yet started will
+ * return a value of zero.
+ *
+ * @return The current position in time of the animation.
+ */
+ public long getCurrentPlayTime() {
+ if (!mInitialized || mPlayingState == STOPPED) {
+ return 0;
+ }
+ return AnimationUtils.currentAnimationTimeMillis() - mStartTime;
+ }
+
+ /**
+ * This custom, static handler handles the timing pulse that is shared by
+ * all active animations. This approach ensures that the setting of animation
+ * values will happen on the UI thread and that all animations will share
+ * the same times for calculating their values, which makes synchronizing
+ * animations possible.
+ *
+ */
+ protected static class AnimationHandler extends Handler {
+ /**
+ * There are only two messages that we care about: ANIMATION_START and
+ * ANIMATION_FRAME. The START message is sent when an animation's start()
+ * method is called. It cannot start synchronously when start() is called
+ * because the call may be on the wrong thread, and it would also not be
+ * synchronized with other animations because it would not start on a common
+ * timing pulse. So each animation sends a START message to the handler, which
+ * causes the handler to place the animation on the active animations queue and
+ * start processing frames for that animation.
+ * The FRAME message is the one that is sent over and over while there are any
+ * active animations to process.
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ boolean callAgain = true;
+ ArrayList animations = sAnimations.get();
+ ArrayList delayedAnims = sDelayedAnims.get();
+ switch (msg.what) {
+ // TODO: should we avoid sending frame message when starting if we
+ // were already running?
+ case ANIMATION_START:
+ ArrayList pendingAnimations = sPendingAnimations.get();
+ if (animations.size() > 0 || delayedAnims.size() > 0) {
+ callAgain = false;
+ }
+ // pendingAnims holds any animations that have requested to be started
+ // We're going to clear sPendingAnimations, but starting animation may
+ // cause more to be added to the pending list (for example, if one animation
+ // starting triggers another starting). So we loop until sPendingAnimations
+ // is empty.
+ while (pendingAnimations.size() > 0) {
+ ArrayList pendingCopy =
+ (ArrayList) pendingAnimations.clone();
+ pendingAnimations.clear();
+ int count = pendingCopy.size();
+ for (int i = 0; i < count; ++i) {
+ ValueAnimator anim = pendingCopy.get(i);
+ // If the animation has a startDelay, place it on the delayed list
+ if (anim.mStartDelay == 0) {
+ anim.startAnimation();
+ } else {
+ delayedAnims.add(anim);
+ }
+ }
+ }
+ // fall through to process first frame of new animations
+ case ANIMATION_FRAME:
+ // currentTime holds the common time for all animations processed
+ // during this frame
+ long currentTime = AnimationUtils.currentAnimationTimeMillis();
+ ArrayList readyAnims = sReadyAnims.get();
+ ArrayList endingAnims = sEndingAnims.get();
+
+ // First, process animations currently sitting on the delayed queue, adding
+ // them to the active animations if they are ready
+ int numDelayedAnims = delayedAnims.size();
+ for (int i = 0; i < numDelayedAnims; ++i) {
+ ValueAnimator anim = delayedAnims.get(i);
+ if (anim.delayedAnimationFrame(currentTime)) {
+ readyAnims.add(anim);
+ }
+ }
+ int numReadyAnims = readyAnims.size();
+ if (numReadyAnims > 0) {
+ for (int i = 0; i < numReadyAnims; ++i) {
+ ValueAnimator anim = readyAnims.get(i);
+ anim.startAnimation();
+ anim.mRunning = true;
+ delayedAnims.remove(anim);
+ }
+ readyAnims.clear();
+ }
+
+ // Now process all active animations. The return value from animationFrame()
+ // tells the handler whether it should now be ended
+ int numAnims = animations.size();
+ int i = 0;
+ while (i < numAnims) {
+ ValueAnimator anim = animations.get(i);
+ if (anim.animationFrame(currentTime)) {
+ endingAnims.add(anim);
+ }
+ if (animations.size() == numAnims) {
+ ++i;
+ } else {
+ // An animation might be canceled or ended by client code
+ // during the animation frame. Check to see if this happened by
+ // seeing whether the current index is the same as it was before
+ // calling animationFrame(). Another approach would be to copy
+ // animations to a temporary list and process that list instead,
+ // but that entails garbage and processing overhead that would
+ // be nice to avoid.
+ --numAnims;
+ endingAnims.remove(anim);
+ }
+ }
+ if (endingAnims.size() > 0) {
+ for (i = 0; i < endingAnims.size(); ++i) {
+ endingAnims.get(i).endAnimation();
+ }
+ endingAnims.clear();
+ }
+
+ // If there are still active or delayed animations, call the handler again
+ // after the frameDelay
+ if (callAgain && (!animations.isEmpty() || !delayedAnims.isEmpty())) {
+ sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay -
+ (AnimationUtils.currentAnimationTimeMillis() - currentTime)));
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * The amount of time, in milliseconds, to delay starting the animation after
+ * {@link #start()} is called.
+ *
+ * @return the number of milliseconds to delay running the animation
+ */
+ public long getStartDelay() {
+ return mStartDelay;
+ }
+
+ /**
+ * The amount of time, in milliseconds, to delay starting the animation after
+ * {@link #start()} is called.
+
+ * @param startDelay The amount of the delay, in milliseconds
+ */
+ public void setStartDelay(long startDelay) {
+ this.mStartDelay = startDelay;
+ }
+
+ /**
+ * The amount of time, in milliseconds, between each frame of the animation. This is a
+ * requested time that the animation will attempt to honor, but the actual delay between
+ * frames may be different, depending on system load and capabilities. This is a static
+ * function because the same delay will be applied to all animations, since they are all
+ * run off of a single timing loop.
+ *
+ * @return the requested time between frames, in milliseconds
+ */
+ public static long getFrameDelay() {
+ return sFrameDelay;
+ }
+
+ /**
+ * The amount of time, in milliseconds, between each frame of the animation. This is a
+ * requested time that the animation will attempt to honor, but the actual delay between
+ * frames may be different, depending on system load and capabilities. This is a static
+ * function because the same delay will be applied to all animations, since they are all
+ * run off of a single timing loop.
+ *
+ * @param frameDelay the requested time between frames, in milliseconds
+ */
+ public static void setFrameDelay(long frameDelay) {
+ sFrameDelay = frameDelay;
+ }
+
+ /**
+ * The most recent value calculated by this ValueAnimator when there is just one
+ * property being animated. This value is only sensible while the animation is running. The main
+ * purpose for this read-only property is to retrieve the value from the ValueAnimator
+ * during a call to {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)}, which
+ * is called during each animation frame, immediately after the value is calculated.
+ *
+ * @return animatedValue The value most recently calculated by this ValueAnimator for
+ * the single property being animated. If there are several properties being animated
+ * (specified by several PropertyValuesHolder objects in the constructor), this function
+ * returns the animated value for the first of those objects.
+ */
+ public Object getAnimatedValue() {
+ if (mValues != null && mValues.length > 0) {
+ return mValues[0].getAnimatedValue();
+ }
+ // Shouldn't get here; should always have values unless ValueAnimator was set up wrong
+ return null;
+ }
+
+ /**
+ * The most recent value calculated by this ValueAnimator for propertyName.
+ * The main purpose for this read-only property is to retrieve the value from the
+ * ValueAnimator during a call to
+ * {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)}, which
+ * is called during each animation frame, immediately after the value is calculated.
+ *
+ * @return animatedValue The value most recently calculated for the named property
+ * by this ValueAnimator.
+ */
+ public Object getAnimatedValue(String propertyName) {
+ PropertyValuesHolder valuesHolder = mValuesMap.get(propertyName);
+ if (valuesHolder != null) {
+ return valuesHolder.getAnimatedValue();
+ } else {
+ // At least avoid crashing if called with bogus propertyName
+ return null;
+ }
+ }
+
+ /**
+ * Sets how many times the animation should be repeated. If the repeat
+ * count is 0, the animation is never repeated. If the repeat count is
+ * greater than 0 or {@link #INFINITE}, the repeat mode will be taken
+ * into account. The repeat count is 0 by default.
+ *
+ * @param value the number of times the animation should be repeated
+ */
+ public void setRepeatCount(int value) {
+ mRepeatCount = value;
+ }
+ /**
+ * Defines how many times the animation should repeat. The default value
+ * is 0.
+ *
+ * @return the number of times the animation should repeat, or {@link #INFINITE}
+ */
+ public int getRepeatCount() {
+ return mRepeatCount;
+ }
+
+ /**
+ * Defines what this animation should do when it reaches the end. This
+ * setting is applied only when the repeat count is either greater than
+ * 0 or {@link #INFINITE}. Defaults to {@link #RESTART}.
+ *
+ * @param value {@link #RESTART} or {@link #REVERSE}
+ */
+ public void setRepeatMode(int value) {
+ mRepeatMode = value;
+ }
+
+ /**
+ * Defines what this animation should do when it reaches the end.
+ *
+ * @return either one of {@link #REVERSE} or {@link #RESTART}
+ */
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ /**
+ * Adds a listener to the set of listeners that are sent update events through the life of
+ * an animation. This method is called on all listeners for every frame of the animation,
+ * after the values for the animation have been calculated.
+ *
+ * @param listener the listener to be added to the current set of listeners for this animation.
+ */
+ public void addUpdateListener(AnimatorUpdateListener listener) {
+ if (mUpdateListeners == null) {
+ mUpdateListeners = new ArrayList();
+ }
+ mUpdateListeners.add(listener);
+ }
+
+ /**
+ * Removes all listeners from the set listening to frame updates for this animation.
+ */
+ public void removeAllUpdateListeners() {
+ if (mUpdateListeners == null) {
+ return;
+ }
+ mUpdateListeners.clear();
+ mUpdateListeners = null;
+ }
+
+ /**
+ * Removes a listener from the set listening to frame updates for this animation.
+ *
+ * @param listener the listener to be removed from the current set of update listeners
+ * for this animation.
+ */
+ public void removeUpdateListener(AnimatorUpdateListener listener) {
+ if (mUpdateListeners == null) {
+ return;
+ }
+ mUpdateListeners.remove(listener);
+ if (mUpdateListeners.size() == 0) {
+ mUpdateListeners = null;
+ }
+ }
+
+
+ /**
+ * The time interpolator used in calculating the elapsed fraction of this animation. The
+ * interpolator determines whether the animation runs with linear or non-linear motion,
+ * such as acceleration and deceleration. The default value is
+ * {@link android.view.animation.AccelerateDecelerateInterpolator}
+ *
+ * @param value the interpolator to be used by this animation. A value of null
+ * will result in linear interpolation.
+ */
+ @Override
+ public void setInterpolator(Interpolator value) {
+ if (value != null) {
+ mInterpolator = value;
+ } else {
+ mInterpolator = new LinearInterpolator();
+ }
+ }
+
+ /**
+ * Returns the timing interpolator that this ValueAnimator uses.
+ *
+ * @return The timing interpolator for this ValueAnimator.
+ */
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * The type evaluator to be used when calculating the animated values of this animation.
+ * The system will automatically assign a float or int evaluator based on the type
+ * of startValue and endValue in the constructor. But if these values
+ * are not one of these primitive types, or if different evaluation is desired (such as is
+ * necessary with int values that represent colors), a custom evaluator needs to be assigned.
+ * For example, when running an animation on color values, the {@link ArgbEvaluator}
+ * should be used to get correct RGB color interpolation.
+ *
+ * If this ValueAnimator has only one set of values being animated between, this evaluator
+ * will be used for that set. If there are several sets of values being animated, which is
+ * the case if PropertyValuesHOlder objects were set on the ValueAnimator, then the evaluator
+ * is assigned just to the first PropertyValuesHolder object.
+ *
+ * @param value the evaluator to be used this animation
+ */
+ public void setEvaluator(TypeEvaluator value) {
+ if (value != null && mValues != null && mValues.length > 0) {
+ mValues[0].setEvaluator(value);
+ }
+ }
+
+ /**
+ * Start the animation playing. This version of start() takes a boolean flag that indicates
+ * whether the animation should play in reverse. The flag is usually false, but may be set
+ * to true if called from the reverse() method.
+ *
+ * The animation started by calling this method will be run on the thread that called
+ * this method. This thread should have a Looper on it (a runtime exception will be thrown if
+ * this is not the case). Also, if the animation will animate
+ * properties of objects in the view hierarchy, then the calling thread should be the UI
+ * thread for that view hierarchy.
+ *
+ * @param playBackwards Whether the ValueAnimator should start playing in reverse.
+ */
+ private void start(boolean playBackwards) {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
+ mPlayingBackwards = playBackwards;
+ mCurrentIteration = 0;
+ mPlayingState = STOPPED;
+ mStarted = true;
+ mStartedDelay = false;
+ sPendingAnimations.get().add(this);
+ if (mStartDelay == 0) {
+ // This sets the initial value of the animation, prior to actually starting it running
+ setCurrentPlayTime(getCurrentPlayTime());
+ mPlayingState = STOPPED;
+ mRunning = true;
+
+ if (mListeners != null) {
+ ArrayList tmpListeners =
+ (ArrayList) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationStart(this);
+ }
+ }
+ }
+ AnimationHandler animationHandler = sAnimationHandler.get();
+ if (animationHandler == null) {
+ animationHandler = new AnimationHandler();
+ sAnimationHandler.set(animationHandler);
+ }
+ animationHandler.sendEmptyMessage(ANIMATION_START);
+ }
+
+ @Override
+ public void start() {
+ start(false);
+ }
+
+ @Override
+ public void cancel() {
+ // Only cancel if the animation is actually running or has been started and is about
+ // to run
+ if (mPlayingState != STOPPED || sPendingAnimations.get().contains(this) ||
+ sDelayedAnims.get().contains(this)) {
+ // Only notify listeners if the animator has actually started
+ if (mRunning && mListeners != null) {
+ ArrayList tmpListeners =
+ (ArrayList) mListeners.clone();
+ for (AnimatorListener listener : tmpListeners) {
+ listener.onAnimationCancel(this);
+ }
+ }
+ endAnimation();
+ }
+ }
+
+ @Override
+ public void end() {
+ if (!sAnimations.get().contains(this) && !sPendingAnimations.get().contains(this)) {
+ // Special case if the animation has not yet started; get it ready for ending
+ mStartedDelay = false;
+ startAnimation();
+ } else if (!mInitialized) {
+ initAnimation();
+ }
+ // The final value set on the target varies, depending on whether the animation
+ // was supposed to repeat an odd number of times
+ if (mRepeatCount > 0 && (mRepeatCount & 0x01) == 1) {
+ animateValue(0f);
+ } else {
+ animateValue(1f);
+ }
+ endAnimation();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return (mPlayingState == RUNNING || mRunning);
+ }
+
+ @Override
+ public boolean isStarted() {
+ return mStarted;
+ }
+
+ /**
+ * Plays the ValueAnimator in reverse. If the animation is already running,
+ * it will stop itself and play backwards from the point reached when reverse was called.
+ * If the animation is not currently running, then it will start from the end and
+ * play backwards. This behavior is only set for the current animation; future playing
+ * of the animation will use the default behavior of playing forward.
+ */
+ public void reverse() {
+ mPlayingBackwards = !mPlayingBackwards;
+ if (mPlayingState == RUNNING) {
+ long currentTime = AnimationUtils.currentAnimationTimeMillis();
+ long currentPlayTime = currentTime - mStartTime;
+ long timeLeft = mDuration - currentPlayTime;
+ mStartTime = currentTime - timeLeft;
+ } else {
+ start(true);
+ }
+ }
+
+ /**
+ * Called internally to end an animation by removing it from the animations list. Must be
+ * called on the UI thread.
+ */
+ private void endAnimation() {
+ sAnimations.get().remove(this);
+ sPendingAnimations.get().remove(this);
+ sDelayedAnims.get().remove(this);
+ mPlayingState = STOPPED;
+ if (mRunning && mListeners != null) {
+ ArrayList tmpListeners =
+ (ArrayList) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationEnd(this);
+ }
+ }
+ mRunning = false;
+ mStarted = false;
+ }
+
+ /**
+ * Called internally to start an animation by adding it to the active animations list. Must be
+ * called on the UI thread.
+ */
+ private void startAnimation() {
+ initAnimation();
+ sAnimations.get().add(this);
+ if (mStartDelay > 0 && mListeners != null) {
+ // Listeners were already notified in start() if startDelay is 0; this is
+ // just for delayed animations
+ ArrayList tmpListeners =
+ (ArrayList) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationStart(this);
+ }
+ }
+ }
+
+ /**
+ * Internal function called to process an animation frame on an animation that is currently
+ * sleeping through its startDelay phase. The return value indicates whether it
+ * should be woken up and put on the active animations queue.
+ *
+ * @param currentTime The current animation time, used to calculate whether the animation
+ * has exceeded its startDelay and should be started.
+ * @return True if the animation's startDelay has been exceeded and the animation
+ * should be added to the set of active animations.
+ */
+ private boolean delayedAnimationFrame(long currentTime) {
+ if (!mStartedDelay) {
+ mStartedDelay = true;
+ mDelayStartTime = currentTime;
+ } else {
+ long deltaTime = currentTime - mDelayStartTime;
+ if (deltaTime > mStartDelay) {
+ // startDelay ended - start the anim and record the
+ // mStartTime appropriately
+ mStartTime = currentTime - (deltaTime - mStartDelay);
+ mPlayingState = RUNNING;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This internal function processes a single animation frame for a given animation. The
+ * currentTime parameter is the timing pulse sent by the handler, used to calculate the
+ * elapsed duration, and therefore
+ * the elapsed fraction, of the animation. The return value indicates whether the animation
+ * should be ended (which happens when the elapsed time of the animation exceeds the
+ * animation's duration, including the repeatCount).
+ *
+ * @param currentTime The current time, as tracked by the static timing handler
+ * @return true if the animation's duration, including any repetitions due to
+ * repeatCount has been exceeded and the animation should be ended.
+ */
+ boolean animationFrame(long currentTime) {
+ boolean done = false;
+
+ if (mPlayingState == STOPPED) {
+ mPlayingState = RUNNING;
+ if (mSeekTime < 0) {
+ mStartTime = currentTime;
+ } else {
+ mStartTime = currentTime - mSeekTime;
+ // Now that we're playing, reset the seek time
+ mSeekTime = -1;
+ }
+ }
+ switch (mPlayingState) {
+ case RUNNING:
+ case SEEKED:
+ float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;
+ if (fraction >= 1f) {
+ if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) {
+ // Time to repeat
+ if (mListeners != null) {
+ int numListeners = mListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ mListeners.get(i).onAnimationRepeat(this);
+ }
+ }
+ if (mRepeatMode == REVERSE) {
+ mPlayingBackwards = mPlayingBackwards ? false : true;
+ }
+ mCurrentIteration += (int)fraction;
+ fraction = fraction % 1f;
+ mStartTime += mDuration;
+ } else {
+ done = true;
+ fraction = Math.min(fraction, 1.0f);
+ }
+ }
+ if (mPlayingBackwards) {
+ fraction = 1f - fraction;
+ }
+ animateValue(fraction);
+ break;
+ }
+
+ return done;
+ }
+
+ /**
+ * Returns the current animation fraction, which is the elapsed/interpolated fraction used in
+ * the most recent frame update on the animation.
+ *
+ * @return Elapsed/interpolated fraction of the animation.
+ */
+ public float getAnimatedFraction() {
+ return mCurrentFraction;
+ }
+
+ /**
+ * This method is called with the elapsed fraction of the animation during every
+ * animation frame. This function turns the elapsed fraction into an interpolated fraction
+ * and then into an animated value (from the evaluator. The function is called mostly during
+ * animation updates, but it is also called when the end()
+ * function is called, to set the final value on the property.
+ *
+ * Overrides of this method must call the superclass to perform the calculation
+ * of the animated value.
+ *
+ * @param fraction The elapsed fraction of the animation.
+ */
+ void animateValue(float fraction) {
+ fraction = mInterpolator.getInterpolation(fraction);
+ mCurrentFraction = fraction;
+ int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].calculateValue(fraction);
+ }
+ if (mUpdateListeners != null) {
+ int numListeners = mUpdateListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ mUpdateListeners.get(i).onAnimationUpdate(this);
+ }
+ }
+ }
+
+ @Override
+ public ValueAnimator clone() {
+ final ValueAnimator anim = (ValueAnimator) super.clone();
+ if (mUpdateListeners != null) {
+ ArrayList oldListeners = mUpdateListeners;
+ anim.mUpdateListeners = new ArrayList();
+ int numListeners = oldListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ anim.mUpdateListeners.add(oldListeners.get(i));
+ }
+ }
+ anim.mSeekTime = -1;
+ anim.mPlayingBackwards = false;
+ anim.mCurrentIteration = 0;
+ anim.mInitialized = false;
+ anim.mPlayingState = STOPPED;
+ anim.mStartedDelay = false;
+ PropertyValuesHolder[] oldValues = mValues;
+ if (oldValues != null) {
+ int numValues = oldValues.length;
+ anim.mValues = new PropertyValuesHolder[numValues];
+ anim.mValuesMap = new HashMap(numValues);
+ for (int i = 0; i < numValues; ++i) {
+ PropertyValuesHolder newValuesHolder = oldValues[i].clone();
+ anim.mValues[i] = newValuesHolder;
+ anim.mValuesMap.put(newValuesHolder.getPropertyName(), newValuesHolder);
+ }
+ }
+ return anim;
+ }
+
+ /**
+ * Implementors of this interface can add themselves as update listeners
+ * to an ValueAnimator instance to receive callbacks on every animation
+ * frame, after the current frame's values have been calculated for that
+ * ValueAnimator.
+ */
+ public static interface AnimatorUpdateListener {
+ /**
+ * Notifies the occurrence of another frame of the animation.
+ *
+ * @param animation The animation which was repeated.
+ */
+ void onAnimationUpdate(ValueAnimator animation);
+
+ }
+
+ /**
+ * Return the number of animations currently running.
+ *
+ * Used by StrictMode internally to annotate violations. Only
+ * called on the main thread.
+ *
+ * @hide
+ */
+ public static int getCurrentAnimationsCount() {
+ return sAnimations.get().size();
+ }
+
+ /**
+ * Clear all animations on this thread, without canceling or ending them.
+ * This should be used with caution.
+ *
+ * @hide
+ */
+ public static void clearAllAnimations() {
+ sAnimations.get().clear();
+ sPendingAnimations.get().clear();
+ sDelayedAnims.get().clear();
+ }
+
+ @Override
+ public String toString() {
+ String returnVal = "ValueAnimator@" + Integer.toHexString(hashCode());
+ if (mValues != null) {
+ for (int i = 0; i < mValues.length; ++i) {
+ returnVal += "\n " + mValues[i].toString();
+ }
+ }
+ return returnVal;
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/util/FloatProperty.java b/app/src/main/java/com/nineoldandroids/util/FloatProperty.java
new file mode 100644
index 000000000..ba3f48f09
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/util/FloatProperty.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 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 com.nineoldandroids.util;
+
+/**
+ * An implementation of {@link android.util.Property} to be used specifically with fields of type
+ * float. This type-specific subclass enables performance benefit by allowing
+ * calls to a {@link #set(Object, Float) set()} function that takes the primitive
+ * float type and avoids autoboxing and other overhead associated with the
+ * Float class.
+ *
+ * @param The class on which the Property is declared.
+ *
+ * @hide
+ */
+public abstract class FloatProperty extends Property {
+
+ public FloatProperty(String name) {
+ super(Float.class, name);
+ }
+
+ /**
+ * A type-specific override of the {@link #set(Object, Float)} that is faster when dealing
+ * with fields of type float.
+ */
+ public abstract void setValue(T object, float value);
+
+ @Override
+ final public void set(T object, Float value) {
+ setValue(object, value);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nineoldandroids/util/IntProperty.java b/app/src/main/java/com/nineoldandroids/util/IntProperty.java
new file mode 100644
index 000000000..73d155d2a
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/util/IntProperty.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 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 com.nineoldandroids.util;
+
+/**
+ * An implementation of {@link android.util.Property} to be used specifically with fields of type
+ * int. This type-specific subclass enables performance benefit by allowing
+ * calls to a {@link #set(Object, Integer) set()} function that takes the primitive
+ * int type and avoids autoboxing and other overhead associated with the
+ * Integer class.
+ *
+ * @param The class on which the Property is declared.
+ *
+ * @hide
+ */
+public abstract class IntProperty extends Property {
+
+ public IntProperty(String name) {
+ super(Integer.class, name);
+ }
+
+ /**
+ * A type-specific override of the {@link #set(Object, Integer)} that is faster when dealing
+ * with fields of type int.
+ */
+ public abstract void setValue(T object, int value);
+
+ @Override
+ final public void set(T object, Integer value) {
+ set(object, value.intValue());
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nineoldandroids/util/NoSuchPropertyException.java b/app/src/main/java/com/nineoldandroids/util/NoSuchPropertyException.java
new file mode 100644
index 000000000..02fa234ab
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/util/NoSuchPropertyException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 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 com.nineoldandroids.util;
+
+/**
+ * Thrown when code requests a {@link com.nineoldandroids.util.Property} on a class that does
+ * not expose the appropriate method or field.
+ *
+ * @see com.nineoldandroids.util.Property#of(Class, Class, String)
+ */
+public class NoSuchPropertyException extends RuntimeException {
+
+ public NoSuchPropertyException(String s) {
+ super(s);
+ }
+
+}
diff --git a/app/src/main/java/com/nineoldandroids/util/Property.java b/app/src/main/java/com/nineoldandroids/util/Property.java
new file mode 100644
index 000000000..1e108722a
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/util/Property.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011 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 com.nineoldandroids.util;
+
+
+/**
+ * A property is an abstraction that can be used to represent a mutable value that is held
+ * in a host object. The Property's {@link #set(Object, Object)} or {@link #get(Object)}
+ * methods can be implemented in terms of the private fields of the host object, or via "setter" and
+ * "getter" methods or by some other mechanism, as appropriate.
+ *
+ * @param The class on which the property is declared.
+ * @param The type that this property represents.
+ */
+public abstract class Property {
+
+ private final String mName;
+ private final Class mType;
+
+ /**
+ * This factory method creates and returns a Property given the class and
+ * name parameters, where the "name" parameter represents either:
+ *
+ * - a public
getName() method on the class which takes no arguments, plus an
+ * optional public setName() method which takes a value of the same type
+ * returned by getName()
+ * - a public
isName() method on the class which takes no arguments, plus an
+ * optional public setName() method which takes a value of the same type
+ * returned by isName()
+ * - a public
name field on the class
+ *
+ *
+ * If either of the get/is method alternatives is found on the class, but an appropriate
+ * setName() method is not found, the Property will be
+ * {@link #isReadOnly() readOnly}. Calling the {@link #set(Object, Object)} method on such
+ * a property is allowed, but will have no effect.
+ *
+ * If neither the methods nor the field are found on the class a
+ * {@link NoSuchPropertyException} exception will be thrown.
+ */
+ public static Property of(Class hostType, Class valueType, String name) {
+ return new ReflectiveProperty(hostType, valueType, name);
+ }
+
+ /**
+ * A constructor that takes an identifying name and {@link #getType() type} for the property.
+ */
+ public Property(Class type, String name) {
+ mName = name;
+ mType = type;
+ }
+
+ /**
+ * Returns true if the {@link #set(Object, Object)} method does not set the value on the target
+ * object (in which case the {@link #set(Object, Object) set()} method should throw a {@link
+ * NoSuchPropertyException} exception). This may happen if the Property wraps functionality that
+ * allows querying the underlying value but not setting it. For example, the {@link #of(Class,
+ * Class, String)} factory method may return a Property with name "foo" for an object that has
+ * only a getFoo() or isFoo() method, but no matching
+ * setFoo() method.
+ */
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ /**
+ * Sets the value on object which this property represents. If the method is unable
+ * to set the value on the target object it will throw an {@link UnsupportedOperationException}
+ * exception.
+ */
+ public void set(T object, V value) {
+ throw new UnsupportedOperationException("Property " + getName() +" is read-only");
+ }
+
+ /**
+ * Returns the current value that this property represents on the given object.
+ */
+ public abstract V get(T object);
+
+ /**
+ * Returns the name for this property.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the type for this property.
+ */
+ public Class getType() {
+ return mType;
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/util/ReflectiveProperty.java b/app/src/main/java/com/nineoldandroids/util/ReflectiveProperty.java
new file mode 100644
index 000000000..4b779c15f
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/util/ReflectiveProperty.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2011 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 com.nineoldandroids.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Internal class to automatically generate a Property for a given class/name pair, given the
+ * specification of {@link com.nineoldandroids.util.Property#of(Class, Class, String)}
+ */
+class ReflectiveProperty extends Property {
+
+ private static final String PREFIX_GET = "get";
+ private static final String PREFIX_IS = "is";
+ private static final String PREFIX_SET = "set";
+ private Method mSetter;
+ private Method mGetter;
+ private Field mField;
+
+ /**
+ * For given property name 'name', look for getName/isName method or 'name' field.
+ * Also look for setName method (optional - could be readonly). Failing method getters and
+ * field results in throwing NoSuchPropertyException.
+ *
+ * @param propertyHolder The class on which the methods or field are found
+ * @param name The name of the property, where this name is capitalized and appended to
+ * "get" and "is to search for the appropriate methods. If the get/is methods are not found,
+ * the constructor will search for a field with that exact name.
+ */
+ public ReflectiveProperty(Class propertyHolder, Class valueType, String name) {
+ // TODO: cache reflection info for each new class/name pair
+ super(valueType, name);
+ char firstLetter = Character.toUpperCase(name.charAt(0));
+ String theRest = name.substring(1);
+ String capitalizedName = firstLetter + theRest;
+ String getterName = PREFIX_GET + capitalizedName;
+ try {
+ mGetter = propertyHolder.getMethod(getterName, (Class>[]) null);
+ } catch (NoSuchMethodException e) {
+ try {
+ /* The native implementation uses JNI to do reflection, which allows access to private methods.
+ * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback.
+ */
+ mGetter = propertyHolder.getDeclaredMethod(getterName, (Class>[]) null);
+ mGetter.setAccessible(true);
+ } catch (NoSuchMethodException e2) {
+ // getName() not available - try isName() instead
+ getterName = PREFIX_IS + capitalizedName;
+ try {
+ mGetter = propertyHolder.getMethod(getterName, (Class>[]) null);
+ } catch (NoSuchMethodException e3) {
+ try {
+ /* The native implementation uses JNI to do reflection, which allows access to private methods.
+ * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback.
+ */
+ mGetter = propertyHolder.getDeclaredMethod(getterName, (Class>[]) null);
+ mGetter.setAccessible(true);
+ } catch (NoSuchMethodException e4) {
+ // Try public field instead
+ try {
+ mField = propertyHolder.getField(name);
+ Class fieldType = mField.getType();
+ if (!typesMatch(valueType, fieldType)) {
+ throw new NoSuchPropertyException("Underlying type (" + fieldType + ") " +
+ "does not match Property type (" + valueType + ")");
+ }
+ return;
+ } catch (NoSuchFieldException e5) {
+ // no way to access property - throw appropriate exception
+ throw new NoSuchPropertyException("No accessor method or field found for"
+ + " property with name " + name);
+ }
+ }
+ }
+ }
+ }
+ Class getterType = mGetter.getReturnType();
+ // Check to make sure our getter type matches our valueType
+ if (!typesMatch(valueType, getterType)) {
+ throw new NoSuchPropertyException("Underlying type (" + getterType + ") " +
+ "does not match Property type (" + valueType + ")");
+ }
+ String setterName = PREFIX_SET + capitalizedName;
+ try {
+ // mSetter = propertyHolder.getMethod(setterName, getterType);
+ // The native implementation uses JNI to do reflection, which allows access to private methods.
+ mSetter = propertyHolder.getDeclaredMethod(setterName, getterType);
+ mSetter.setAccessible(true);
+ } catch (NoSuchMethodException ignored) {
+ // Okay to not have a setter - just a readonly property
+ }
+ }
+
+ /**
+ * Utility method to check whether the type of the underlying field/method on the target
+ * object matches the type of the Property. The extra checks for primitive types are because
+ * generics will force the Property type to be a class, whereas the type of the underlying
+ * method/field will probably be a primitive type instead. Accept float as matching Float,
+ * etc.
+ */
+ private boolean typesMatch(Class valueType, Class getterType) {
+ if (getterType != valueType) {
+ if (getterType.isPrimitive()) {
+ return (getterType == float.class && valueType == Float.class) ||
+ (getterType == int.class && valueType == Integer.class) ||
+ (getterType == boolean.class && valueType == Boolean.class) ||
+ (getterType == long.class && valueType == Long.class) ||
+ (getterType == double.class && valueType == Double.class) ||
+ (getterType == short.class && valueType == Short.class) ||
+ (getterType == byte.class && valueType == Byte.class) ||
+ (getterType == char.class && valueType == Character.class);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void set(T object, V value) {
+ if (mSetter != null) {
+ try {
+ mSetter.invoke(object, value);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ } else if (mField != null) {
+ try {
+ mField.set(object, value);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ }
+ } else {
+ throw new UnsupportedOperationException("Property " + getName() +" is read-only");
+ }
+ }
+
+ @Override
+ public V get(T object) {
+ if (mGetter != null) {
+ try {
+ return (V) mGetter.invoke(object, (Object[])null);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ } else if (mField != null) {
+ try {
+ return (V) mField.get(object);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ }
+ }
+ // Should not get here: there should always be a non-null getter or field
+ throw new AssertionError();
+ }
+
+ /**
+ * Returns false if there is no setter or public field underlying this Property.
+ */
+ @Override
+ public boolean isReadOnly() {
+ return (mSetter == null && mField == null);
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/view/ViewHelper.java b/app/src/main/java/com/nineoldandroids/view/ViewHelper.java
new file mode 100644
index 000000000..d79dc0f35
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/view/ViewHelper.java
@@ -0,0 +1,292 @@
+package com.nineoldandroids.view;
+
+import android.view.View;
+
+import static com.nineoldandroids.view.animation.AnimatorProxy.NEEDS_PROXY;
+import static com.nineoldandroids.view.animation.AnimatorProxy.wrap;
+
+public final class ViewHelper {
+ private ViewHelper() {}
+
+ public static float getAlpha(View view) {
+ return NEEDS_PROXY ? wrap(view).getAlpha() : Honeycomb.getAlpha(view);
+ }
+
+ public static void setAlpha(View view, float alpha) {
+ if (NEEDS_PROXY) {
+ wrap(view).setAlpha(alpha);
+ } else {
+ Honeycomb.setAlpha(view, alpha);
+ }
+ }
+
+ public static float getPivotX(View view) {
+ return NEEDS_PROXY ? wrap(view).getPivotX() : Honeycomb.getPivotX(view);
+ }
+
+ public static void setPivotX(View view, float pivotX) {
+ if (NEEDS_PROXY) {
+ wrap(view).setPivotX(pivotX);
+ } else {
+ Honeycomb.setPivotX(view, pivotX);
+ }
+ }
+
+ public static float getPivotY(View view) {
+ return NEEDS_PROXY ? wrap(view).getPivotY() : Honeycomb.getPivotY(view);
+ }
+
+ public static void setPivotY(View view, float pivotY) {
+ if (NEEDS_PROXY) {
+ wrap(view).setPivotY(pivotY);
+ } else {
+ Honeycomb.setPivotY(view, pivotY);
+ }
+ }
+
+ public static float getRotation(View view) {
+ return NEEDS_PROXY ? wrap(view).getRotation() : Honeycomb.getRotation(view);
+ }
+
+ public static void setRotation(View view, float rotation) {
+ if (NEEDS_PROXY) {
+ wrap(view).setRotation(rotation);
+ } else {
+ Honeycomb.setRotation(view, rotation);
+ }
+ }
+
+ public static float getRotationX(View view) {
+ return NEEDS_PROXY ? wrap(view).getRotationX() : Honeycomb.getRotationX(view);
+ }
+
+ public static void setRotationX(View view, float rotationX) {
+ if (NEEDS_PROXY) {
+ wrap(view).setRotationX(rotationX);
+ } else {
+ Honeycomb.setRotationX(view, rotationX);
+ }
+ }
+
+ public static float getRotationY(View view) {
+ return NEEDS_PROXY ? wrap(view).getRotationY() : Honeycomb.getRotationY(view);
+ }
+
+ public static void setRotationY(View view, float rotationY) {
+ if (NEEDS_PROXY) {
+ wrap(view).setRotationY(rotationY);
+ } else {
+ Honeycomb.setRotationY(view, rotationY);
+ }
+ }
+
+ public static float getScaleX(View view) {
+ return NEEDS_PROXY ? wrap(view).getScaleX() : Honeycomb.getScaleX(view);
+ }
+
+ public static void setScaleX(View view, float scaleX) {
+ if (NEEDS_PROXY) {
+ wrap(view).setScaleX(scaleX);
+ } else {
+ Honeycomb.setScaleX(view, scaleX);
+ }
+ }
+
+ public static float getScaleY(View view) {
+ return NEEDS_PROXY ? wrap(view).getScaleY() : Honeycomb.getScaleY(view);
+ }
+
+ public static void setScaleY(View view, float scaleY) {
+ if (NEEDS_PROXY) {
+ wrap(view).setScaleY(scaleY);
+ } else {
+ Honeycomb.setScaleY(view, scaleY);
+ }
+ }
+
+ public static float getScrollX(View view) {
+ return NEEDS_PROXY ? wrap(view).getScrollX() : Honeycomb.getScrollX(view);
+ }
+
+ public static void setScrollX(View view, int scrollX) {
+ if (NEEDS_PROXY) {
+ wrap(view).setScrollX(scrollX);
+ } else {
+ Honeycomb.setScrollX(view, scrollX);
+ }
+ }
+
+ public static float getScrollY(View view) {
+ return NEEDS_PROXY ? wrap(view).getScrollY() : Honeycomb.getScrollY(view);
+ }
+
+ public static void setScrollY(View view, int scrollY) {
+ if (NEEDS_PROXY) {
+ wrap(view).setScrollY(scrollY);
+ } else {
+ Honeycomb.setScrollY(view, scrollY);
+ }
+ }
+
+ public static float getTranslationX(View view) {
+ return NEEDS_PROXY ? wrap(view).getTranslationX() : Honeycomb.getTranslationX(view);
+ }
+
+ public static void setTranslationX(View view, float translationX) {
+ if (NEEDS_PROXY) {
+ wrap(view).setTranslationX(translationX);
+ } else {
+ Honeycomb.setTranslationX(view, translationX);
+ }
+ }
+
+ public static float getTranslationY(View view) {
+ return NEEDS_PROXY ? wrap(view).getTranslationY() : Honeycomb.getTranslationY(view);
+ }
+
+ public static void setTranslationY(View view, float translationY) {
+ if (NEEDS_PROXY) {
+ wrap(view).setTranslationY(translationY);
+ } else {
+ Honeycomb.setTranslationY(view, translationY);
+ }
+ }
+
+ public static float getX(View view) {
+ return NEEDS_PROXY ? wrap(view).getX() : Honeycomb.getX(view);
+ }
+
+ public static void setX(View view, float x) {
+ if (NEEDS_PROXY) {
+ wrap(view).setX(x);
+ } else {
+ Honeycomb.setX(view, x);
+ }
+ }
+
+ public static float getY(View view) {
+ return NEEDS_PROXY ? wrap(view).getY() : Honeycomb.getY(view);
+ }
+
+ public static void setY(View view, float y) {
+ if (NEEDS_PROXY) {
+ wrap(view).setY(y);
+ } else {
+ Honeycomb.setY(view, y);
+ }
+ }
+
+ private static final class Honeycomb {
+ static float getAlpha(View view) {
+ return view.getAlpha();
+ }
+
+ static void setAlpha(View view, float alpha) {
+ view.setAlpha(alpha);
+ }
+
+ static float getPivotX(View view) {
+ return view.getPivotX();
+ }
+
+ static void setPivotX(View view, float pivotX) {
+ view.setPivotX(pivotX);
+ }
+
+ static float getPivotY(View view) {
+ return view.getPivotY();
+ }
+
+ static void setPivotY(View view, float pivotY) {
+ view.setPivotY(pivotY);
+ }
+
+ static float getRotation(View view) {
+ return view.getRotation();
+ }
+
+ static void setRotation(View view, float rotation) {
+ view.setRotation(rotation);
+ }
+
+ static float getRotationX(View view) {
+ return view.getRotationX();
+ }
+
+ static void setRotationX(View view, float rotationX) {
+ view.setRotationX(rotationX);
+ }
+
+ static float getRotationY(View view) {
+ return view.getRotationY();
+ }
+
+ static void setRotationY(View view, float rotationY) {
+ view.setRotationY(rotationY);
+ }
+
+ static float getScaleX(View view) {
+ return view.getScaleX();
+ }
+
+ static void setScaleX(View view, float scaleX) {
+ view.setScaleX(scaleX);
+ }
+
+ static float getScaleY(View view) {
+ return view.getScaleY();
+ }
+
+ static void setScaleY(View view, float scaleY) {
+ view.setScaleY(scaleY);
+ }
+
+ static float getScrollX(View view) {
+ return view.getScrollX();
+ }
+
+ static void setScrollX(View view, int scrollX) {
+ view.setScrollX(scrollX);
+ }
+
+ static float getScrollY(View view) {
+ return view.getScrollY();
+ }
+
+ static void setScrollY(View view, int scrollY) {
+ view.setScrollY(scrollY);
+ }
+
+ static float getTranslationX(View view) {
+ return view.getTranslationX();
+ }
+
+ static void setTranslationX(View view, float translationX) {
+ view.setTranslationX(translationX);
+ }
+
+ static float getTranslationY(View view) {
+ return view.getTranslationY();
+ }
+
+ static void setTranslationY(View view, float translationY) {
+ view.setTranslationY(translationY);
+ }
+
+ static float getX(View view) {
+ return view.getX();
+ }
+
+ static void setX(View view, float x) {
+ view.setX(x);
+ }
+
+ static float getY(View view) {
+ return view.getY();
+ }
+
+ static void setY(View view, float y) {
+ view.setY(y);
+ }
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimator.java b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimator.java
new file mode 100644
index 000000000..4de664fda
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimator.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2011 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 com.nineoldandroids.view;
+
+import java.util.WeakHashMap;
+import android.os.Build;
+import android.view.View;
+import android.view.animation.Interpolator;
+import com.nineoldandroids.animation.Animator;
+
+/**
+ * This class enables automatic and optimized animation of select properties on View objects.
+ * If only one or two properties on a View object are being animated, then using an
+ * {@link android.animation.ObjectAnimator} is fine; the property setters called by ObjectAnimator
+ * are well equipped to do the right thing to set the property and invalidate the view
+ * appropriately. But if several properties are animated simultaneously, or if you just want a
+ * more convenient syntax to animate a specific property, then ViewPropertyAnimator might be
+ * more well-suited to the task.
+ *
+ * This class may provide better performance for several simultaneous animations, because
+ * it will optimize invalidate calls to take place only once for several properties instead of each
+ * animated property independently causing its own invalidation. Also, the syntax of using this
+ * class could be easier to use because the caller need only tell the View object which
+ * property to animate, and the value to animate either to or by, and this class handles the
+ * details of configuring the underlying Animator class and starting it.
+ *
+ * This class is not constructed by the caller, but rather by the View whose properties
+ * it will animate. Calls to {@link android.view.View#animate()} will return a reference
+ * to the appropriate ViewPropertyAnimator object for that View.
+ *
+ */
+public abstract class ViewPropertyAnimator {
+ private static final WeakHashMap ANIMATORS =
+ new WeakHashMap(0);
+
+ /**
+ * This method returns a ViewPropertyAnimator object, which can be used to animate specific
+ * properties on this View.
+ *
+ * @param view View to animate.
+ * @return The ViewPropertyAnimator associated with this View.
+ */
+ public static ViewPropertyAnimator animate(View view) {
+ ViewPropertyAnimator animator = ANIMATORS.get(view);
+ if (animator == null) {
+ final int version = Integer.valueOf(Build.VERSION.SDK);
+ if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ animator = new ViewPropertyAnimatorICS(view);
+ } else if (version >= Build.VERSION_CODES.HONEYCOMB) {
+ animator = new ViewPropertyAnimatorHC(view);
+ } else {
+ animator = new ViewPropertyAnimatorPreHC(view);
+ }
+ ANIMATORS.put(view, animator);
+ }
+ return animator;
+ }
+
+
+ /**
+ * Sets the duration for the underlying animator that animates the requested properties.
+ * By default, the animator uses the default value for ValueAnimator. Calling this method
+ * will cause the declared value to be used instead.
+ * @param duration The length of ensuing property animations, in milliseconds. The value
+ * cannot be negative.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator setDuration(long duration);
+
+ /**
+ * Returns the current duration of property animations. If the duration was set on this
+ * object, that value is returned. Otherwise, the default value of the underlying Animator
+ * is returned.
+ *
+ * @see #setDuration(long)
+ * @return The duration of animations, in milliseconds.
+ */
+ public abstract long getDuration();
+
+ /**
+ * Returns the current startDelay of property animations. If the startDelay was set on this
+ * object, that value is returned. Otherwise, the default value of the underlying Animator
+ * is returned.
+ *
+ * @see #setStartDelay(long)
+ * @return The startDelay of animations, in milliseconds.
+ */
+ public abstract long getStartDelay();
+
+ /**
+ * Sets the startDelay for the underlying animator that animates the requested properties.
+ * By default, the animator uses the default value for ValueAnimator. Calling this method
+ * will cause the declared value to be used instead.
+ * @param startDelay The delay of ensuing property animations, in milliseconds. The value
+ * cannot be negative.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator setStartDelay(long startDelay);
+
+ /**
+ * Sets the interpolator for the underlying animator that animates the requested properties.
+ * By default, the animator uses the default interpolator for ValueAnimator. Calling this method
+ * will cause the declared object to be used instead.
+ *
+ * @param interpolator The TimeInterpolator to be used for ensuing property animations.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator setInterpolator(/*Time*/Interpolator interpolator);
+
+ /**
+ * Sets a listener for events in the underlying Animators that run the property
+ * animations.
+ *
+ * @param listener The listener to be called with AnimatorListener events.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator setListener(Animator.AnimatorListener listener);
+
+ /**
+ * Starts the currently pending property animations immediately. Calling start()
+ * is optional because all animations start automatically at the next opportunity. However,
+ * if the animations are needed to start immediately and synchronously (not at the time when
+ * the next event is processed by the hierarchy, which is when the animations would begin
+ * otherwise), then this method can be used.
+ */
+ public abstract void start();
+
+ /**
+ * Cancels all property animations that are currently running or pending.
+ */
+ public abstract void cancel();
+
+ /**
+ * This method will cause the View's x property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator x(float value);
+
+ /**
+ * This method will cause the View's x property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator xBy(float value);
+
+ /**
+ * This method will cause the View's y property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator y(float value);
+
+ /**
+ * This method will cause the View's y property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator yBy(float value);
+
+ /**
+ * This method will cause the View's rotation property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setRotation(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator rotation(float value);
+
+ /**
+ * This method will cause the View's rotation property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setRotation(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator rotationBy(float value);
+
+ /**
+ * This method will cause the View's rotationX property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setRotationX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator rotationX(float value);
+
+ /**
+ * This method will cause the View's rotationX property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setRotationX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator rotationXBy(float value);
+
+ /**
+ * This method will cause the View's rotationY property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setRotationY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator rotationY(float value);
+
+ /**
+ * This method will cause the View's rotationY property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setRotationY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator rotationYBy(float value);
+
+ /**
+ * This method will cause the View's translationX property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setTranslationX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator translationX(float value);
+
+ /**
+ * This method will cause the View's translationX property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setTranslationX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator translationXBy(float value);
+
+ /**
+ * This method will cause the View's translationY property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setTranslationY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator translationY(float value);
+
+ /**
+ * This method will cause the View's translationY property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setTranslationY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator translationYBy(float value);
+
+ /**
+ * This method will cause the View's scaleX property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setScaleX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator scaleX(float value);
+
+ /**
+ * This method will cause the View's scaleX property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setScaleX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator scaleXBy(float value);
+
+ /**
+ * This method will cause the View's scaleY property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setScaleY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator scaleY(float value);
+
+ /**
+ * This method will cause the View's scaleY property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setScaleY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator scaleYBy(float value);
+
+ /**
+ * This method will cause the View's alpha property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see android.view.View#setAlpha(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator alpha(float value);
+
+ /**
+ * This method will cause the View's alpha property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see android.view.View#setAlpha(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public abstract ViewPropertyAnimator alphaBy(float value);
+}
diff --git a/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorHC.java b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorHC.java
new file mode 100644
index 000000000..7ad2adaa2
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorHC.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2011 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 com.nineoldandroids.view;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+import android.view.View;
+import android.view.animation.Interpolator;
+import com.nineoldandroids.animation.Animator;
+import com.nineoldandroids.animation.ValueAnimator;
+
+class ViewPropertyAnimatorHC extends ViewPropertyAnimator {
+
+ /**
+ * A WeakReference holding the View whose properties are being animated by this class.
+ * This is set at construction time.
+ */
+ private final WeakReference mView;
+
+ /**
+ * The duration of the underlying Animator object. By default, we don't set the duration
+ * on the Animator and just use its default duration. If the duration is ever set on this
+ * Animator, then we use the duration that it was set to.
+ */
+ private long mDuration;
+
+ /**
+ * A flag indicating whether the duration has been set on this object. If not, we don't set
+ * the duration on the underlying Animator, but instead just use its default duration.
+ */
+ private boolean mDurationSet = false;
+
+ /**
+ * The startDelay of the underlying Animator object. By default, we don't set the startDelay
+ * on the Animator and just use its default startDelay. If the startDelay is ever set on this
+ * Animator, then we use the startDelay that it was set to.
+ */
+ private long mStartDelay = 0;
+
+ /**
+ * A flag indicating whether the startDelay has been set on this object. If not, we don't set
+ * the startDelay on the underlying Animator, but instead just use its default startDelay.
+ */
+ private boolean mStartDelaySet = false;
+
+ /**
+ * The interpolator of the underlying Animator object. By default, we don't set the interpolator
+ * on the Animator and just use its default interpolator. If the interpolator is ever set on
+ * this Animator, then we use the interpolator that it was set to.
+ */
+ private /*Time*/Interpolator mInterpolator;
+
+ /**
+ * A flag indicating whether the interpolator has been set on this object. If not, we don't set
+ * the interpolator on the underlying Animator, but instead just use its default interpolator.
+ */
+ private boolean mInterpolatorSet = false;
+
+ /**
+ * Listener for the lifecycle events of the underlying
+ */
+ private Animator.AnimatorListener mListener = null;
+
+ /**
+ * This listener is the mechanism by which the underlying Animator causes changes to the
+ * properties currently being animated, as well as the cleanup after an animation is
+ * complete.
+ */
+ private AnimatorEventListener mAnimatorEventListener = new AnimatorEventListener();
+
+ /**
+ * This list holds the properties that have been asked to animate. We allow the caller to
+ * request several animations prior to actually starting the underlying animator. This
+ * enables us to run one single animator to handle several properties in parallel. Each
+ * property is tossed onto the pending list until the animation actually starts (which is
+ * done by posting it onto mView), at which time the pending list is cleared and the properties
+ * on that list are added to the list of properties associated with that animator.
+ */
+ ArrayList mPendingAnimations = new ArrayList();
+
+ /**
+ * Constants used to associate a property being requested and the mechanism used to set
+ * the property (this class calls directly into View to set the properties in question).
+ */
+ private static final int NONE = 0x0000;
+ private static final int TRANSLATION_X = 0x0001;
+ private static final int TRANSLATION_Y = 0x0002;
+ private static final int SCALE_X = 0x0004;
+ private static final int SCALE_Y = 0x0008;
+ private static final int ROTATION = 0x0010;
+ private static final int ROTATION_X = 0x0020;
+ private static final int ROTATION_Y = 0x0040;
+ private static final int X = 0x0080;
+ private static final int Y = 0x0100;
+ private static final int ALPHA = 0x0200;
+
+ private static final int TRANSFORM_MASK = TRANSLATION_X | TRANSLATION_Y | SCALE_X | SCALE_Y |
+ ROTATION | ROTATION_X | ROTATION_Y | X | Y;
+
+ /**
+ * The mechanism by which the user can request several properties that are then animated
+ * together works by posting this Runnable to start the underlying Animator. Every time
+ * a property animation is requested, we cancel any previous postings of the Runnable
+ * and re-post it. This means that we will only ever run the Runnable (and thus start the
+ * underlying animator) after the caller is done setting the properties that should be
+ * animated together.
+ */
+ private Runnable mAnimationStarter = new Runnable() {
+ @Override
+ public void run() {
+ startAnimation();
+ }
+ };
+
+ /**
+ * This class holds information about the overall animation being run on the set of
+ * properties. The mask describes which properties are being animated and the
+ * values holder is the list of all property/value objects.
+ */
+ private static class PropertyBundle {
+ int mPropertyMask;
+ ArrayList mNameValuesHolder;
+
+ PropertyBundle(int propertyMask, ArrayList nameValuesHolder) {
+ mPropertyMask = propertyMask;
+ mNameValuesHolder = nameValuesHolder;
+ }
+
+ /**
+ * Removes the given property from being animated as a part of this
+ * PropertyBundle. If the property was a part of this bundle, it returns
+ * true to indicate that it was, in fact, canceled. This is an indication
+ * to the caller that a cancellation actually occurred.
+ *
+ * @param propertyConstant The property whose cancellation is requested.
+ * @return true if the given property is a part of this bundle and if it
+ * has therefore been canceled.
+ */
+ boolean cancel(int propertyConstant) {
+ if ((mPropertyMask & propertyConstant) != 0 && mNameValuesHolder != null) {
+ int count = mNameValuesHolder.size();
+ for (int i = 0; i < count; ++i) {
+ NameValuesHolder nameValuesHolder = mNameValuesHolder.get(i);
+ if (nameValuesHolder.mNameConstant == propertyConstant) {
+ mNameValuesHolder.remove(i);
+ mPropertyMask &= ~propertyConstant;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * This list tracks the list of properties being animated by any particular animator.
+ * In most situations, there would only ever be one animator running at a time. But it is
+ * possible to request some properties to animate together, then while those properties
+ * are animating, to request some other properties to animate together. The way that
+ * works is by having this map associate the group of properties being animated with the
+ * animator handling the animation. On every update event for an Animator, we ask the
+ * map for the associated properties and set them accordingly.
+ */
+ private HashMap mAnimatorMap =
+ new HashMap();
+
+ /**
+ * This is the information we need to set each property during the animation.
+ * mNameConstant is used to set the appropriate field in View, and the from/delta
+ * values are used to calculate the animated value for a given animation fraction
+ * during the animation.
+ */
+ private static class NameValuesHolder {
+ int mNameConstant;
+ float mFromValue;
+ float mDeltaValue;
+ NameValuesHolder(int nameConstant, float fromValue, float deltaValue) {
+ mNameConstant = nameConstant;
+ mFromValue = fromValue;
+ mDeltaValue = deltaValue;
+ }
+ }
+
+ /**
+ * Constructor, called by View. This is private by design, as the user should only
+ * get a ViewPropertyAnimator by calling View.animate().
+ *
+ * @param view The View associated with this ViewPropertyAnimator
+ */
+ ViewPropertyAnimatorHC(View view) {
+ mView = new WeakReference(view);
+ }
+
+ /**
+ * Sets the duration for the underlying animator that animates the requested properties.
+ * By default, the animator uses the default value for ValueAnimator. Calling this method
+ * will cause the declared value to be used instead.
+ * @param duration The length of ensuing property animations, in milliseconds. The value
+ * cannot be negative.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator setDuration(long duration) {
+ if (duration < 0) {
+ throw new IllegalArgumentException("Animators cannot have negative duration: " +
+ duration);
+ }
+ mDurationSet = true;
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Returns the current duration of property animations. If the duration was set on this
+ * object, that value is returned. Otherwise, the default value of the underlying Animator
+ * is returned.
+ *
+ * @see #setDuration(long)
+ * @return The duration of animations, in milliseconds.
+ */
+ public long getDuration() {
+ if (mDurationSet) {
+ return mDuration;
+ } else {
+ // Just return the default from ValueAnimator, since that's what we'd get if
+ // the value has not been set otherwise
+ return new ValueAnimator().getDuration();
+ }
+ }
+
+ @Override
+ public long getStartDelay() {
+ if (mStartDelaySet) {
+ return mStartDelay;
+ } else {
+ // Just return the default from ValueAnimator (0), since that's what we'd get if
+ // the value has not been set otherwise
+ return 0;
+ }
+ }
+
+ @Override
+ public ViewPropertyAnimator setStartDelay(long startDelay) {
+ if (startDelay < 0) {
+ throw new IllegalArgumentException("Animators cannot have negative duration: " +
+ startDelay);
+ }
+ mStartDelaySet = true;
+ mStartDelay = startDelay;
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator setInterpolator(/*Time*/Interpolator interpolator) {
+ mInterpolatorSet = true;
+ mInterpolator = interpolator;
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator setListener(Animator.AnimatorListener listener) {
+ mListener = listener;
+ return this;
+ }
+
+ @Override
+ public void start() {
+ startAnimation();
+ }
+
+ @Override
+ public void cancel() {
+ if (mAnimatorMap.size() > 0) {
+ HashMap mAnimatorMapCopy =
+ (HashMap)mAnimatorMap.clone();
+ Set animatorSet = mAnimatorMapCopy.keySet();
+ for (Animator runningAnim : animatorSet) {
+ runningAnim.cancel();
+ }
+ }
+ mPendingAnimations.clear();
+ View v = mView.get();
+ if (v != null) {
+ v.removeCallbacks(mAnimationStarter);
+ }
+ }
+
+ @Override
+ public ViewPropertyAnimator x(float value) {
+ animateProperty(X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator xBy(float value) {
+ animatePropertyBy(X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator y(float value) {
+ animateProperty(Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator yBy(float value) {
+ animatePropertyBy(Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotation(float value) {
+ animateProperty(ROTATION, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationBy(float value) {
+ animatePropertyBy(ROTATION, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationX(float value) {
+ animateProperty(ROTATION_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationXBy(float value) {
+ animatePropertyBy(ROTATION_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationY(float value) {
+ animateProperty(ROTATION_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationYBy(float value) {
+ animatePropertyBy(ROTATION_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationX(float value) {
+ animateProperty(TRANSLATION_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationXBy(float value) {
+ animatePropertyBy(TRANSLATION_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationY(float value) {
+ animateProperty(TRANSLATION_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationYBy(float value) {
+ animatePropertyBy(TRANSLATION_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleX(float value) {
+ animateProperty(SCALE_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleXBy(float value) {
+ animatePropertyBy(SCALE_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleY(float value) {
+ animateProperty(SCALE_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleYBy(float value) {
+ animatePropertyBy(SCALE_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator alpha(float value) {
+ animateProperty(ALPHA, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator alphaBy(float value) {
+ animatePropertyBy(ALPHA, value);
+ return this;
+ }
+
+ /**
+ * Starts the underlying Animator for a set of properties. We use a single animator that
+ * simply runs from 0 to 1, and then use that fractional value to set each property
+ * value accordingly.
+ */
+ private void startAnimation() {
+ ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
+ ArrayList nameValueList =
+ (ArrayList) mPendingAnimations.clone();
+ mPendingAnimations.clear();
+ int propertyMask = 0;
+ int propertyCount = nameValueList.size();
+ for (int i = 0; i < propertyCount; ++i) {
+ NameValuesHolder nameValuesHolder = nameValueList.get(i);
+ propertyMask |= nameValuesHolder.mNameConstant;
+ }
+ mAnimatorMap.put(animator, new PropertyBundle(propertyMask, nameValueList));
+ animator.addUpdateListener(mAnimatorEventListener);
+ animator.addListener(mAnimatorEventListener);
+ if (mStartDelaySet) {
+ animator.setStartDelay(mStartDelay);
+ }
+ if (mDurationSet) {
+ animator.setDuration(mDuration);
+ }
+ if (mInterpolatorSet) {
+ animator.setInterpolator(mInterpolator);
+ }
+ animator.start();
+ }
+
+ /**
+ * Utility function, called by the various x(), y(), etc. methods. This stores the
+ * constant name for the property along with the from/delta values that will be used to
+ * calculate and set the property during the animation. This structure is added to the
+ * pending animations, awaiting the eventual start() of the underlying animator. A
+ * Runnable is posted to start the animation, and any pending such Runnable is canceled
+ * (which enables us to end up starting just one animator for all of the properties
+ * specified at one time).
+ *
+ * @param constantName The specifier for the property being animated
+ * @param toValue The value to which the property will animate
+ */
+ private void animateProperty(int constantName, float toValue) {
+ float fromValue = getValue(constantName);
+ float deltaValue = toValue - fromValue;
+ animatePropertyBy(constantName, fromValue, deltaValue);
+ }
+
+ /**
+ * Utility function, called by the various xBy(), yBy(), etc. methods. This method is
+ * just like animateProperty(), except the value is an offset from the property's
+ * current value, instead of an absolute "to" value.
+ *
+ * @param constantName The specifier for the property being animated
+ * @param byValue The amount by which the property will change
+ */
+ private void animatePropertyBy(int constantName, float byValue) {
+ float fromValue = getValue(constantName);
+ animatePropertyBy(constantName, fromValue, byValue);
+ }
+
+ /**
+ * Utility function, called by animateProperty() and animatePropertyBy(), which handles the
+ * details of adding a pending animation and posting the request to start the animation.
+ *
+ * @param constantName The specifier for the property being animated
+ * @param startValue The starting value of the property
+ * @param byValue The amount by which the property will change
+ */
+ private void animatePropertyBy(int constantName, float startValue, float byValue) {
+ // First, cancel any existing animations on this property
+ if (mAnimatorMap.size() > 0) {
+ Animator animatorToCancel = null;
+ Set animatorSet = mAnimatorMap.keySet();
+ for (Animator runningAnim : animatorSet) {
+ PropertyBundle bundle = mAnimatorMap.get(runningAnim);
+ if (bundle.cancel(constantName)) {
+ // property was canceled - cancel the animation if it's now empty
+ // Note that it's safe to break out here because every new animation
+ // on a property will cancel a previous animation on that property, so
+ // there can only ever be one such animation running.
+ if (bundle.mPropertyMask == NONE) {
+ // the animation is no longer changing anything - cancel it
+ animatorToCancel = runningAnim;
+ break;
+ }
+ }
+ }
+ if (animatorToCancel != null) {
+ animatorToCancel.cancel();
+ }
+ }
+
+ NameValuesHolder nameValuePair = new NameValuesHolder(constantName, startValue, byValue);
+ mPendingAnimations.add(nameValuePair);
+ View v = mView.get();
+ if (v != null) {
+ v.removeCallbacks(mAnimationStarter);
+ v.post(mAnimationStarter);
+ }
+ }
+
+ /**
+ * This method handles setting the property values directly in the View object's fields.
+ * propertyConstant tells it which property should be set, value is the value to set
+ * the property to.
+ *
+ * @param propertyConstant The property to be set
+ * @param value The value to set the property to
+ */
+ private void setValue(int propertyConstant, float value) {
+ //final View.TransformationInfo info = mView.mTransformationInfo;
+ View v = mView.get();
+ if (v != null) {
+ switch (propertyConstant) {
+ case TRANSLATION_X:
+ //info.mTranslationX = value;
+ v.setTranslationX(value);
+ break;
+ case TRANSLATION_Y:
+ //info.mTranslationY = value;
+ v.setTranslationY(value);
+ break;
+ case ROTATION:
+ //info.mRotation = value;
+ v.setRotation(value);
+ break;
+ case ROTATION_X:
+ //info.mRotationX = value;
+ v.setRotationX(value);
+ break;
+ case ROTATION_Y:
+ //info.mRotationY = value;
+ v.setRotationY(value);
+ break;
+ case SCALE_X:
+ //info.mScaleX = value;
+ v.setScaleX(value);
+ break;
+ case SCALE_Y:
+ //info.mScaleY = value;
+ v.setScaleY(value);
+ break;
+ case X:
+ //info.mTranslationX = value - v.mLeft;
+ v.setX(value);
+ break;
+ case Y:
+ //info.mTranslationY = value - v.mTop;
+ v.setY(value);
+ break;
+ case ALPHA:
+ //info.mAlpha = value;
+ v.setAlpha(value);
+ break;
+ }
+ }
+ }
+
+ /**
+ * This method gets the value of the named property from the View object.
+ *
+ * @param propertyConstant The property whose value should be returned
+ * @return float The value of the named property
+ */
+ private float getValue(int propertyConstant) {
+ //final View.TransformationInfo info = mView.mTransformationInfo;
+ View v = mView.get();
+ if (v != null) {
+ switch (propertyConstant) {
+ case TRANSLATION_X:
+ //return info.mTranslationX;
+ return v.getTranslationX();
+ case TRANSLATION_Y:
+ //return info.mTranslationY;
+ return v.getTranslationY();
+ case ROTATION:
+ //return info.mRotation;
+ return v.getRotation();
+ case ROTATION_X:
+ //return info.mRotationX;
+ return v.getRotationX();
+ case ROTATION_Y:
+ //return info.mRotationY;
+ return v.getRotationY();
+ case SCALE_X:
+ //return info.mScaleX;
+ return v.getScaleX();
+ case SCALE_Y:
+ //return info.mScaleY;
+ return v.getScaleY();
+ case X:
+ //return v.mLeft + info.mTranslationX;
+ return v.getX();
+ case Y:
+ //return v.mTop + info.mTranslationY;
+ return v.getY();
+ case ALPHA:
+ //return info.mAlpha;
+ return v.getAlpha();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Utility class that handles the various Animator events. The only ones we care
+ * about are the end event (which we use to clean up the animator map when an animator
+ * finishes) and the update event (which we use to calculate the current value of each
+ * property and then set it on the view object).
+ */
+ private class AnimatorEventListener
+ implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationStart(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationCancel(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationRepeat(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationEnd(animation);
+ }
+ mAnimatorMap.remove(animation);
+ // If the map is empty, it means all animation are done or canceled, so the listener
+ // isn't needed anymore. Not nulling it would cause it to leak any objects used in
+ // its implementation
+ if (mAnimatorMap.isEmpty()) {
+ mListener = null;
+ }
+ }
+
+ /**
+ * Calculate the current value for each property and set it on the view. Invalidate
+ * the view object appropriately, depending on which properties are being animated.
+ *
+ * @param animation The animator associated with the properties that need to be
+ * set. This animator holds the animation fraction which we will use to calculate
+ * the current value of each property.
+ */
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ // alpha requires slightly different treatment than the other (transform) properties.
+ // The logic in setAlpha() is not simply setting mAlpha, plus the invalidation
+ // logic is dependent on how the view handles an internal call to onSetAlpha().
+ // We track what kinds of properties are set, and how alpha is handled when it is
+ // set, and perform the invalidation steps appropriately.
+ //boolean alphaHandled = false;
+ //mView.invalidateParentCaches();
+ float fraction = animation.getAnimatedFraction();
+ PropertyBundle propertyBundle = mAnimatorMap.get(animation);
+ int propertyMask = propertyBundle.mPropertyMask;
+ if ((propertyMask & TRANSFORM_MASK) != 0) {
+ View v = mView.get();
+ if (v != null) {
+ v.invalidate(/*false*/);
+ }
+ }
+ ArrayList valueList = propertyBundle.mNameValuesHolder;
+ if (valueList != null) {
+ int count = valueList.size();
+ for (int i = 0; i < count; ++i) {
+ NameValuesHolder values = valueList.get(i);
+ float value = values.mFromValue + fraction * values.mDeltaValue;
+ //if (values.mNameConstant == ALPHA) {
+ // alphaHandled = mView.setAlphaNoInvalidation(value);
+ //} else {
+ setValue(values.mNameConstant, value);
+ //}
+ }
+ }
+ /*if ((propertyMask & TRANSFORM_MASK) != 0) {
+ mView.mTransformationInfo.mMatrixDirty = true;
+ mView.mPrivateFlags |= View.DRAWN; // force another invalidation
+ }*/
+ // invalidate(false) in all cases except if alphaHandled gets set to true
+ // via the call to setAlphaNoInvalidation(), above
+ View v = mView.get();
+ if (v != null) {
+ v.invalidate(/*alphaHandled*/);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorICS.java b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorICS.java
new file mode 100644
index 000000000..b60a820e4
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorICS.java
@@ -0,0 +1,298 @@
+package com.nineoldandroids.view;
+
+import java.lang.ref.WeakReference;
+
+import android.view.View;
+import android.view.animation.Interpolator;
+import com.nineoldandroids.animation.Animator.AnimatorListener;
+
+class ViewPropertyAnimatorICS extends ViewPropertyAnimator {
+ /**
+ * A value to be returned when the WeakReference holding the native implementation
+ * returns null
+ */
+ private final static long RETURN_WHEN_NULL = -1L;
+
+ /**
+ * A WeakReference holding the native implementation of ViewPropertyAnimator
+ */
+ private final WeakReference mNative;
+
+ ViewPropertyAnimatorICS(View view) {
+ mNative = new WeakReference(view.animate());
+ }
+
+ @Override
+ public ViewPropertyAnimator setDuration(long duration) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.setDuration(duration);
+ }
+ return this;
+ }
+
+ @Override
+ public long getDuration() {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ return n.getDuration();
+ }
+ return RETURN_WHEN_NULL;
+ }
+
+ @Override
+ public ViewPropertyAnimator setStartDelay(long startDelay) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.setStartDelay(startDelay);
+ }
+ return this;
+ }
+
+ @Override
+ public long getStartDelay() {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ return n.getStartDelay();
+ }
+ return RETURN_WHEN_NULL;
+ }
+
+ @Override
+ public ViewPropertyAnimator setInterpolator(Interpolator interpolator) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.setInterpolator(interpolator);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator setListener(final AnimatorListener listener) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ if (listener == null) {
+ n.setListener(null);
+ } else {
+ n.setListener(new android.animation.Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(android.animation.Animator animation) {
+ listener.onAnimationStart(null);
+ }
+
+ @Override
+ public void onAnimationRepeat(android.animation.Animator animation) {
+ listener.onAnimationRepeat(null);
+ }
+
+ @Override
+ public void onAnimationEnd(android.animation.Animator animation) {
+ listener.onAnimationEnd(null);
+ }
+
+ @Override
+ public void onAnimationCancel(android.animation.Animator animation) {
+ listener.onAnimationCancel(null);
+ }
+ });
+ }
+ }
+ return this;
+ }
+
+ @Override
+ public void start() {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.start();
+ }
+ }
+
+ @Override
+ public void cancel() {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.cancel();
+ }
+ }
+
+ @Override
+ public ViewPropertyAnimator x(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.x(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator xBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.xBy(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator y(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.y(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator yBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.yBy(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotation(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.rotation(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.rotationBy(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationX(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.rotationX(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationXBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.rotationXBy(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationY(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.rotationY(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationYBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.rotationYBy(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationX(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.translationX(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationXBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.translationXBy(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationY(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.translationY(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationYBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.translationYBy(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleX(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.scaleX(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleXBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.scaleXBy(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleY(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.scaleY(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleYBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.scaleYBy(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator alpha(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.alpha(value);
+ }
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator alphaBy(float value) {
+ android.view.ViewPropertyAnimator n = mNative.get();
+ if (n != null) {
+ n.alphaBy(value);
+ }
+ return this;
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorPreHC.java b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorPreHC.java
new file mode 100644
index 000000000..c170f6d21
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorPreHC.java
@@ -0,0 +1,724 @@
+/*
+ * Copyright (C) 2011 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 com.nineoldandroids.view;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+import android.view.View;
+import android.view.animation.Interpolator;
+import com.nineoldandroids.animation.Animator;
+import com.nineoldandroids.animation.ValueAnimator;
+import com.nineoldandroids.view.animation.AnimatorProxy;
+
+class ViewPropertyAnimatorPreHC extends ViewPropertyAnimator {
+ /**
+ * Proxy animation class which will allow us access to post-Honeycomb properties that were not
+ * otherwise available.
+ */
+ private final AnimatorProxy mProxy;
+
+ /**
+ * A WeakReference holding the View whose properties are being animated by this class. This is
+ * set at construction time.
+ */
+ private final WeakReference mView;
+
+ /**
+ * The duration of the underlying Animator object. By default, we don't set the duration
+ * on the Animator and just use its default duration. If the duration is ever set on this
+ * Animator, then we use the duration that it was set to.
+ */
+ private long mDuration;
+
+ /**
+ * A flag indicating whether the duration has been set on this object. If not, we don't set
+ * the duration on the underlying Animator, but instead just use its default duration.
+ */
+ private boolean mDurationSet = false;
+
+ /**
+ * The startDelay of the underlying Animator object. By default, we don't set the startDelay
+ * on the Animator and just use its default startDelay. If the startDelay is ever set on this
+ * Animator, then we use the startDelay that it was set to.
+ */
+ private long mStartDelay = 0;
+
+ /**
+ * A flag indicating whether the startDelay has been set on this object. If not, we don't set
+ * the startDelay on the underlying Animator, but instead just use its default startDelay.
+ */
+ private boolean mStartDelaySet = false;
+
+ /**
+ * The interpolator of the underlying Animator object. By default, we don't set the interpolator
+ * on the Animator and just use its default interpolator. If the interpolator is ever set on
+ * this Animator, then we use the interpolator that it was set to.
+ */
+ private /*Time*/Interpolator mInterpolator;
+
+ /**
+ * A flag indicating whether the interpolator has been set on this object. If not, we don't set
+ * the interpolator on the underlying Animator, but instead just use its default interpolator.
+ */
+ private boolean mInterpolatorSet = false;
+
+ /**
+ * Listener for the lifecycle events of the underlying
+ */
+ private Animator.AnimatorListener mListener = null;
+
+ /**
+ * This listener is the mechanism by which the underlying Animator causes changes to the
+ * properties currently being animated, as well as the cleanup after an animation is
+ * complete.
+ */
+ private AnimatorEventListener mAnimatorEventListener = new AnimatorEventListener();
+
+ /**
+ * This list holds the properties that have been asked to animate. We allow the caller to
+ * request several animations prior to actually starting the underlying animator. This
+ * enables us to run one single animator to handle several properties in parallel. Each
+ * property is tossed onto the pending list until the animation actually starts (which is
+ * done by posting it onto mView), at which time the pending list is cleared and the properties
+ * on that list are added to the list of properties associated with that animator.
+ */
+ ArrayList mPendingAnimations = new ArrayList();
+
+ /**
+ * Constants used to associate a property being requested and the mechanism used to set
+ * the property (this class calls directly into View to set the properties in question).
+ */
+ private static final int NONE = 0x0000;
+ private static final int TRANSLATION_X = 0x0001;
+ private static final int TRANSLATION_Y = 0x0002;
+ private static final int SCALE_X = 0x0004;
+ private static final int SCALE_Y = 0x0008;
+ private static final int ROTATION = 0x0010;
+ private static final int ROTATION_X = 0x0020;
+ private static final int ROTATION_Y = 0x0040;
+ private static final int X = 0x0080;
+ private static final int Y = 0x0100;
+ private static final int ALPHA = 0x0200;
+
+ private static final int TRANSFORM_MASK = TRANSLATION_X | TRANSLATION_Y | SCALE_X | SCALE_Y |
+ ROTATION | ROTATION_X | ROTATION_Y | X | Y;
+
+ /**
+ * The mechanism by which the user can request several properties that are then animated
+ * together works by posting this Runnable to start the underlying Animator. Every time
+ * a property animation is requested, we cancel any previous postings of the Runnable
+ * and re-post it. This means that we will only ever run the Runnable (and thus start the
+ * underlying animator) after the caller is done setting the properties that should be
+ * animated together.
+ */
+ private Runnable mAnimationStarter = new Runnable() {
+ @Override
+ public void run() {
+ startAnimation();
+ }
+ };
+
+ /**
+ * This class holds information about the overall animation being run on the set of
+ * properties. The mask describes which properties are being animated and the
+ * values holder is the list of all property/value objects.
+ */
+ private static class PropertyBundle {
+ int mPropertyMask;
+ ArrayList mNameValuesHolder;
+
+ PropertyBundle(int propertyMask, ArrayList nameValuesHolder) {
+ mPropertyMask = propertyMask;
+ mNameValuesHolder = nameValuesHolder;
+ }
+
+ /**
+ * Removes the given property from being animated as a part of this
+ * PropertyBundle. If the property was a part of this bundle, it returns
+ * true to indicate that it was, in fact, canceled. This is an indication
+ * to the caller that a cancellation actually occurred.
+ *
+ * @param propertyConstant The property whose cancellation is requested.
+ * @return true if the given property is a part of this bundle and if it
+ * has therefore been canceled.
+ */
+ boolean cancel(int propertyConstant) {
+ if ((mPropertyMask & propertyConstant) != 0 && mNameValuesHolder != null) {
+ int count = mNameValuesHolder.size();
+ for (int i = 0; i < count; ++i) {
+ NameValuesHolder nameValuesHolder = mNameValuesHolder.get(i);
+ if (nameValuesHolder.mNameConstant == propertyConstant) {
+ mNameValuesHolder.remove(i);
+ mPropertyMask &= ~propertyConstant;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * This list tracks the list of properties being animated by any particular animator.
+ * In most situations, there would only ever be one animator running at a time. But it is
+ * possible to request some properties to animate together, then while those properties
+ * are animating, to request some other properties to animate together. The way that
+ * works is by having this map associate the group of properties being animated with the
+ * animator handling the animation. On every update event for an Animator, we ask the
+ * map for the associated properties and set them accordingly.
+ */
+ private HashMap mAnimatorMap =
+ new HashMap();
+
+ /**
+ * This is the information we need to set each property during the animation.
+ * mNameConstant is used to set the appropriate field in View, and the from/delta
+ * values are used to calculate the animated value for a given animation fraction
+ * during the animation.
+ */
+ private static class NameValuesHolder {
+ int mNameConstant;
+ float mFromValue;
+ float mDeltaValue;
+ NameValuesHolder(int nameConstant, float fromValue, float deltaValue) {
+ mNameConstant = nameConstant;
+ mFromValue = fromValue;
+ mDeltaValue = deltaValue;
+ }
+ }
+
+ /**
+ * Constructor, called by View. This is private by design, as the user should only
+ * get a ViewPropertyAnimator by calling View.animate().
+ *
+ * @param view The View associated with this ViewPropertyAnimator
+ */
+ ViewPropertyAnimatorPreHC(View view) {
+ mView = new WeakReference(view);
+ mProxy = AnimatorProxy.wrap(view);
+ }
+
+ /**
+ * Sets the duration for the underlying animator that animates the requested properties.
+ * By default, the animator uses the default value for ValueAnimator. Calling this method
+ * will cause the declared value to be used instead.
+ * @param duration The length of ensuing property animations, in milliseconds. The value
+ * cannot be negative.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator setDuration(long duration) {
+ if (duration < 0) {
+ throw new IllegalArgumentException("Animators cannot have negative duration: " +
+ duration);
+ }
+ mDurationSet = true;
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Returns the current duration of property animations. If the duration was set on this
+ * object, that value is returned. Otherwise, the default value of the underlying Animator
+ * is returned.
+ *
+ * @see #setDuration(long)
+ * @return The duration of animations, in milliseconds.
+ */
+ public long getDuration() {
+ if (mDurationSet) {
+ return mDuration;
+ } else {
+ // Just return the default from ValueAnimator, since that's what we'd get if
+ // the value has not been set otherwise
+ return new ValueAnimator().getDuration();
+ }
+ }
+
+ @Override
+ public long getStartDelay() {
+ if (mStartDelaySet) {
+ return mStartDelay;
+ } else {
+ // Just return the default from ValueAnimator (0), since that's what we'd get if
+ // the value has not been set otherwise
+ return 0;
+ }
+ }
+
+ @Override
+ public ViewPropertyAnimator setStartDelay(long startDelay) {
+ if (startDelay < 0) {
+ throw new IllegalArgumentException("Animators cannot have negative duration: " +
+ startDelay);
+ }
+ mStartDelaySet = true;
+ mStartDelay = startDelay;
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator setInterpolator(/*Time*/Interpolator interpolator) {
+ mInterpolatorSet = true;
+ mInterpolator = interpolator;
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator setListener(Animator.AnimatorListener listener) {
+ mListener = listener;
+ return this;
+ }
+
+ @Override
+ public void start() {
+ startAnimation();
+ }
+
+ @Override
+ public void cancel() {
+ if (mAnimatorMap.size() > 0) {
+ HashMap mAnimatorMapCopy =
+ (HashMap)mAnimatorMap.clone();
+ Set animatorSet = mAnimatorMapCopy.keySet();
+ for (Animator runningAnim : animatorSet) {
+ runningAnim.cancel();
+ }
+ }
+ mPendingAnimations.clear();
+ View v = mView.get();
+ if (v != null) {
+ v.removeCallbacks(mAnimationStarter);
+ }
+ }
+
+ @Override
+ public ViewPropertyAnimator x(float value) {
+ animateProperty(X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator xBy(float value) {
+ animatePropertyBy(X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator y(float value) {
+ animateProperty(Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator yBy(float value) {
+ animatePropertyBy(Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotation(float value) {
+ animateProperty(ROTATION, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationBy(float value) {
+ animatePropertyBy(ROTATION, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationX(float value) {
+ animateProperty(ROTATION_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationXBy(float value) {
+ animatePropertyBy(ROTATION_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationY(float value) {
+ animateProperty(ROTATION_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator rotationYBy(float value) {
+ animatePropertyBy(ROTATION_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationX(float value) {
+ animateProperty(TRANSLATION_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationXBy(float value) {
+ animatePropertyBy(TRANSLATION_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationY(float value) {
+ animateProperty(TRANSLATION_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator translationYBy(float value) {
+ animatePropertyBy(TRANSLATION_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleX(float value) {
+ animateProperty(SCALE_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleXBy(float value) {
+ animatePropertyBy(SCALE_X, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleY(float value) {
+ animateProperty(SCALE_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator scaleYBy(float value) {
+ animatePropertyBy(SCALE_Y, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator alpha(float value) {
+ animateProperty(ALPHA, value);
+ return this;
+ }
+
+ @Override
+ public ViewPropertyAnimator alphaBy(float value) {
+ animatePropertyBy(ALPHA, value);
+ return this;
+ }
+
+ /**
+ * Starts the underlying Animator for a set of properties. We use a single animator that
+ * simply runs from 0 to 1, and then use that fractional value to set each property
+ * value accordingly.
+ */
+ private void startAnimation() {
+ ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
+ ArrayList nameValueList =
+ (ArrayList) mPendingAnimations.clone();
+ mPendingAnimations.clear();
+ int propertyMask = 0;
+ int propertyCount = nameValueList.size();
+ for (int i = 0; i < propertyCount; ++i) {
+ NameValuesHolder nameValuesHolder = nameValueList.get(i);
+ propertyMask |= nameValuesHolder.mNameConstant;
+ }
+ mAnimatorMap.put(animator, new PropertyBundle(propertyMask, nameValueList));
+ animator.addUpdateListener(mAnimatorEventListener);
+ animator.addListener(mAnimatorEventListener);
+ if (mStartDelaySet) {
+ animator.setStartDelay(mStartDelay);
+ }
+ if (mDurationSet) {
+ animator.setDuration(mDuration);
+ }
+ if (mInterpolatorSet) {
+ animator.setInterpolator(mInterpolator);
+ }
+ animator.start();
+ }
+
+ /**
+ * Utility function, called by the various x(), y(), etc. methods. This stores the
+ * constant name for the property along with the from/delta values that will be used to
+ * calculate and set the property during the animation. This structure is added to the
+ * pending animations, awaiting the eventual start() of the underlying animator. A
+ * Runnable is posted to start the animation, and any pending such Runnable is canceled
+ * (which enables us to end up starting just one animator for all of the properties
+ * specified at one time).
+ *
+ * @param constantName The specifier for the property being animated
+ * @param toValue The value to which the property will animate
+ */
+ private void animateProperty(int constantName, float toValue) {
+ float fromValue = getValue(constantName);
+ float deltaValue = toValue - fromValue;
+ animatePropertyBy(constantName, fromValue, deltaValue);
+ }
+
+ /**
+ * Utility function, called by the various xBy(), yBy(), etc. methods. This method is
+ * just like animateProperty(), except the value is an offset from the property's
+ * current value, instead of an absolute "to" value.
+ *
+ * @param constantName The specifier for the property being animated
+ * @param byValue The amount by which the property will change
+ */
+ private void animatePropertyBy(int constantName, float byValue) {
+ float fromValue = getValue(constantName);
+ animatePropertyBy(constantName, fromValue, byValue);
+ }
+
+ /**
+ * Utility function, called by animateProperty() and animatePropertyBy(), which handles the
+ * details of adding a pending animation and posting the request to start the animation.
+ *
+ * @param constantName The specifier for the property being animated
+ * @param startValue The starting value of the property
+ * @param byValue The amount by which the property will change
+ */
+ private void animatePropertyBy(int constantName, float startValue, float byValue) {
+ // First, cancel any existing animations on this property
+ if (mAnimatorMap.size() > 0) {
+ Animator animatorToCancel = null;
+ Set animatorSet = mAnimatorMap.keySet();
+ for (Animator runningAnim : animatorSet) {
+ PropertyBundle bundle = mAnimatorMap.get(runningAnim);
+ if (bundle.cancel(constantName)) {
+ // property was canceled - cancel the animation if it's now empty
+ // Note that it's safe to break out here because every new animation
+ // on a property will cancel a previous animation on that property, so
+ // there can only ever be one such animation running.
+ if (bundle.mPropertyMask == NONE) {
+ // the animation is no longer changing anything - cancel it
+ animatorToCancel = runningAnim;
+ break;
+ }
+ }
+ }
+ if (animatorToCancel != null) {
+ animatorToCancel.cancel();
+ }
+ }
+
+ NameValuesHolder nameValuePair = new NameValuesHolder(constantName, startValue, byValue);
+ mPendingAnimations.add(nameValuePair);
+ View v = mView.get();
+ if (v != null) {
+ v.removeCallbacks(mAnimationStarter);
+ v.post(mAnimationStarter);
+ }
+ }
+
+ /**
+ * This method handles setting the property values directly in the View object's fields.
+ * propertyConstant tells it which property should be set, value is the value to set
+ * the property to.
+ *
+ * @param propertyConstant The property to be set
+ * @param value The value to set the property to
+ */
+ private void setValue(int propertyConstant, float value) {
+ //final View.TransformationInfo info = mView.mTransformationInfo;
+ switch (propertyConstant) {
+ case TRANSLATION_X:
+ //info.mTranslationX = value;
+ mProxy.setTranslationX(value);
+ break;
+ case TRANSLATION_Y:
+ //info.mTranslationY = value;
+ mProxy.setTranslationY(value);
+ break;
+ case ROTATION:
+ //info.mRotation = value;
+ mProxy.setRotation(value);
+ break;
+ case ROTATION_X:
+ //info.mRotationX = value;
+ mProxy.setRotationX(value);
+ break;
+ case ROTATION_Y:
+ //info.mRotationY = value;
+ mProxy.setRotationY(value);
+ break;
+ case SCALE_X:
+ //info.mScaleX = value;
+ mProxy.setScaleX(value);
+ break;
+ case SCALE_Y:
+ //info.mScaleY = value;
+ mProxy.setScaleY(value);
+ break;
+ case X:
+ //info.mTranslationX = value - mView.mLeft;
+ mProxy.setX(value);
+ break;
+ case Y:
+ //info.mTranslationY = value - mView.mTop;
+ mProxy.setY(value);
+ break;
+ case ALPHA:
+ //info.mAlpha = value;
+ mProxy.setAlpha(value);
+ break;
+ }
+ }
+
+ /**
+ * This method gets the value of the named property from the View object.
+ *
+ * @param propertyConstant The property whose value should be returned
+ * @return float The value of the named property
+ */
+ private float getValue(int propertyConstant) {
+ //final View.TransformationInfo info = mView.mTransformationInfo;
+ switch (propertyConstant) {
+ case TRANSLATION_X:
+ //return info.mTranslationX;
+ return mProxy.getTranslationX();
+ case TRANSLATION_Y:
+ //return info.mTranslationY;
+ return mProxy.getTranslationY();
+ case ROTATION:
+ //return info.mRotation;
+ return mProxy.getRotation();
+ case ROTATION_X:
+ //return info.mRotationX;
+ return mProxy.getRotationX();
+ case ROTATION_Y:
+ //return info.mRotationY;
+ return mProxy.getRotationY();
+ case SCALE_X:
+ //return info.mScaleX;
+ return mProxy.getScaleX();
+ case SCALE_Y:
+ //return info.mScaleY;
+ return mProxy.getScaleY();
+ case X:
+ //return mView.mLeft + info.mTranslationX;
+ return mProxy.getX();
+ case Y:
+ //return mView.mTop + info.mTranslationY;
+ return mProxy.getY();
+ case ALPHA:
+ //return info.mAlpha;
+ return mProxy.getAlpha();
+ }
+ return 0;
+ }
+
+ /**
+ * Utility class that handles the various Animator events. The only ones we care
+ * about are the end event (which we use to clean up the animator map when an animator
+ * finishes) and the update event (which we use to calculate the current value of each
+ * property and then set it on the view object).
+ */
+ private class AnimatorEventListener
+ implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationStart(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationCancel(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationRepeat(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationEnd(animation);
+ }
+ mAnimatorMap.remove(animation);
+ // If the map is empty, it means all animation are done or canceled, so the listener
+ // isn't needed anymore. Not nulling it would cause it to leak any objects used in
+ // its implementation
+ if (mAnimatorMap.isEmpty()) {
+ mListener = null;
+ }
+ }
+
+ /**
+ * Calculate the current value for each property and set it on the view. Invalidate
+ * the view object appropriately, depending on which properties are being animated.
+ *
+ * @param animation The animator associated with the properties that need to be
+ * set. This animator holds the animation fraction which we will use to calculate
+ * the current value of each property.
+ */
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ // alpha requires slightly different treatment than the other (transform) properties.
+ // The logic in setAlpha() is not simply setting mAlpha, plus the invalidation
+ // logic is dependent on how the view handles an internal call to onSetAlpha().
+ // We track what kinds of properties are set, and how alpha is handled when it is
+ // set, and perform the invalidation steps appropriately.
+ //boolean alphaHandled = false;
+ //mView.invalidateParentCaches();
+ float fraction = animation.getAnimatedFraction();
+ PropertyBundle propertyBundle = mAnimatorMap.get(animation);
+ int propertyMask = propertyBundle.mPropertyMask;
+ if ((propertyMask & TRANSFORM_MASK) != 0) {
+ View v = mView.get();
+ if (v != null) {
+ v.invalidate(/*false*/);
+ }
+ }
+ ArrayList valueList = propertyBundle.mNameValuesHolder;
+ if (valueList != null) {
+ int count = valueList.size();
+ for (int i = 0; i < count; ++i) {
+ NameValuesHolder values = valueList.get(i);
+ float value = values.mFromValue + fraction * values.mDeltaValue;
+ //if (values.mNameConstant == ALPHA) {
+ // alphaHandled = mView.setAlphaNoInvalidation(value);
+ //} else {
+ setValue(values.mNameConstant, value);
+ //}
+ }
+ }
+ /*if ((propertyMask & TRANSFORM_MASK) != 0) {
+ mView.mTransformationInfo.mMatrixDirty = true;
+ mView.mPrivateFlags |= View.DRAWN; // force another invalidation
+ }*/
+ // invalidate(false) in all cases except if alphaHandled gets set to true
+ // via the call to setAlphaNoInvalidation(), above
+ View v = mView.get();
+ if (v != null) {
+ v.invalidate(/*alphaHandled*/);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/nineoldandroids/view/animation/AnimatorProxy.java b/app/src/main/java/com/nineoldandroids/view/animation/AnimatorProxy.java
new file mode 100644
index 000000000..ef3dda267
--- /dev/null
+++ b/app/src/main/java/com/nineoldandroids/view/animation/AnimatorProxy.java
@@ -0,0 +1,322 @@
+package com.nineoldandroids.view.animation;
+
+import android.graphics.Camera;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.os.Build;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
+
+/**
+ * A proxy class to allow for modifying post-3.0 view properties on all pre-3.0
+ * platforms. DO NOT wrap your views with this class if you
+ * are using {@code ObjectAnimator} as it will handle that itself.
+ */
+public final class AnimatorProxy extends Animation {
+ /** Whether or not the current running platform needs to be proxied. */
+ public static final boolean NEEDS_PROXY = Integer.valueOf(Build.VERSION.SDK).intValue() < Build.VERSION_CODES.HONEYCOMB;
+
+ private static final WeakHashMap PROXIES =
+ new WeakHashMap();
+
+ /**
+ * Create a proxy to allow for modifying post-3.0 view properties on all
+ * pre-3.0 platforms. DO NOT wrap your views if you are
+ * using {@code ObjectAnimator} as it will handle that itself.
+ *
+ * @param view View to wrap.
+ * @return Proxy to post-3.0 properties.
+ */
+ public static AnimatorProxy wrap(View view) {
+ AnimatorProxy proxy = PROXIES.get(view);
+ // This checks if the proxy already exists and whether it still is the animation of the given view
+ if (proxy == null || proxy != view.getAnimation()) {
+ proxy = new AnimatorProxy(view);
+ PROXIES.put(view, proxy);
+ }
+ return proxy;
+ }
+
+ private final WeakReference mView;
+ private final Camera mCamera = new Camera();
+ private boolean mHasPivot;
+
+ private float mAlpha = 1;
+ private float mPivotX;
+ private float mPivotY;
+ private float mRotationX;
+ private float mRotationY;
+ private float mRotationZ;
+ private float mScaleX = 1;
+ private float mScaleY = 1;
+ private float mTranslationX;
+ private float mTranslationY;
+
+ private final RectF mBefore = new RectF();
+ private final RectF mAfter = new RectF();
+ private final Matrix mTempMatrix = new Matrix();
+
+ private AnimatorProxy(View view) {
+ setDuration(0); //perform transformation immediately
+ setFillAfter(true); //persist transformation beyond duration
+ view.setAnimation(this);
+ mView = new WeakReference(view);
+ }
+
+ public float getAlpha() {
+ return mAlpha;
+ }
+ public void setAlpha(float alpha) {
+ if (mAlpha != alpha) {
+ mAlpha = alpha;
+ View view = mView.get();
+ if (view != null) {
+ view.invalidate();
+ }
+ }
+ }
+ public float getPivotX() {
+ return mPivotX;
+ }
+ public void setPivotX(float pivotX) {
+ if (!mHasPivot || mPivotX != pivotX) {
+ prepareForUpdate();
+ mHasPivot = true;
+ mPivotX = pivotX;
+ invalidateAfterUpdate();
+ }
+ }
+ public float getPivotY() {
+ return mPivotY;
+ }
+ public void setPivotY(float pivotY) {
+ if (!mHasPivot || mPivotY != pivotY) {
+ prepareForUpdate();
+ mHasPivot = true;
+ mPivotY = pivotY;
+ invalidateAfterUpdate();
+ }
+ }
+ public float getRotation() {
+ return mRotationZ;
+ }
+ public void setRotation(float rotation) {
+ if (mRotationZ != rotation) {
+ prepareForUpdate();
+ mRotationZ = rotation;
+ invalidateAfterUpdate();
+ }
+ }
+ public float getRotationX() {
+ return mRotationX;
+ }
+ public void setRotationX(float rotationX) {
+ if (mRotationX != rotationX) {
+ prepareForUpdate();
+ mRotationX = rotationX;
+ invalidateAfterUpdate();
+ }
+ }
+ public float getRotationY() {
+ return mRotationY;
+ }
+
+ public void setRotationY(float rotationY) {
+ if (mRotationY != rotationY) {
+ prepareForUpdate();
+ mRotationY = rotationY;
+ invalidateAfterUpdate();
+ }
+ }
+ public float getScaleX() {
+ return mScaleX;
+ }
+ public void setScaleX(float scaleX) {
+ if (mScaleX != scaleX) {
+ prepareForUpdate();
+ mScaleX = scaleX;
+ invalidateAfterUpdate();
+ }
+ }
+ public float getScaleY() {
+ return mScaleY;
+ }
+ public void setScaleY(float scaleY) {
+ if (mScaleY != scaleY) {
+ prepareForUpdate();
+ mScaleY = scaleY;
+ invalidateAfterUpdate();
+ }
+ }
+ public int getScrollX() {
+ View view = mView.get();
+ if (view == null) {
+ return 0;
+ }
+ return view.getScrollX();
+ }
+ public void setScrollX(int value) {
+ View view = mView.get();
+ if (view != null) {
+ view.scrollTo(value, view.getScrollY());
+ }
+ }
+ public int getScrollY() {
+ View view = mView.get();
+ if (view == null) {
+ return 0;
+ }
+ return view.getScrollY();
+ }
+ public void setScrollY(int value) {
+ View view = mView.get();
+ if (view != null) {
+ view.scrollTo(view.getScrollX(), value);
+ }
+ }
+
+ public float getTranslationX() {
+ return mTranslationX;
+ }
+ public void setTranslationX(float translationX) {
+ if (mTranslationX != translationX) {
+ prepareForUpdate();
+ mTranslationX = translationX;
+ invalidateAfterUpdate();
+ }
+ }
+ public float getTranslationY() {
+ return mTranslationY;
+ }
+ public void setTranslationY(float translationY) {
+ if (mTranslationY != translationY) {
+ prepareForUpdate();
+ mTranslationY = translationY;
+ invalidateAfterUpdate();
+ }
+ }
+ public float getX() {
+ View view = mView.get();
+ if (view == null) {
+ return 0;
+ }
+ return view.getLeft() + mTranslationX;
+ }
+ public void setX(float x) {
+ View view = mView.get();
+ if (view != null) {
+ setTranslationX(x - view.getLeft());
+ }
+ }
+ public float getY() {
+ View view = mView.get();
+ if (view == null) {
+ return 0;
+ }
+ return view.getTop() + mTranslationY;
+ }
+ public void setY(float y) {
+ View view = mView.get();
+ if (view != null) {
+ setTranslationY(y - view.getTop());
+ }
+ }
+
+ private void prepareForUpdate() {
+ View view = mView.get();
+ if (view != null) {
+ computeRect(mBefore, view);
+ }
+ }
+ private void invalidateAfterUpdate() {
+ View view = mView.get();
+ if (view == null || view.getParent() == null) {
+ return;
+ }
+
+ final RectF after = mAfter;
+ computeRect(after, view);
+ after.union(mBefore);
+
+ ((View)view.getParent()).invalidate(
+ (int) Math.floor(after.left),
+ (int) Math.floor(after.top),
+ (int) Math.ceil(after.right),
+ (int) Math.ceil(after.bottom));
+ }
+
+ private void computeRect(final RectF r, View view) {
+ // compute current rectangle according to matrix transformation
+ final float w = view.getWidth();
+ final float h = view.getHeight();
+
+ // use a rectangle at 0,0 to make sure we don't run into issues with scaling
+ r.set(0, 0, w, h);
+
+ final Matrix m = mTempMatrix;
+ m.reset();
+ transformMatrix(m, view);
+ mTempMatrix.mapRect(r);
+
+ r.offset(view.getLeft(), view.getTop());
+
+ // Straighten coords if rotations flipped them
+ if (r.right < r.left) {
+ final float f = r.right;
+ r.right = r.left;
+ r.left = f;
+ }
+ if (r.bottom < r.top) {
+ final float f = r.top;
+ r.top = r.bottom;
+ r.bottom = f;
+ }
+ }
+
+ private void transformMatrix(Matrix m, View view) {
+ final float w = view.getWidth();
+ final float h = view.getHeight();
+ final boolean hasPivot = mHasPivot;
+ final float pX = hasPivot ? mPivotX : w / 2f;
+ final float pY = hasPivot ? mPivotY : h / 2f;
+
+ final float rX = mRotationX;
+ final float rY = mRotationY;
+ final float rZ = mRotationZ;
+ if ((rX != 0) || (rY != 0) || (rZ != 0)) {
+ final Camera camera = mCamera;
+ camera.save();
+ camera.rotateX(rX);
+ camera.rotateY(rY);
+ camera.rotateZ(-rZ);
+ camera.getMatrix(m);
+ camera.restore();
+ m.preTranslate(-pX, -pY);
+ m.postTranslate(pX, pY);
+ }
+
+ final float sX = mScaleX;
+ final float sY = mScaleY;
+ if ((sX != 1.0f) || (sY != 1.0f)) {
+ m.postScale(sX, sY);
+ final float sPX = -(pX / w) * ((sX * w) - w);
+ final float sPY = -(pY / h) * ((sY * h) - h);
+ m.postTranslate(sPX, sPY);
+ }
+
+ m.postTranslate(mTranslationX, mTranslationY);
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ View view = mView.get();
+ if (view != null) {
+ t.setAlpha(mAlpha);
+ transformMatrix(t.getMatrix(), view);
+ }
+ }
+}
diff --git a/app/src/main/res/layout/widget_float_label_edit_text.xml b/app/src/main/res/layout/widget_float_label_edit_text.xml
new file mode 100644
index 000000000..180374aea
--- /dev/null
+++ b/app/src/main/res/layout/widget_float_label_edit_text.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 4bfd46471..78ddc8f47 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -28,4 +28,13 @@
+
+
+
+
+
+
+
+
+