summaryrefslogtreecommitdiff
path: root/android/animation
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
committerJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
commit10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch)
tree8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/animation
parent677516fb6b6f207d373984757d3d9450474b6b00 (diff)
downloadandroid-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \ --bid 4335822 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4335822.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/animation')
-rw-r--r--android/animation/AnimationHandler.java317
-rw-r--r--android/animation/AnimationThread.java176
-rw-r--r--android/animation/Animator.java676
-rw-r--r--android/animation/AnimatorInflater.java1082
-rw-r--r--android/animation/AnimatorListenerAdapter.java68
-rw-r--r--android/animation/AnimatorSet.java2091
-rw-r--r--android/animation/ArgbEvaluator.java90
-rw-r--r--android/animation/BidirectionalTypeConverter.java73
-rw-r--r--android/animation/FloatArrayEvaluator.java77
-rw-r--r--android/animation/FloatEvaluator.java42
-rw-r--r--android/animation/FloatKeyframeSet.java119
-rw-r--r--android/animation/IntArrayEvaluator.java75
-rw-r--r--android/animation/IntEvaluator.java42
-rw-r--r--android/animation/IntKeyframeSet.java118
-rw-r--r--android/animation/Keyframe.java388
-rw-r--r--android/animation/KeyframeSet.java257
-rw-r--r--android/animation/Keyframes.java89
-rw-r--r--android/animation/LayoutTransition.java1541
-rw-r--r--android/animation/ObjectAnimator.java1017
-rw-r--r--android/animation/PathKeyframes.java251
-rw-r--r--android/animation/PointFEvaluator.java83
-rw-r--r--android/animation/PropertyValuesHolder.java1729
-rw-r--r--android/animation/PropertyValuesHolder_Delegate.java195
-rw-r--r--android/animation/RectEvaluator.java84
-rw-r--r--android/animation/RevealAnimator.java46
-rw-r--r--android/animation/StateListAnimator.java333
-rw-r--r--android/animation/TimeAnimator.java99
-rw-r--r--android/animation/TimeInterpolator.java38
-rw-r--r--android/animation/TypeConverter.java56
-rw-r--r--android/animation/TypeEvaluator.java44
-rw-r--r--android/animation/ValueAnimator.java1651
31 files changed, 12947 insertions, 0 deletions
diff --git a/android/animation/AnimationHandler.java b/android/animation/AnimationHandler.java
new file mode 100644
index 00000000..260323fe
--- /dev/null
+++ b/android/animation/AnimationHandler.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.view.Choreographer;
+
+import java.util.ArrayList;
+
+/**
+ * This custom, static handler handles the timing pulse that is shared by all active
+ * ValueAnimators. This approach ensures that the setting of animation values will happen on the
+ * same thread that animations start on, and that all animations will share the same times for
+ * calculating their values, which makes synchronizing animations possible.
+ *
+ * The handler uses the Choreographer by default for doing periodic callbacks. A custom
+ * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
+ * may be independent of UI frame update. This could be useful in testing.
+ *
+ * @hide
+ */
+public class AnimationHandler {
+ /**
+ * Internal per-thread collections used to avoid set collisions as animations start and end
+ * while being processed.
+ * @hide
+ */
+ private final ArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
+ new ArrayMap<>();
+ private final ArrayList<AnimationFrameCallback> mAnimationCallbacks =
+ new ArrayList<>();
+ private final ArrayList<AnimationFrameCallback> mCommitCallbacks =
+ new ArrayList<>();
+ private AnimationFrameCallbackProvider mProvider;
+
+ private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ doAnimationFrame(getProvider().getFrameTime());
+ if (mAnimationCallbacks.size() > 0) {
+ getProvider().postFrameCallback(this);
+ }
+ }
+ };
+
+ public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
+ private boolean mListDirty = false;
+
+ public static AnimationHandler getInstance() {
+ if (sAnimatorHandler.get() == null) {
+ sAnimatorHandler.set(new AnimationHandler());
+ }
+ return sAnimatorHandler.get();
+ }
+
+ /**
+ * By default, the Choreographer is used to provide timing for frame callbacks. A custom
+ * provider can be used here to provide different timing pulse.
+ */
+ public void setProvider(AnimationFrameCallbackProvider provider) {
+ if (provider == null) {
+ mProvider = new MyFrameCallbackProvider();
+ } else {
+ mProvider = provider;
+ }
+ }
+
+ private AnimationFrameCallbackProvider getProvider() {
+ if (mProvider == null) {
+ mProvider = new MyFrameCallbackProvider();
+ }
+ return mProvider;
+ }
+
+ /**
+ * Register to get a callback on the next frame after the delay.
+ */
+ public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
+ if (mAnimationCallbacks.size() == 0) {
+ getProvider().postFrameCallback(mFrameCallback);
+ }
+ if (!mAnimationCallbacks.contains(callback)) {
+ mAnimationCallbacks.add(callback);
+ }
+
+ if (delay > 0) {
+ mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
+ }
+ }
+
+ /**
+ * Register to get a one shot callback for frame commit timing. Frame commit timing is the
+ * time *after* traversals are done, as opposed to the animation frame timing, which is
+ * before any traversals. This timing can be used to adjust the start time of an animation
+ * when expensive traversals create big delta between the animation frame timing and the time
+ * that animation is first shown on screen.
+ *
+ * Note this should only be called when the animation has already registered to receive
+ * animation frame callbacks. This callback will be guaranteed to happen *after* the next
+ * animation frame callback.
+ */
+ public void addOneShotCommitCallback(final AnimationFrameCallback callback) {
+ if (!mCommitCallbacks.contains(callback)) {
+ mCommitCallbacks.add(callback);
+ }
+ }
+
+ /**
+ * Removes the given callback from the list, so it will no longer be called for frame related
+ * timing.
+ */
+ public void removeCallback(AnimationFrameCallback callback) {
+ mCommitCallbacks.remove(callback);
+ mDelayedCallbackStartTime.remove(callback);
+ int id = mAnimationCallbacks.indexOf(callback);
+ if (id >= 0) {
+ mAnimationCallbacks.set(id, null);
+ mListDirty = true;
+ }
+ }
+
+ private void doAnimationFrame(long frameTime) {
+ long currentTime = SystemClock.uptimeMillis();
+ final int size = mAnimationCallbacks.size();
+ for (int i = 0; i < size; i++) {
+ final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
+ if (callback == null) {
+ continue;
+ }
+ if (isCallbackDue(callback, currentTime)) {
+ callback.doAnimationFrame(frameTime);
+ if (mCommitCallbacks.contains(callback)) {
+ getProvider().postCommitCallback(new Runnable() {
+ @Override
+ public void run() {
+ commitAnimationFrame(callback, getProvider().getFrameTime());
+ }
+ });
+ }
+ }
+ }
+ cleanUpList();
+ }
+
+ private void commitAnimationFrame(AnimationFrameCallback callback, long frameTime) {
+ if (!mDelayedCallbackStartTime.containsKey(callback) &&
+ mCommitCallbacks.contains(callback)) {
+ callback.commitAnimationFrame(frameTime);
+ mCommitCallbacks.remove(callback);
+ }
+ }
+
+ /**
+ * Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay
+ * so that they can start getting frame callbacks.
+ *
+ * @return true if they have passed the initial delay or have no delay, false otherwise.
+ */
+ private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
+ Long startTime = mDelayedCallbackStartTime.get(callback);
+ if (startTime == null) {
+ return true;
+ }
+ if (startTime < currentTime) {
+ mDelayedCallbackStartTime.remove(callback);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Return the number of callbacks that have registered for frame callbacks.
+ */
+ public static int getAnimationCount() {
+ AnimationHandler handler = sAnimatorHandler.get();
+ if (handler == null) {
+ return 0;
+ }
+ return handler.getCallbackSize();
+ }
+
+ public static void setFrameDelay(long delay) {
+ getInstance().getProvider().setFrameDelay(delay);
+ }
+
+ public static long getFrameDelay() {
+ return getInstance().getProvider().getFrameDelay();
+ }
+
+ void autoCancelBasedOn(ObjectAnimator objectAnimator) {
+ for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
+ AnimationFrameCallback cb = mAnimationCallbacks.get(i);
+ if (cb == null) {
+ continue;
+ }
+ if (objectAnimator.shouldAutoCancel(cb)) {
+ ((Animator) mAnimationCallbacks.get(i)).cancel();
+ }
+ }
+ }
+
+ private void cleanUpList() {
+ if (mListDirty) {
+ for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
+ if (mAnimationCallbacks.get(i) == null) {
+ mAnimationCallbacks.remove(i);
+ }
+ }
+ mListDirty = false;
+ }
+ }
+
+ private int getCallbackSize() {
+ int count = 0;
+ int size = mAnimationCallbacks.size();
+ for (int i = size - 1; i >= 0; i--) {
+ if (mAnimationCallbacks.get(i) != null) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Default provider of timing pulse that uses Choreographer for frame callbacks.
+ */
+ private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
+
+ final Choreographer mChoreographer = Choreographer.getInstance();
+
+ @Override
+ public void postFrameCallback(Choreographer.FrameCallback callback) {
+ mChoreographer.postFrameCallback(callback);
+ }
+
+ @Override
+ public void postCommitCallback(Runnable runnable) {
+ mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
+ }
+
+ @Override
+ public long getFrameTime() {
+ return mChoreographer.getFrameTime();
+ }
+
+ @Override
+ public long getFrameDelay() {
+ return Choreographer.getFrameDelay();
+ }
+
+ @Override
+ public void setFrameDelay(long delay) {
+ Choreographer.setFrameDelay(delay);
+ }
+ }
+
+ /**
+ * Callbacks that receives notifications for animation timing and frame commit timing.
+ */
+ interface AnimationFrameCallback {
+ /**
+ * Run animation based on the frame time.
+ * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
+ * base.
+ * @return if the animation has finished.
+ */
+ boolean doAnimationFrame(long frameTime);
+
+ /**
+ * This notifies the callback of frame commit time. Frame commit time is the time after
+ * traversals happen, as opposed to the normal animation frame time that is before
+ * traversals. This is used to compensate expensive traversals that happen as the
+ * animation starts. When traversals take a long time to complete, the rendering of the
+ * initial frame will be delayed (by a long time). But since the startTime of the
+ * animation is set before the traversal, by the time of next frame, a lot of time would
+ * have passed since startTime was set, the animation will consequently skip a few frames
+ * to respect the new frameTime. By having the commit time, we can adjust the start time to
+ * when the first frame was drawn (after any expensive traversals) so that no frames
+ * will be skipped.
+ *
+ * @param frameTime The frame time after traversals happen, if any, in the
+ * {@link SystemClock#uptimeMillis()} time base.
+ */
+ void commitAnimationFrame(long frameTime);
+ }
+
+ /**
+ * The intention for having this interface is to increase the testability of ValueAnimator.
+ * Specifically, we can have a custom implementation of the interface below and provide
+ * timing pulse without using Choreographer. That way we could use any arbitrary interval for
+ * our timing pulse in the tests.
+ *
+ * @hide
+ */
+ public interface AnimationFrameCallbackProvider {
+ void postFrameCallback(Choreographer.FrameCallback callback);
+ void postCommitCallback(Runnable runnable);
+ long getFrameTime();
+ long getFrameDelay();
+ void setFrameDelay(long delay);
+ }
+}
diff --git a/android/animation/AnimationThread.java b/android/animation/AnimationThread.java
new file mode 100644
index 00000000..ce2aec79
--- /dev/null
+++ b/android/animation/AnimationThread.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import com.android.ide.common.rendering.api.IAnimationListener;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.Result.Status;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.RenderSessionImpl;
+
+import android.os.Handler;
+import android.os.Handler_Delegate;
+import android.os.Message;
+
+import java.util.PriorityQueue;
+import java.util.Queue;
+
+/**
+ * Abstract animation thread.
+ * <p/>
+ * This does not actually start an animation, instead it fakes a looper that will play whatever
+ * animation is sending messages to its own {@link Handler}.
+ * <p/>
+ * Classes should implement {@link #preAnimation()} and {@link #postAnimation()}.
+ * <p/>
+ * If {@link #preAnimation()} does not start an animation somehow then the thread doesn't do
+ * anything.
+ *
+ */
+public abstract class AnimationThread extends Thread {
+
+ private static class MessageBundle implements Comparable<MessageBundle> {
+ final Handler mTarget;
+ final Message mMessage;
+ final long mUptimeMillis;
+
+ MessageBundle(Handler target, Message message, long uptimeMillis) {
+ mTarget = target;
+ mMessage = message;
+ mUptimeMillis = uptimeMillis;
+ }
+
+ @Override
+ public int compareTo(MessageBundle bundle) {
+ if (mUptimeMillis < bundle.mUptimeMillis) {
+ return -1;
+ }
+ return 1;
+ }
+ }
+
+ private final RenderSessionImpl mSession;
+
+ private Queue<MessageBundle> mQueue = new PriorityQueue<MessageBundle>();
+ private final IAnimationListener mListener;
+
+ public AnimationThread(RenderSessionImpl scene, String threadName,
+ IAnimationListener listener) {
+ super(threadName);
+ mSession = scene;
+ mListener = listener;
+ }
+
+ public abstract Result preAnimation();
+ public abstract void postAnimation();
+
+ @Override
+ public void run() {
+ Bridge.prepareThread();
+ try {
+ /* FIXME: The ANIMATION_FRAME message no longer exists. Instead, the
+ * animation timing loop is completely based on a Choreographer objects
+ * that schedules animation and drawing frames. The animation handler is
+ * no longer even a handler; it is just a Runnable enqueued on the Choreographer.
+ Handler_Delegate.setCallback(new IHandlerCallback() {
+ @Override
+ public void sendMessageAtTime(Handler handler, Message msg, long uptimeMillis) {
+ if (msg.what == ValueAnimator.ANIMATION_START ||
+ msg.what == ValueAnimator.ANIMATION_FRAME) {
+ mQueue.add(new MessageBundle(handler, msg, uptimeMillis));
+ } else {
+ // just ignore.
+ }
+ }
+ });
+ */
+
+ // call out to the pre-animation work, which should start an animation or more.
+ Result result = preAnimation();
+ if (result.isSuccess() == false) {
+ mListener.done(result);
+ }
+
+ // loop the animation
+ RenderSession session = mSession.getSession();
+ do {
+ // check early.
+ if (mListener.isCanceled()) {
+ break;
+ }
+
+ // get the next message.
+ MessageBundle bundle = mQueue.poll();
+ if (bundle == null) {
+ break;
+ }
+
+ // sleep enough for this bundle to be on time
+ long currentTime = System.currentTimeMillis();
+ if (currentTime < bundle.mUptimeMillis) {
+ try {
+ sleep(bundle.mUptimeMillis - currentTime);
+ } catch (InterruptedException e) {
+ // FIXME log/do something/sleep again?
+ e.printStackTrace();
+ }
+ }
+
+ // check after sleeping.
+ if (mListener.isCanceled()) {
+ break;
+ }
+
+ // ready to do the work, acquire the scene.
+ result = mSession.acquire(250);
+ if (result.isSuccess() == false) {
+ mListener.done(result);
+ return;
+ }
+
+ // process the bundle. If the animation is not finished, this will enqueue
+ // the next message, so mQueue will have another one.
+ try {
+ // check after acquiring in case it took a while.
+ if (mListener.isCanceled()) {
+ break;
+ }
+
+ bundle.mTarget.handleMessage(bundle.mMessage);
+ if (mSession.render(false /*freshRender*/).isSuccess()) {
+ mListener.onNewFrame(session);
+ }
+ } finally {
+ mSession.release();
+ }
+ } while (mListener.isCanceled() == false && mQueue.size() > 0);
+
+ mListener.done(Status.SUCCESS.createResult());
+
+ } catch (Throwable throwable) {
+ // can't use Bridge.getLog() as the exception might be thrown outside
+ // of an acquire/release block.
+ mListener.done(Status.ERROR_UNKNOWN.createResult("Error playing animation", throwable));
+
+ } finally {
+ postAnimation();
+ Handler_Delegate.setCallback(null);
+ Bridge.cleanupThread();
+ }
+ }
+}
diff --git a/android/animation/Animator.java b/android/animation/Animator.java
new file mode 100644
index 00000000..4ebcc446
--- /dev/null
+++ b/android/animation/Animator.java
@@ -0,0 +1,676 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ConstantState;
+
+import java.util.ArrayList;
+
+/**
+ * This is the superclass for classes which provide basic support for animations which can be
+ * started, ended, and have <code>AnimatorListeners</code> added to them.
+ */
+public abstract class Animator implements Cloneable {
+
+ /**
+ * The value used to indicate infinite duration (e.g. when Animators repeat infinitely).
+ */
+ public static final long DURATION_INFINITE = -1;
+ /**
+ * The set of listeners to be sent events through the life of an animation.
+ */
+ ArrayList<AnimatorListener> mListeners = null;
+
+ /**
+ * The set of listeners to be sent pause/resume events through the life
+ * of an animation.
+ */
+ ArrayList<AnimatorPauseListener> mPauseListeners = null;
+
+ /**
+ * Whether this animator is currently in a paused state.
+ */
+ boolean mPaused = false;
+
+ /**
+ * A set of flags which identify the type of configuration changes that can affect this
+ * Animator. Used by the Animator cache.
+ */
+ @Config int mChangingConfigurations = 0;
+
+ /**
+ * If this animator is inflated from a constant state, keep a reference to it so that
+ * ConstantState will not be garbage collected until this animator is collected
+ */
+ private AnimatorConstantState mConstantState;
+
+ /**
+ * Starts this animation. If the animation has a nonzero startDelay, the animation will start
+ * running after that delay elapses. A non-delayed animation will have its initial
+ * value(s) set immediately, followed by calls to
+ * {@link AnimatorListener#onAnimationStart(Animator)} for any listeners of this animator.
+ *
+ * <p>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.</p>
+ *
+ */
+ public void start() {
+ }
+
+ /**
+ * Cancels the animation. Unlike {@link #end()}, <code>cancel()</code> causes the animation to
+ * stop in its tracks, sending an
+ * {@link android.animation.Animator.AnimatorListener#onAnimationCancel(Animator)} to
+ * its listeners, followed by an
+ * {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator)} message.
+ *
+ * <p>This method must be called on the thread that is running the animation.</p>
+ */
+ public void cancel() {
+ }
+
+ /**
+ * Ends the animation. This causes the animation to assign the end value of the property being
+ * animated, then calling the
+ * {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator)} method on
+ * its listeners.
+ *
+ * <p>This method must be called on the thread that is running the animation.</p>
+ */
+ public void end() {
+ }
+
+ /**
+ * Pauses a running animation. This method should only be called on the same thread on
+ * which the animation was started. If the animation has not yet been {@link
+ * #isStarted() started} or has since ended, then the call is ignored. Paused
+ * animations can be resumed by calling {@link #resume()}.
+ *
+ * @see #resume()
+ * @see #isPaused()
+ * @see AnimatorPauseListener
+ */
+ public void pause() {
+ if (isStarted() && !mPaused) {
+ mPaused = true;
+ if (mPauseListeners != null) {
+ ArrayList<AnimatorPauseListener> tmpListeners =
+ (ArrayList<AnimatorPauseListener>) mPauseListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationPause(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * Resumes a paused animation, causing the animator to pick up where it left off
+ * when it was paused. This method should only be called on the same thread on
+ * which the animation was started. Calls to resume() on an animator that is
+ * not currently paused will be ignored.
+ *
+ * @see #pause()
+ * @see #isPaused()
+ * @see AnimatorPauseListener
+ */
+ public void resume() {
+ if (mPaused) {
+ mPaused = false;
+ if (mPauseListeners != null) {
+ ArrayList<AnimatorPauseListener> tmpListeners =
+ (ArrayList<AnimatorPauseListener>) mPauseListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationResume(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether this animator is currently in a paused state.
+ *
+ * @return True if the animator is currently paused, false otherwise.
+ *
+ * @see #pause()
+ * @see #resume()
+ */
+ public boolean isPaused() {
+ return mPaused;
+ }
+
+ /**
+ * The amount of time, in milliseconds, to delay processing the animation
+ * after {@link #start()} is called.
+ *
+ * @return the number of milliseconds to delay running the animation
+ */
+ public abstract long getStartDelay();
+
+ /**
+ * The amount of time, in milliseconds, to delay processing the animation
+ * after {@link #start()} is called.
+
+ * @param startDelay The amount of the delay, in milliseconds
+ */
+ public abstract void setStartDelay(long startDelay);
+
+ /**
+ * Sets the duration of the animation.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ */
+ public abstract Animator setDuration(long duration);
+
+ /**
+ * Gets the duration of the animation.
+ *
+ * @return The length of the animation, in milliseconds.
+ */
+ public abstract long getDuration();
+
+ /**
+ * Gets the total duration of the animation, accounting for animation sequences, start delay,
+ * and repeating. Return {@link #DURATION_INFINITE} if the duration is infinite.
+ *
+ * @return Total time an animation takes to finish, starting from the time {@link #start()}
+ * is called. {@link #DURATION_INFINITE} will be returned if the animation or any
+ * child animation repeats infinite times.
+ */
+ public long getTotalDuration() {
+ long duration = getDuration();
+ if (duration == DURATION_INFINITE) {
+ return DURATION_INFINITE;
+ } else {
+ return getStartDelay() + duration;
+ }
+ }
+
+ /**
+ * The time interpolator used in calculating the elapsed fraction of the
+ * 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
+ */
+ public abstract void setInterpolator(TimeInterpolator value);
+
+ /**
+ * Returns the timing interpolator that this animation uses.
+ *
+ * @return The timing interpolator for this animation.
+ */
+ public TimeInterpolator getInterpolator() {
+ return null;
+ }
+
+ /**
+ * Returns whether this Animator is currently running (having been started and gone past any
+ * initial startDelay period and not yet ended).
+ *
+ * @return Whether the Animator is running.
+ */
+ public abstract boolean isRunning();
+
+ /**
+ * Returns whether this Animator has been started and not yet ended. For reusable
+ * Animators (which most Animators are, apart from the one-shot animator produced by
+ * {@link android.view.ViewAnimationUtils#createCircularReveal(
+ * android.view.View, int, int, float, float) createCircularReveal()}),
+ * this state is a superset of {@link #isRunning()}, because an Animator with a
+ * nonzero {@link #getStartDelay() startDelay} will return true for {@link #isStarted()} during
+ * the delay phase, whereas {@link #isRunning()} will return true only after the delay phase
+ * is complete. Non-reusable animators will always return true after they have been
+ * started, because they cannot return to a non-started state.
+ *
+ * @return Whether the Animator has been started and not yet ended.
+ */
+ public boolean isStarted() {
+ // Default method returns value for isRunning(). Subclasses should override to return a
+ // real value.
+ return isRunning();
+ }
+
+ /**
+ * Adds a listener to the set of listeners that are sent events through the life of an
+ * animation, such as start, repeat, and end.
+ *
+ * @param listener the listener to be added to the current set of listeners for this animation.
+ */
+ public void addListener(AnimatorListener listener) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<AnimatorListener>();
+ }
+ mListeners.add(listener);
+ }
+
+ /**
+ * Removes a listener from the set listening to this animation.
+ *
+ * @param listener the listener to be removed from the current set of listeners for this
+ * animation.
+ */
+ public void removeListener(AnimatorListener listener) {
+ if (mListeners == null) {
+ return;
+ }
+ mListeners.remove(listener);
+ if (mListeners.size() == 0) {
+ mListeners = null;
+ }
+ }
+
+ /**
+ * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently
+ * listening for events on this <code>Animator</code> object.
+ *
+ * @return ArrayList<AnimatorListener> The set of listeners.
+ */
+ public ArrayList<AnimatorListener> getListeners() {
+ return mListeners;
+ }
+
+ /**
+ * Adds a pause listener to this animator.
+ *
+ * @param listener the listener to be added to the current set of pause listeners
+ * for this animation.
+ */
+ public void addPauseListener(AnimatorPauseListener listener) {
+ if (mPauseListeners == null) {
+ mPauseListeners = new ArrayList<AnimatorPauseListener>();
+ }
+ mPauseListeners.add(listener);
+ }
+
+ /**
+ * Removes a pause listener from the set listening to this animation.
+ *
+ * @param listener the listener to be removed from the current set of pause
+ * listeners for this animation.
+ */
+ public void removePauseListener(AnimatorPauseListener listener) {
+ if (mPauseListeners == null) {
+ return;
+ }
+ mPauseListeners.remove(listener);
+ if (mPauseListeners.size() == 0) {
+ mPauseListeners = null;
+ }
+ }
+
+ /**
+ * Removes all {@link #addListener(android.animation.Animator.AnimatorListener) listeners}
+ * and {@link #addPauseListener(android.animation.Animator.AnimatorPauseListener)
+ * pauseListeners} from this object.
+ */
+ public void removeAllListeners() {
+ if (mListeners != null) {
+ mListeners.clear();
+ mListeners = null;
+ }
+ if (mPauseListeners != null) {
+ mPauseListeners.clear();
+ mPauseListeners = null;
+ }
+ }
+
+ /**
+ * Return a mask of the configuration parameters for which this animator may change, requiring
+ * that it should be re-created from Resources. The default implementation returns whatever
+ * value was provided through setChangingConfigurations(int) or 0 by default.
+ *
+ * @return Returns a mask of the changing configuration parameters, as defined by
+ * {@link android.content.pm.ActivityInfo}.
+ * @see android.content.pm.ActivityInfo
+ * @hide
+ */
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations;
+ }
+
+ /**
+ * Set a mask of the configuration parameters for which this animator may change, requiring
+ * that it be re-created from resource.
+ *
+ * @param configs A mask of the changing configuration parameters, as
+ * defined by {@link android.content.pm.ActivityInfo}.
+ *
+ * @see android.content.pm.ActivityInfo
+ * @hide
+ */
+ public void setChangingConfigurations(@Config int configs) {
+ mChangingConfigurations = configs;
+ }
+
+ /**
+ * Sets the changing configurations value to the union of the current changing configurations
+ * and the provided configs.
+ * This method is called while loading the animator.
+ * @hide
+ */
+ public void appendChangingConfigurations(@Config int configs) {
+ mChangingConfigurations |= configs;
+ }
+
+ /**
+ * Return a {@link android.content.res.ConstantState} instance that holds the shared state of
+ * this Animator.
+ * <p>
+ * This constant state is used to create new instances of this animator when needed, instead
+ * of re-loading it from resources. Default implementation creates a new
+ * {@link AnimatorConstantState}. You can override this method to provide your custom logic or
+ * return null if you don't want this animator to be cached.
+ *
+ * @return The ConfigurationBoundResourceCache.BaseConstantState associated to this Animator.
+ * @see android.content.res.ConstantState
+ * @see #clone()
+ * @hide
+ */
+ public ConstantState<Animator> createConstantState() {
+ return new AnimatorConstantState(this);
+ }
+
+ @Override
+ public Animator clone() {
+ try {
+ final Animator anim = (Animator) super.clone();
+ if (mListeners != null) {
+ anim.mListeners = new ArrayList<AnimatorListener>(mListeners);
+ }
+ if (mPauseListeners != null) {
+ anim.mPauseListeners = new ArrayList<AnimatorPauseListener>(mPauseListeners);
+ }
+ 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 ValueAnimator}, but this method
+ * is on the superclass for the convenience of dealing generically with those subclasses
+ * that do handle targets.
+ * <p>
+ * <strong>Note:</strong> The target is stored as a weak reference internally to avoid leaking
+ * resources by having animators directly reference old targets. Therefore, you should
+ * ensure that animator targets always have a hard reference elsewhere.
+ *
+ * @param target The object being animated
+ */
+ public void setTarget(@Nullable 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");
+ }
+
+ // Pulse an animation frame into the animation.
+ boolean pulseAnimationFrame(long frameTime) {
+ // TODO: Need to find a better signal than this. There's a bug in SystemUI that's preventing
+ // returning !isStarted() from working.
+ return false;
+ }
+
+ /**
+ * Internal use only.
+ * This call starts the animation in regular or reverse direction without requiring them to
+ * register frame callbacks. The caller will be responsible for all the subsequent animation
+ * pulses. Specifically, the caller needs to call doAnimationFrame(...) for the animation on
+ * every frame.
+ *
+ * @param inReverse whether the animation should play in reverse direction
+ */
+ void startWithoutPulsing(boolean inReverse) {
+ if (inReverse) {
+ reverse();
+ } else {
+ start();
+ }
+ }
+
+ /**
+ * Internal use only.
+ * Skips the animation value to end/start, depending on whether the play direction is forward
+ * or backward.
+ *
+ * @param inReverse whether the end value is based on a reverse direction. If yes, this is
+ * equivalent to skip to start value in a forward playing direction.
+ */
+ void skipToEndValue(boolean inReverse) {}
+
+
+ /**
+ * Internal use only.
+ *
+ * Returns whether the animation has start/end values setup. For most of the animations, this
+ * should always be true. For ObjectAnimators, the start values are setup in the initialization
+ * of the animation.
+ */
+ boolean isInitialized() {
+ return true;
+ }
+
+ /**
+ * Internal use only.
+ */
+ void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {}
+
+ /**
+ * <p>An animation listener receives notifications from an animation.
+ * Notifications indicate animation related events, such as the end or the
+ * repetition of the animation.</p>
+ */
+ public static interface AnimatorListener {
+
+ /**
+ * <p>Notifies the start of the animation as well as the animation's overall play direction.
+ * This method's default behavior is to call {@link #onAnimationStart(Animator)}. This
+ * method can be overridden, though not required, to get the additional play direction info
+ * when an animation starts. Skipping calling super when overriding this method results in
+ * {@link #onAnimationStart(Animator)} not getting called.
+ *
+ * @param animation The started animation.
+ * @param isReverse Whether the animation is playing in reverse.
+ */
+ default void onAnimationStart(Animator animation, boolean isReverse) {
+ onAnimationStart(animation);
+ }
+
+ /**
+ * <p>Notifies the end of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.</p>
+ *
+ * <p>This method's default behavior is to call {@link #onAnimationEnd(Animator)}. This
+ * method can be overridden, though not required, to get the additional play direction info
+ * when an animation ends. Skipping calling super when overriding this method results in
+ * {@link #onAnimationEnd(Animator)} not getting called.
+ *
+ * @param animation The animation which reached its end.
+ * @param isReverse Whether the animation is playing in reverse.
+ */
+ default void onAnimationEnd(Animator animation, boolean isReverse) {
+ onAnimationEnd(animation);
+ }
+
+ /**
+ * <p>Notifies the start of the animation.</p>
+ *
+ * @param animation The started animation.
+ */
+ void onAnimationStart(Animator animation);
+
+ /**
+ * <p>Notifies the end of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.</p>
+ *
+ * @param animation The animation which reached its end.
+ */
+ void onAnimationEnd(Animator animation);
+
+ /**
+ * <p>Notifies the cancellation of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.</p>
+ *
+ * @param animation The animation which was canceled.
+ */
+ void onAnimationCancel(Animator animation);
+
+ /**
+ * <p>Notifies the repetition of the animation.</p>
+ *
+ * @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 {
+ /**
+ * <p>Notifies that the animation was paused.</p>
+ *
+ * @param animation The animaton being paused.
+ * @see #pause()
+ */
+ void onAnimationPause(Animator animation);
+
+ /**
+ * <p>Notifies that the animation was resumed, after being
+ * previously paused.</p>
+ *
+ * @param animation The animation being resumed.
+ * @see #resume()
+ */
+ void onAnimationResume(Animator animation);
+ }
+
+ /**
+ * <p>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.
+ *
+ * <p>Regardless of whether or not the animation runs asynchronously, all
+ * listener callbacks will be called on the UI thread.</p>
+ *
+ * <p>To be able to use this hint the following must be true:</p>
+ * <ol>
+ * <li>The animator is immutable while {@link #isStarted()} is true. Requests
+ * to change duration, delay, etc... may be ignored.</li>
+ * <li>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.</li>
+ * <li>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.</li>
+ * </ol>
+ * @hide
+ */
+ public void setAllowRunningAsynchronously(boolean mayRunAsync) {
+ // It is up to subclasses to support this, if they can.
+ }
+
+ /**
+ * Creates a {@link ConstantState} which holds changing configurations information associated
+ * with the given Animator.
+ * <p>
+ * When {@link #newInstance()} is called, default implementation clones the Animator.
+ */
+ private static class AnimatorConstantState extends ConstantState<Animator> {
+
+ final Animator mAnimator;
+ @Config int mChangingConf;
+
+ public AnimatorConstantState(Animator animator) {
+ mAnimator = animator;
+ // ensure a reference back to here so that constante state is not gc'ed.
+ mAnimator.mConstantState = this;
+ mChangingConf = mAnimator.getChangingConfigurations();
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConf;
+ }
+
+ @Override
+ public Animator newInstance() {
+ final Animator clone = mAnimator.clone();
+ clone.mConstantState = this;
+ return clone;
+ }
+ }
+}
diff --git a/android/animation/AnimatorInflater.java b/android/animation/AnimatorInflater.java
new file mode 100644
index 00000000..f69bbfd3
--- /dev/null
+++ b/android/animation/AnimatorInflater.java
@@ -0,0 +1,1082 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.animation;
+
+import android.annotation.AnimatorRes;
+import android.annotation.AnyRes;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ConfigurationBoundResourceCache;
+import android.content.res.ConstantState;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.PathParser;
+import android.util.StateSet;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.view.InflateException;
+import android.view.animation.AnimationUtils;
+import android.view.animation.BaseInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * This class is used to instantiate animator XML files into Animator objects.
+ * <p>
+ * For performance reasons, inflation relies heavily on pre-processing of
+ * XML files that is done at build time. Therefore, it is not currently possible
+ * to use this inflater with an XmlPullParser over a plain XML file at runtime;
+ * it only works with an XmlPullParser returned from a compiled resource (R.
+ * <em>something</em> file.)
+ */
+public class AnimatorInflater {
+ private static final String TAG = "AnimatorInflater";
+ /**
+ * These flags are used when parsing AnimatorSet objects
+ */
+ private static final int TOGETHER = 0;
+ private static final int SEQUENTIALLY = 1;
+
+ /**
+ * Enum values used in XML attributes to indicate the value for mValueType
+ */
+ private static final int VALUE_TYPE_FLOAT = 0;
+ private static final int VALUE_TYPE_INT = 1;
+ private static final int VALUE_TYPE_PATH = 2;
+ private static final int VALUE_TYPE_COLOR = 3;
+ private static final int VALUE_TYPE_UNDEFINED = 4;
+
+ private static final boolean DBG_ANIMATOR_INFLATER = false;
+
+ // used to calculate changing configs for resource references
+ private static final TypedValue sTmpTypedValue = new TypedValue();
+
+ /**
+ * Loads an {@link Animator} object from a resource
+ *
+ * @param context Application context used to access resources
+ * @param id The resource id of the animation to load
+ * @return The animator object reference by the specified id
+ * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded
+ */
+ public static Animator loadAnimator(Context context, @AnimatorRes int id)
+ throws NotFoundException {
+ return loadAnimator(context.getResources(), context.getTheme(), id);
+ }
+
+ /**
+ * Loads an {@link Animator} object from a resource
+ *
+ * @param resources The resources
+ * @param theme The theme
+ * @param id The resource id of the animation to load
+ * @return The animator object reference by the specified id
+ * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded
+ * @hide
+ */
+ public static Animator loadAnimator(Resources resources, Theme theme, int id)
+ throws NotFoundException {
+ return loadAnimator(resources, theme, id, 1);
+ }
+
+ /** @hide */
+ public static Animator loadAnimator(Resources resources, Theme theme, int id,
+ float pathErrorScale) throws NotFoundException {
+ final ConfigurationBoundResourceCache<Animator> animatorCache = resources
+ .getAnimatorCache();
+ Animator animator = animatorCache.getInstance(id, resources, theme);
+ if (animator != null) {
+ if (DBG_ANIMATOR_INFLATER) {
+ Log.d(TAG, "loaded animator from cache, " + resources.getResourceName(id));
+ }
+ return animator;
+ } else if (DBG_ANIMATOR_INFLATER) {
+ Log.d(TAG, "cache miss for animator " + resources.getResourceName(id));
+ }
+ XmlResourceParser parser = null;
+ try {
+ parser = resources.getAnimation(id);
+ animator = createAnimatorFromXml(resources, theme, parser, pathErrorScale);
+ if (animator != null) {
+ animator.appendChangingConfigurations(getChangingConfigs(resources, id));
+ final ConstantState<Animator> constantState = animator.createConstantState();
+ if (constantState != null) {
+ if (DBG_ANIMATOR_INFLATER) {
+ Log.d(TAG, "caching animator for res " + resources.getResourceName(id));
+ }
+ animatorCache.put(id, theme, constantState);
+ // create a new animator so that cached version is never used by the user
+ animator = constantState.newInstance(resources, theme);
+ }
+ }
+ return animator;
+ } catch (XmlPullParserException ex) {
+ Resources.NotFoundException rnf =
+ new Resources.NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ Resources.NotFoundException rnf =
+ new Resources.NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ public static StateListAnimator loadStateListAnimator(Context context, int id)
+ throws NotFoundException {
+ final Resources resources = context.getResources();
+ final ConfigurationBoundResourceCache<StateListAnimator> cache = resources
+ .getStateListAnimatorCache();
+ final Theme theme = context.getTheme();
+ StateListAnimator animator = cache.getInstance(id, resources, theme);
+ if (animator != null) {
+ return animator;
+ }
+ XmlResourceParser parser = null;
+ try {
+ parser = resources.getAnimation(id);
+ animator = createStateListAnimatorFromXml(context, parser, Xml.asAttributeSet(parser));
+ if (animator != null) {
+ animator.appendChangingConfigurations(getChangingConfigs(resources, id));
+ final ConstantState<StateListAnimator> constantState = animator
+ .createConstantState();
+ if (constantState != null) {
+ cache.put(id, theme, constantState);
+ // return a clone so that the animator in constant state is never used.
+ animator = constantState.newInstance(resources, theme);
+ }
+ }
+ return animator;
+ } catch (XmlPullParserException ex) {
+ Resources.NotFoundException rnf =
+ new Resources.NotFoundException(
+ "Can't load state list animator resource ID #0x" +
+ Integer.toHexString(id)
+ );
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ Resources.NotFoundException rnf =
+ new Resources.NotFoundException(
+ "Can't load state list animator resource ID #0x" +
+ Integer.toHexString(id)
+ );
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+ }
+
+ private static StateListAnimator createStateListAnimatorFromXml(Context context,
+ XmlPullParser parser, AttributeSet attributeSet)
+ throws IOException, XmlPullParserException {
+ int type;
+ StateListAnimator stateListAnimator = new StateListAnimator();
+
+ while (true) {
+ type = parser.next();
+ switch (type) {
+ case XmlPullParser.END_DOCUMENT:
+ case XmlPullParser.END_TAG:
+ return stateListAnimator;
+
+ case XmlPullParser.START_TAG:
+ // parse item
+ Animator animator = null;
+ if ("item".equals(parser.getName())) {
+ int attributeCount = parser.getAttributeCount();
+ int[] states = new int[attributeCount];
+ int stateIndex = 0;
+ for (int i = 0; i < attributeCount; i++) {
+ int attrName = attributeSet.getAttributeNameResource(i);
+ if (attrName == R.attr.animation) {
+ final int animId = attributeSet.getAttributeResourceValue(i, 0);
+ animator = loadAnimator(context, animId);
+ } else {
+ states[stateIndex++] =
+ attributeSet.getAttributeBooleanValue(i, false) ?
+ attrName : -attrName;
+ }
+ }
+ if (animator == null) {
+ animator = createAnimatorFromXml(context.getResources(),
+ context.getTheme(), parser, 1f);
+ }
+
+ if (animator == null) {
+ throw new Resources.NotFoundException(
+ "animation state item must have a valid animation");
+ }
+ stateListAnimator
+ .addState(StateSet.trimStateSet(states, stateIndex), animator);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * PathDataEvaluator is used to interpolate between two paths which are
+ * represented in the same format but different control points' values.
+ * The path is represented as verbs and points for each of the verbs.
+ */
+ private static class PathDataEvaluator implements TypeEvaluator<PathParser.PathData> {
+ private final PathParser.PathData mPathData = new PathParser.PathData();
+
+ @Override
+ public PathParser.PathData evaluate(float fraction, PathParser.PathData startPathData,
+ PathParser.PathData endPathData) {
+ if (!PathParser.interpolatePathData(mPathData, startPathData, endPathData, fraction)) {
+ throw new IllegalArgumentException("Can't interpolate between"
+ + " two incompatible pathData");
+ }
+ return mPathData;
+ }
+ }
+
+ private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType,
+ int valueFromId, int valueToId, String propertyName) {
+
+ TypedValue tvFrom = styledAttributes.peekValue(valueFromId);
+ boolean hasFrom = (tvFrom != null);
+ int fromType = hasFrom ? tvFrom.type : 0;
+ TypedValue tvTo = styledAttributes.peekValue(valueToId);
+ boolean hasTo = (tvTo != null);
+ int toType = hasTo ? tvTo.type : 0;
+
+ if (valueType == VALUE_TYPE_UNDEFINED) {
+ // Check whether it's color type. If not, fall back to default type (i.e. float type)
+ if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) {
+ valueType = VALUE_TYPE_COLOR;
+ } else {
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ }
+
+ boolean getFloats = (valueType == VALUE_TYPE_FLOAT);
+
+ PropertyValuesHolder returnValue = null;
+
+ if (valueType == VALUE_TYPE_PATH) {
+ String fromString = styledAttributes.getString(valueFromId);
+ String toString = styledAttributes.getString(valueToId);
+ PathParser.PathData nodesFrom = fromString == null
+ ? null : new PathParser.PathData(fromString);
+ PathParser.PathData nodesTo = toString == null
+ ? null : new PathParser.PathData(toString);
+
+ if (nodesFrom != null || nodesTo != null) {
+ if (nodesFrom != null) {
+ TypeEvaluator evaluator = new PathDataEvaluator();
+ if (nodesTo != null) {
+ if (!PathParser.canMorph(nodesFrom, nodesTo)) {
+ throw new InflateException(" Can't morph from " + fromString + " to " +
+ toString);
+ }
+ returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
+ nodesFrom, nodesTo);
+ } else {
+ returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
+ (Object) nodesFrom);
+ }
+ } else if (nodesTo != null) {
+ TypeEvaluator evaluator = new PathDataEvaluator();
+ returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
+ (Object) nodesTo);
+ }
+ }
+ } else {
+ TypeEvaluator evaluator = null;
+ // Integer and float value types are handled here.
+ if (valueType == VALUE_TYPE_COLOR) {
+ // special case for colors: ignore valueType and get ints
+ evaluator = ArgbEvaluator.getInstance();
+ }
+ if (getFloats) {
+ float valueFrom;
+ float valueTo;
+ if (hasFrom) {
+ if (fromType == TypedValue.TYPE_DIMENSION) {
+ valueFrom = styledAttributes.getDimension(valueFromId, 0f);
+ } else {
+ valueFrom = styledAttributes.getFloat(valueFromId, 0f);
+ }
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = styledAttributes.getDimension(valueToId, 0f);
+ } else {
+ valueTo = styledAttributes.getFloat(valueToId, 0f);
+ }
+ returnValue = PropertyValuesHolder.ofFloat(propertyName,
+ valueFrom, valueTo);
+ } else {
+ returnValue = PropertyValuesHolder.ofFloat(propertyName, valueFrom);
+ }
+ } else {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = styledAttributes.getDimension(valueToId, 0f);
+ } else {
+ valueTo = styledAttributes.getFloat(valueToId, 0f);
+ }
+ returnValue = PropertyValuesHolder.ofFloat(propertyName, valueTo);
+ }
+ } else {
+ int valueFrom;
+ int valueTo;
+ if (hasFrom) {
+ if (fromType == TypedValue.TYPE_DIMENSION) {
+ valueFrom = (int) styledAttributes.getDimension(valueFromId, 0f);
+ } else if (isColorType(fromType)) {
+ valueFrom = styledAttributes.getColor(valueFromId, 0);
+ } else {
+ valueFrom = styledAttributes.getInt(valueFromId, 0);
+ }
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = (int) styledAttributes.getDimension(valueToId, 0f);
+ } else if (isColorType(toType)) {
+ valueTo = styledAttributes.getColor(valueToId, 0);
+ } else {
+ valueTo = styledAttributes.getInt(valueToId, 0);
+ }
+ returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom, valueTo);
+ } else {
+ returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom);
+ }
+ } else {
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = (int) styledAttributes.getDimension(valueToId, 0f);
+ } else if (isColorType(toType)) {
+ valueTo = styledAttributes.getColor(valueToId, 0);
+ } else {
+ valueTo = styledAttributes.getInt(valueToId, 0);
+ }
+ returnValue = PropertyValuesHolder.ofInt(propertyName, valueTo);
+ }
+ }
+ }
+ if (returnValue != null && evaluator != null) {
+ returnValue.setEvaluator(evaluator);
+ }
+ }
+
+ return returnValue;
+ }
+
+ /**
+ * @param anim The animator, must not be null
+ * @param arrayAnimator Incoming typed array for Animator's attributes.
+ * @param arrayObjectAnimator Incoming typed array for Object Animator's
+ * attributes.
+ * @param pixelSize The relative pixel size, used to calculate the
+ * maximum error for path animations.
+ */
+ private static void parseAnimatorFromTypeArray(ValueAnimator anim,
+ TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) {
+ long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300);
+
+ long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0);
+
+ int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, VALUE_TYPE_UNDEFINED);
+
+ if (valueType == VALUE_TYPE_UNDEFINED) {
+ valueType = inferValueTypeFromValues(arrayAnimator, R.styleable.Animator_valueFrom,
+ R.styleable.Animator_valueTo);
+ }
+ PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType,
+ R.styleable.Animator_valueFrom, R.styleable.Animator_valueTo, "");
+ if (pvh != null) {
+ anim.setValues(pvh);
+ }
+
+ anim.setDuration(duration);
+ anim.setStartDelay(startDelay);
+
+ if (arrayAnimator.hasValue(R.styleable.Animator_repeatCount)) {
+ anim.setRepeatCount(
+ arrayAnimator.getInt(R.styleable.Animator_repeatCount, 0));
+ }
+ if (arrayAnimator.hasValue(R.styleable.Animator_repeatMode)) {
+ anim.setRepeatMode(
+ arrayAnimator.getInt(R.styleable.Animator_repeatMode,
+ ValueAnimator.RESTART));
+ }
+
+ if (arrayObjectAnimator != null) {
+ setupObjectAnimator(anim, arrayObjectAnimator, valueType, pixelSize);
+ }
+ }
+
+ /**
+ * Setup the Animator to achieve path morphing.
+ *
+ * @param anim The target Animator which will be updated.
+ * @param arrayAnimator TypedArray for the ValueAnimator.
+ * @return the PathDataEvaluator.
+ */
+ private static TypeEvaluator setupAnimatorForPath(ValueAnimator anim,
+ TypedArray arrayAnimator) {
+ TypeEvaluator evaluator = null;
+ String fromString = arrayAnimator.getString(R.styleable.Animator_valueFrom);
+ String toString = arrayAnimator.getString(R.styleable.Animator_valueTo);
+ PathParser.PathData pathDataFrom = fromString == null
+ ? null : new PathParser.PathData(fromString);
+ PathParser.PathData pathDataTo = toString == null
+ ? null : new PathParser.PathData(toString);
+
+ if (pathDataFrom != null) {
+ if (pathDataTo != null) {
+ anim.setObjectValues(pathDataFrom, pathDataTo);
+ if (!PathParser.canMorph(pathDataFrom, pathDataTo)) {
+ throw new InflateException(arrayAnimator.getPositionDescription()
+ + " Can't morph from " + fromString + " to " + toString);
+ }
+ } else {
+ anim.setObjectValues((Object)pathDataFrom);
+ }
+ evaluator = new PathDataEvaluator();
+ } else if (pathDataTo != null) {
+ anim.setObjectValues((Object)pathDataTo);
+ evaluator = new PathDataEvaluator();
+ }
+
+ if (DBG_ANIMATOR_INFLATER && evaluator != null) {
+ Log.v(TAG, "create a new PathDataEvaluator here");
+ }
+
+ return evaluator;
+ }
+
+ /**
+ * Setup ObjectAnimator's property or values from pathData.
+ *
+ * @param anim The target Animator which will be updated.
+ * @param arrayObjectAnimator TypedArray for the ObjectAnimator.
+ * @param getFloats True if the value type is float.
+ * @param pixelSize The relative pixel size, used to calculate the
+ * maximum error for path animations.
+ */
+ private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator,
+ int valueType, float pixelSize) {
+ ObjectAnimator oa = (ObjectAnimator) anim;
+ String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData);
+
+ // Path can be involved in an ObjectAnimator in the following 3 ways:
+ // 1) Path morphing: the property to be animated is pathData, and valueFrom and valueTo
+ // are both of pathType. valueType = pathType needs to be explicitly defined.
+ // 2) A property in X or Y dimension can be animated along a path: the property needs to be
+ // defined in propertyXName or propertyYName attribute, the path will be defined in the
+ // pathData attribute. valueFrom and valueTo will not be necessary for this animation.
+ // 3) PathInterpolator can also define a path (in pathData) for its interpolation curve.
+ // Here we are dealing with case 2:
+ if (pathData != null) {
+ String propertyXName =
+ arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyXName);
+ String propertyYName =
+ arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyYName);
+
+ if (valueType == VALUE_TYPE_PATH || valueType == VALUE_TYPE_UNDEFINED) {
+ // When pathData is defined, we are in case #2 mentioned above. ValueType can only
+ // be float type, or int type. Otherwise we fallback to default type.
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ if (propertyXName == null && propertyYName == null) {
+ throw new InflateException(arrayObjectAnimator.getPositionDescription()
+ + " propertyXName or propertyYName is needed for PathData");
+ } else {
+ Path path = PathParser.createPathFromPathData(pathData);
+ float error = 0.5f * pixelSize; // max half a pixel error
+ PathKeyframes keyframeSet = KeyframeSet.ofPath(path, error);
+ Keyframes xKeyframes;
+ Keyframes yKeyframes;
+ if (valueType == VALUE_TYPE_FLOAT) {
+ xKeyframes = keyframeSet.createXFloatKeyframes();
+ yKeyframes = keyframeSet.createYFloatKeyframes();
+ } else {
+ xKeyframes = keyframeSet.createXIntKeyframes();
+ yKeyframes = keyframeSet.createYIntKeyframes();
+ }
+ PropertyValuesHolder x = null;
+ PropertyValuesHolder y = null;
+ if (propertyXName != null) {
+ x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes);
+ }
+ if (propertyYName != null) {
+ y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes);
+ }
+ if (x == null) {
+ oa.setValues(y);
+ } else if (y == null) {
+ oa.setValues(x);
+ } else {
+ oa.setValues(x, y);
+ }
+ }
+ } else {
+ String propertyName =
+ arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyName);
+ oa.setPropertyName(propertyName);
+ }
+ }
+
+ /**
+ * Setup ValueAnimator's values.
+ * This will handle all of the integer, float and color types.
+ *
+ * @param anim The target Animator which will be updated.
+ * @param arrayAnimator TypedArray for the ValueAnimator.
+ * @param getFloats True if the value type is float.
+ * @param hasFrom True if "valueFrom" exists.
+ * @param fromType The type of "valueFrom".
+ * @param hasTo True if "valueTo" exists.
+ * @param toType The type of "valueTo".
+ */
+ private static void setupValues(ValueAnimator anim, TypedArray arrayAnimator,
+ boolean getFloats, boolean hasFrom, int fromType, boolean hasTo, int toType) {
+ int valueFromIndex = R.styleable.Animator_valueFrom;
+ int valueToIndex = R.styleable.Animator_valueTo;
+ if (getFloats) {
+ float valueFrom;
+ float valueTo;
+ if (hasFrom) {
+ if (fromType == TypedValue.TYPE_DIMENSION) {
+ valueFrom = arrayAnimator.getDimension(valueFromIndex, 0f);
+ } else {
+ valueFrom = arrayAnimator.getFloat(valueFromIndex, 0f);
+ }
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
+ } else {
+ valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
+ }
+ anim.setFloatValues(valueFrom, valueTo);
+ } else {
+ anim.setFloatValues(valueFrom);
+ }
+ } else {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
+ } else {
+ valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
+ }
+ anim.setFloatValues(valueTo);
+ }
+ } else {
+ int valueFrom;
+ int valueTo;
+ if (hasFrom) {
+ if (fromType == TypedValue.TYPE_DIMENSION) {
+ valueFrom = (int) arrayAnimator.getDimension(valueFromIndex, 0f);
+ } else if (isColorType(fromType)) {
+ valueFrom = arrayAnimator.getColor(valueFromIndex, 0);
+ } else {
+ valueFrom = arrayAnimator.getInt(valueFromIndex, 0);
+ }
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
+ } else if (isColorType(toType)) {
+ valueTo = arrayAnimator.getColor(valueToIndex, 0);
+ } else {
+ valueTo = arrayAnimator.getInt(valueToIndex, 0);
+ }
+ anim.setIntValues(valueFrom, valueTo);
+ } else {
+ anim.setIntValues(valueFrom);
+ }
+ } else {
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
+ } else if (isColorType(toType)) {
+ valueTo = arrayAnimator.getColor(valueToIndex, 0);
+ } else {
+ valueTo = arrayAnimator.getInt(valueToIndex, 0);
+ }
+ anim.setIntValues(valueTo);
+ }
+ }
+ }
+ }
+
+ private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
+ float pixelSize)
+ throws XmlPullParserException, IOException {
+ return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0,
+ pixelSize);
+ }
+
+ private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
+ AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)
+ throws XmlPullParserException, IOException {
+ Animator anim = null;
+ ArrayList<Animator> childAnims = null;
+
+ // Make sure we are on a start tag.
+ int type;
+ int depth = parser.getDepth();
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+ boolean gotValues = false;
+
+ if (name.equals("objectAnimator")) {
+ anim = loadObjectAnimator(res, theme, attrs, pixelSize);
+ } else if (name.equals("animator")) {
+ anim = loadAnimator(res, theme, attrs, null, pixelSize);
+ } else if (name.equals("set")) {
+ anim = new AnimatorSet();
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.AnimatorSet, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.AnimatorSet);
+ }
+ anim.appendChangingConfigurations(a.getChangingConfigurations());
+ int ordering = a.getInt(R.styleable.AnimatorSet_ordering, TOGETHER);
+ createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering,
+ pixelSize);
+ a.recycle();
+ } else if (name.equals("propertyValuesHolder")) {
+ PropertyValuesHolder[] values = loadValues(res, theme, parser,
+ Xml.asAttributeSet(parser));
+ if (values != null && anim != null && (anim instanceof ValueAnimator)) {
+ ((ValueAnimator) anim).setValues(values);
+ }
+ gotValues = true;
+ } else {
+ throw new RuntimeException("Unknown animator name: " + parser.getName());
+ }
+
+ if (parent != null && !gotValues) {
+ if (childAnims == null) {
+ childAnims = new ArrayList<Animator>();
+ }
+ childAnims.add(anim);
+ }
+ }
+ if (parent != null && childAnims != null) {
+ Animator[] animsArray = new Animator[childAnims.size()];
+ int index = 0;
+ for (Animator a : childAnims) {
+ animsArray[index++] = a;
+ }
+ if (sequenceOrdering == TOGETHER) {
+ parent.playTogether(animsArray);
+ } else {
+ parent.playSequentially(animsArray);
+ }
+ }
+ return anim;
+ }
+
+ private static PropertyValuesHolder[] loadValues(Resources res, Theme theme,
+ XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
+ ArrayList<PropertyValuesHolder> values = null;
+
+ int type;
+ while ((type = parser.getEventType()) != XmlPullParser.END_TAG &&
+ type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ parser.next();
+ continue;
+ }
+
+ String name = parser.getName();
+
+ if (name.equals("propertyValuesHolder")) {
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.PropertyValuesHolder, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.PropertyValuesHolder);
+ }
+ String propertyName = a.getString(R.styleable.PropertyValuesHolder_propertyName);
+ int valueType = a.getInt(R.styleable.PropertyValuesHolder_valueType,
+ VALUE_TYPE_UNDEFINED);
+
+ PropertyValuesHolder pvh = loadPvh(res, theme, parser, propertyName, valueType);
+ if (pvh == null) {
+ pvh = getPVH(a, valueType,
+ R.styleable.PropertyValuesHolder_valueFrom,
+ R.styleable.PropertyValuesHolder_valueTo, propertyName);
+ }
+ if (pvh != null) {
+ if (values == null) {
+ values = new ArrayList<PropertyValuesHolder>();
+ }
+ values.add(pvh);
+ }
+ a.recycle();
+ }
+
+ parser.next();
+ }
+
+ PropertyValuesHolder[] valuesArray = null;
+ if (values != null) {
+ int count = values.size();
+ valuesArray = new PropertyValuesHolder[count];
+ for (int i = 0; i < count; ++i) {
+ valuesArray[i] = values.get(i);
+ }
+ }
+ return valuesArray;
+ }
+
+ // When no value type is provided in keyframe, we need to infer the type from the value. i.e.
+ // if value is defined in the style of a color value, then the color type is returned.
+ // Otherwise, default float type is returned.
+ private static int inferValueTypeOfKeyframe(Resources res, Theme theme, AttributeSet attrs) {
+ int valueType;
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.Keyframe);
+ }
+
+ TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value);
+ boolean hasValue = (keyframeValue != null);
+ // When no value type is provided, check whether it's a color type first.
+ // If not, fall back to default value type (i.e. float type).
+ if (hasValue && isColorType(keyframeValue.type)) {
+ valueType = VALUE_TYPE_COLOR;
+ } else {
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ a.recycle();
+ return valueType;
+ }
+
+ private static int inferValueTypeFromValues(TypedArray styledAttributes, int valueFromId,
+ int valueToId) {
+ TypedValue tvFrom = styledAttributes.peekValue(valueFromId);
+ boolean hasFrom = (tvFrom != null);
+ int fromType = hasFrom ? tvFrom.type : 0;
+ TypedValue tvTo = styledAttributes.peekValue(valueToId);
+ boolean hasTo = (tvTo != null);
+ int toType = hasTo ? tvTo.type : 0;
+
+ int valueType;
+ // Check whether it's color type. If not, fall back to default type (i.e. float type)
+ if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) {
+ valueType = VALUE_TYPE_COLOR;
+ } else {
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ return valueType;
+ }
+
+ private static void dumpKeyframes(Object[] keyframes, String header) {
+ if (keyframes == null || keyframes.length == 0) {
+ return;
+ }
+ Log.d(TAG, header);
+ int count = keyframes.length;
+ for (int i = 0; i < count; ++i) {
+ Keyframe keyframe = (Keyframe) keyframes[i];
+ Log.d(TAG, "Keyframe " + i + ": fraction " +
+ (keyframe.getFraction() < 0 ? "null" : keyframe.getFraction()) + ", " +
+ ", value : " + ((keyframe.hasValue()) ? keyframe.getValue() : "null"));
+ }
+ }
+
+ // Load property values holder if there are keyframes defined in it. Otherwise return null.
+ private static PropertyValuesHolder loadPvh(Resources res, Theme theme, XmlPullParser parser,
+ String propertyName, int valueType)
+ throws XmlPullParserException, IOException {
+
+ PropertyValuesHolder value = null;
+ ArrayList<Keyframe> keyframes = null;
+
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_TAG &&
+ type != XmlPullParser.END_DOCUMENT) {
+ String name = parser.getName();
+ if (name.equals("keyframe")) {
+ if (valueType == VALUE_TYPE_UNDEFINED) {
+ valueType = inferValueTypeOfKeyframe(res, theme, Xml.asAttributeSet(parser));
+ }
+ Keyframe keyframe = loadKeyframe(res, theme, Xml.asAttributeSet(parser), valueType);
+ if (keyframe != null) {
+ if (keyframes == null) {
+ keyframes = new ArrayList<Keyframe>();
+ }
+ keyframes.add(keyframe);
+ }
+ parser.next();
+ }
+ }
+
+ int count;
+ if (keyframes != null && (count = keyframes.size()) > 0) {
+ // make sure we have keyframes at 0 and 1
+ // If we have keyframes with set fractions, add keyframes at start/end
+ // appropriately. If start/end have no set fractions:
+ // if there's only one keyframe, set its fraction to 1 and add one at 0
+ // if >1 keyframe, set the last fraction to 1, the first fraction to 0
+ Keyframe firstKeyframe = keyframes.get(0);
+ Keyframe lastKeyframe = keyframes.get(count - 1);
+ float endFraction = lastKeyframe.getFraction();
+ if (endFraction < 1) {
+ if (endFraction < 0) {
+ lastKeyframe.setFraction(1);
+ } else {
+ keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1));
+ ++count;
+ }
+ }
+ float startFraction = firstKeyframe.getFraction();
+ if (startFraction != 0) {
+ if (startFraction < 0) {
+ firstKeyframe.setFraction(0);
+ } else {
+ keyframes.add(0, createNewKeyframe(firstKeyframe, 0));
+ ++count;
+ }
+ }
+ Keyframe[] keyframeArray = new Keyframe[count];
+ keyframes.toArray(keyframeArray);
+ for (int i = 0; i < count; ++i) {
+ Keyframe keyframe = keyframeArray[i];
+ if (keyframe.getFraction() < 0) {
+ if (i == 0) {
+ keyframe.setFraction(0);
+ } else if (i == count - 1) {
+ keyframe.setFraction(1);
+ } else {
+ // figure out the start/end parameters of the current gap
+ // in fractions and distribute the gap among those keyframes
+ int startIndex = i;
+ int endIndex = i;
+ for (int j = startIndex + 1; j < count - 1; ++j) {
+ if (keyframeArray[j].getFraction() >= 0) {
+ break;
+ }
+ endIndex = j;
+ }
+ float gap = keyframeArray[endIndex + 1].getFraction() -
+ keyframeArray[startIndex - 1].getFraction();
+ distributeKeyframes(keyframeArray, gap, startIndex, endIndex);
+ }
+ }
+ }
+ value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray);
+ if (valueType == VALUE_TYPE_COLOR) {
+ value.setEvaluator(ArgbEvaluator.getInstance());
+ }
+ }
+
+ return value;
+ }
+
+ private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) {
+ return sampleKeyframe.getType() == float.class ?
+ Keyframe.ofFloat(fraction) :
+ (sampleKeyframe.getType() == int.class) ?
+ Keyframe.ofInt(fraction) :
+ Keyframe.ofObject(fraction);
+ }
+
+ /**
+ * Utility function to set fractions on keyframes to cover a gap in which the
+ * fractions are not currently set. Keyframe fractions will be distributed evenly
+ * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap
+ * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the
+ * keyframe before startIndex.
+ * Assumptions:
+ * - First and last keyframe fractions (bounding this spread) are already set. So,
+ * for example, if no fractions are set, we will already set first and last keyframe
+ * fraction values to 0 and 1.
+ * - startIndex must be >0 (which follows from first assumption).
+ * - endIndex must be >= startIndex.
+ *
+ * @param keyframes the array of keyframes
+ * @param gap The total gap we need to distribute
+ * @param startIndex The index of the first keyframe whose fraction must be set
+ * @param endIndex The index of the last keyframe whose fraction must be set
+ */
+ private static void distributeKeyframes(Keyframe[] keyframes, float gap,
+ int startIndex, int endIndex) {
+ int count = endIndex - startIndex + 2;
+ float increment = gap / count;
+ for (int i = startIndex; i <= endIndex; ++i) {
+ keyframes[i].setFraction(keyframes[i-1].getFraction() + increment);
+ }
+ }
+
+ private static Keyframe loadKeyframe(Resources res, Theme theme, AttributeSet attrs,
+ int valueType)
+ throws XmlPullParserException, IOException {
+
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.Keyframe);
+ }
+
+ Keyframe keyframe = null;
+
+ float fraction = a.getFloat(R.styleable.Keyframe_fraction, -1);
+
+ TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value);
+ boolean hasValue = (keyframeValue != null);
+ if (valueType == VALUE_TYPE_UNDEFINED) {
+ // When no value type is provided, check whether it's a color type first.
+ // If not, fall back to default value type (i.e. float type).
+ if (hasValue && isColorType(keyframeValue.type)) {
+ valueType = VALUE_TYPE_COLOR;
+ } else {
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ }
+
+ if (hasValue) {
+ switch (valueType) {
+ case VALUE_TYPE_FLOAT:
+ float value = a.getFloat(R.styleable.Keyframe_value, 0);
+ keyframe = Keyframe.ofFloat(fraction, value);
+ break;
+ case VALUE_TYPE_COLOR:
+ case VALUE_TYPE_INT:
+ int intValue = a.getInt(R.styleable.Keyframe_value, 0);
+ keyframe = Keyframe.ofInt(fraction, intValue);
+ break;
+ }
+ } else {
+ keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) :
+ Keyframe.ofInt(fraction);
+ }
+
+ final int resID = a.getResourceId(R.styleable.Keyframe_interpolator, 0);
+ if (resID > 0) {
+ final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID);
+ keyframe.setInterpolator(interpolator);
+ }
+ a.recycle();
+
+ return keyframe;
+ }
+
+ private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs,
+ float pathErrorScale) throws NotFoundException {
+ ObjectAnimator anim = new ObjectAnimator();
+
+ loadAnimator(res, theme, attrs, anim, pathErrorScale);
+
+ return anim;
+ }
+
+ /**
+ * Creates a new animation whose parameters come from the specified context
+ * and attributes set.
+ *
+ * @param res The resources
+ * @param attrs The set of attributes holding the animation parameters
+ * @param anim Null if this is a ValueAnimator, otherwise this is an
+ * ObjectAnimator
+ */
+ private static ValueAnimator loadAnimator(Resources res, Theme theme,
+ AttributeSet attrs, ValueAnimator anim, float pathErrorScale)
+ throws NotFoundException {
+ TypedArray arrayAnimator = null;
+ TypedArray arrayObjectAnimator = null;
+
+ if (theme != null) {
+ arrayAnimator = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0);
+ } else {
+ arrayAnimator = res.obtainAttributes(attrs, R.styleable.Animator);
+ }
+
+ // If anim is not null, then it is an object animator.
+ if (anim != null) {
+ if (theme != null) {
+ arrayObjectAnimator = theme.obtainStyledAttributes(attrs,
+ R.styleable.PropertyAnimator, 0, 0);
+ } else {
+ arrayObjectAnimator = res.obtainAttributes(attrs, R.styleable.PropertyAnimator);
+ }
+ anim.appendChangingConfigurations(arrayObjectAnimator.getChangingConfigurations());
+ }
+
+ if (anim == null) {
+ anim = new ValueAnimator();
+ }
+ anim.appendChangingConfigurations(arrayAnimator.getChangingConfigurations());
+
+ parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale);
+
+ final int resID = arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0);
+ if (resID > 0) {
+ final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID);
+ if (interpolator instanceof BaseInterpolator) {
+ anim.appendChangingConfigurations(
+ ((BaseInterpolator) interpolator).getChangingConfiguration());
+ }
+ anim.setInterpolator(interpolator);
+ }
+
+ arrayAnimator.recycle();
+ if (arrayObjectAnimator != null) {
+ arrayObjectAnimator.recycle();
+ }
+ return anim;
+ }
+
+ private static @Config int getChangingConfigs(@NonNull Resources resources, @AnyRes int id) {
+ synchronized (sTmpTypedValue) {
+ resources.getValue(id, sTmpTypedValue, true);
+ return sTmpTypedValue.changingConfigurations;
+ }
+ }
+
+ private static boolean isColorType(int type) {
+ return (type >= TypedValue.TYPE_FIRST_COLOR_INT) && (type <= TypedValue.TYPE_LAST_COLOR_INT);
+ }
+}
diff --git a/android/animation/AnimatorListenerAdapter.java b/android/animation/AnimatorListenerAdapter.java
new file mode 100644
index 00000000..2ecb8c3d
--- /dev/null
+++ b/android/animation/AnimatorListenerAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * 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,
+ Animator.AnimatorPauseListener {
+
+ /**
+ * {@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) {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAnimationPause(Animator animation) {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAnimationResume(Animator animation) {
+ }
+}
diff --git a/android/animation/AnimatorSet.java b/android/animation/AnimatorSet.java
new file mode 100644
index 00000000..00d6657e
--- /dev/null
+++ b/android/animation/AnimatorSet.java
@@ -0,0 +1,2091 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.app.ActivityThread;
+import android.app.Application;
+import android.os.Build;
+import android.os.Looper;
+import android.util.AndroidRuntimeException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.animation.Animation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This class plays a set of {@link Animator} objects in the specified order. Animations
+ * can be set up to play together, in sequence, or after a specified delay.
+ *
+ * <p>There are two different approaches to adding animations to a <code>AnimatorSet</code>:
+ * either the {@link AnimatorSet#playTogether(Animator[]) playTogether()} or
+ * {@link AnimatorSet#playSequentially(Animator[]) playSequentially()} methods can be called to add
+ * a set of animations all at once, or the {@link AnimatorSet#play(Animator)} can be
+ * used in conjunction with methods in the {@link AnimatorSet.Builder Builder}
+ * class to add animations
+ * one by one.</p>
+ *
+ * <p>It is possible to set up a <code>AnimatorSet</code> 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.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about animating with {@code AnimatorSet}, read the
+ * <a href="{@docRoot}guide/topics/graphics/prop-animation.html#choreography">Property
+ * Animation</a> developer guide.</p>
+ * </div>
+ */
+public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback {
+
+ private static final String TAG = "AnimatorSet";
+ /**
+ * 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<Node> mPlayingSet = new ArrayList<Node>();
+
+ /**
+ * 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 ArrayMap<Animator, Node> mNodeMap = new ArrayMap<Animator, Node>();
+
+ /**
+ * Contains the start and end events of all the nodes. All these events are sorted in this list.
+ */
+ private ArrayList<AnimationEvent> mEvents = new ArrayList<>();
+
+ /**
+ * 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<Node> mNodes = new ArrayList<Node>();
+
+ /**
+ * Tracks whether any change has been made to the AnimatorSet, which is then used to
+ * determine whether the dependency graph should be re-constructed.
+ */
+ private boolean mDependencyDirty = 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 = ValueAnimator.ofFloat(0f, 1f).setDuration(0);
+
+ // Root of the dependency tree of all the animators in the set. In this tree, parent-child
+ // relationship captures the order of animation (i.e. parent and child will play sequentially),
+ // and sibling relationship indicates "with" relationship, as sibling animators start at the
+ // same time.
+ private Node mRootNode = new Node(mDelayAnim);
+
+ // 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;
+
+ // Records the interpolator for the set. Null value indicates that no interpolator
+ // was set on this AnimatorSet, so it should not be passed down to the children.
+ private TimeInterpolator mInterpolator = null;
+
+ // The total duration of finishing all the Animators in the set.
+ private long mTotalDuration = 0;
+
+ // In pre-N releases, calling end() before start() on an animator set is no-op. But that is not
+ // consistent with the behavior for other animator types. In order to keep the behavior
+ // consistent within Animation framework, when end() is called without start(), we will start
+ // the animator set and immediately end it for N and forward.
+ private final boolean mShouldIgnoreEndWithoutStart;
+
+ // In pre-O releases, calling start() doesn't reset all the animators values to start values.
+ // As a result, the start of the animation is inconsistent with what setCurrentPlayTime(0) would
+ // look like on O. Also it is inconsistent with what reverse() does on O, as reverse would
+ // advance all the animations to the right beginning values for before starting to reverse.
+ // From O and forward, we will add an additional step of resetting the animation values (unless
+ // the animation was previously seeked and therefore doesn't start from the beginning).
+ private final boolean mShouldResetValuesAtStart;
+
+ // In pre-O releases, end() may never explicitly called on a child animator. As a result, end()
+ // may not even be properly implemented in a lot of cases. After a few apps crashing on this,
+ // it became necessary to use an sdk target guard for calling end().
+ private final boolean mEndCanBeCalled;
+
+ // The time, in milliseconds, when last frame of the animation came in. -1 when the animation is
+ // not running.
+ private long mLastFrameTime = -1;
+
+ // The time, in milliseconds, when the first frame of the animation came in. This is the
+ // frame before we start counting down the start delay, if any.
+ // -1 when the animation is not running.
+ private long mFirstFrame = -1;
+
+ // The time, in milliseconds, when the first frame of the animation came in.
+ // -1 when the animation is not running.
+ private int mLastEventId = -1;
+
+ // Indicates whether the animation is reversing.
+ private boolean mReversing = false;
+
+ // Indicates whether the animation should register frame callbacks. If false, the animation will
+ // passively wait for an AnimatorSet to pulse it.
+ private boolean mSelfPulse = true;
+
+ // SeekState stores the last seeked play time as well as seek direction.
+ private SeekState mSeekState = new SeekState();
+
+ // Indicates where children animators are all initialized with their start values captured.
+ private boolean mChildrenInitialized = false;
+
+ /**
+ * Set on the next frame after pause() is called, used to calculate a new startTime
+ * or delayStartTime which allows the animator set to continue from the point at which
+ * it was paused. If negative, has not yet been set.
+ */
+ private long mPauseTime = -1;
+
+ // This is to work around a bug in b/34736819. This needs to be removed once app team
+ // fixes their side.
+ private AnimatorListenerAdapter mDummyListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mNodeMap.get(animation) == null) {
+ throw new AndroidRuntimeException("Error: animation ended is not in the node map");
+ }
+ mNodeMap.get(animation).mEnded = true;
+
+ }
+ };
+
+ public AnimatorSet() {
+ super();
+ mNodeMap.put(mDelayAnim, mRootNode);
+ mNodes.add(mRootNode);
+ boolean isPreO;
+ // Set the flag to ignore calling end() without start() for pre-N releases
+ Application app = ActivityThread.currentApplication();
+ if (app == null || app.getApplicationInfo() == null) {
+ mShouldIgnoreEndWithoutStart = true;
+ isPreO = true;
+ } else {
+ if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
+ mShouldIgnoreEndWithoutStart = true;
+ } else {
+ mShouldIgnoreEndWithoutStart = false;
+ }
+
+ isPreO = app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O;
+ }
+ mShouldResetValuesAtStart = !isPreO;
+ mEndCanBeCalled = !isPreO;
+ }
+
+ /**
+ * Sets up this AnimatorSet to play all of the supplied animations at the same time.
+ * This is equivalent to calling {@link #play(Animator)} with the first animator in the
+ * set and then {@link Builder#with(Animator)} with each of the other animators. Note that
+ * an Animator with a {@link Animator#setStartDelay(long) startDelay} will not actually
+ * start until that delay elapses, which means that if the first animator in the list
+ * supplied to this constructor has a startDelay, none of the other animators will start
+ * until that first animator's startDelay has elapsed.
+ *
+ * @param items The animations that will be started simultaneously.
+ */
+ public void playTogether(Animator... items) {
+ if (items != null) {
+ 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<Animator> items) {
+ if (items != null && items.size() > 0) {
+ 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) {
+ 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<Animator> items) {
+ if (items != null && items.size() > 0) {
+ 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<Animator> The list of child animations of this AnimatorSet.
+ */
+ public ArrayList<Animator> getChildAnimations() {
+ ArrayList<Animator> childList = new ArrayList<Animator>();
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ if (node != mRootNode) {
+ childList.add(node.mAnimation);
+ }
+ }
+ 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) {
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ Animator animation = node.mAnimation;
+ if (animation instanceof AnimatorSet) {
+ ((AnimatorSet)animation).setTarget(target);
+ } else if (animation instanceof ObjectAnimator) {
+ ((ObjectAnimator)animation).setTarget(target);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int getChangingConfigurations() {
+ int conf = super.getChangingConfigurations();
+ final int nodeCount = mNodes.size();
+ for (int i = 0; i < nodeCount; i ++) {
+ conf |= mNodes.get(i).mAnimation.getChangingConfigurations();
+ }
+ return conf;
+ }
+
+ /**
+ * Sets the TimeInterpolator for all current {@link #getChildAnimations() child animations}
+ * of this AnimatorSet. The default value is null, which means that no interpolator
+ * is set on this AnimatorSet. Setting the interpolator to any non-null value
+ * will cause that interpolator to be set on the child animations
+ * when the set is started.
+ *
+ * @param interpolator the interpolator to be used by each child animation of this AnimatorSet
+ */
+ @Override
+ public void setInterpolator(TimeInterpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ @Override
+ public TimeInterpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * This method creates a <code>Builder</code> object, which is used to
+ * set up playing constraints. This initial <code>play()</code> method
+ * tells the <code>Builder</code> the animation that is the dependency for
+ * the succeeding commands to the <code>Builder</code>. For example,
+ * calling <code>play(a1).with(a2)</code> sets up the AnimatorSet to play
+ * <code>a1</code> and <code>a2</code> at the same time,
+ * <code>play(a1).before(a2)</code> sets up the AnimatorSet to play
+ * <code>a1</code> first, followed by <code>a2</code>, and
+ * <code>play(a1).after(a2)</code> sets up the AnimatorSet to play
+ * <code>a2</code> first, followed by <code>a1</code>.
+ *
+ * <p>Note that <code>play()</code> is the only way to tell the
+ * <code>Builder</code> the animation upon which the dependency is created,
+ * so successive calls to the various functions in <code>Builder</code>
+ * will all refer to the initial parameter supplied in <code>play()</code>
+ * as the dependency of the other animations. For example, calling
+ * <code>play(a1).before(a2).before(a3)</code> will play both <code>a2</code>
+ * and <code>a3</code> when a1 ends; it does not set up a dependency between
+ * <code>a2</code> and <code>a3</code>.</p>
+ *
+ * @param anim The animation that is the dependency used in later calls to the
+ * methods in the returned <code>Builder</code> object. A null parameter will result
+ * in a null <code>Builder</code> return value.
+ * @return Builder The object that constructs the AnimatorSet based on the dependencies
+ * outlined in the calls to <code>play</code> and the other methods in the
+ * <code>Builder</code object.
+ */
+ public Builder play(Animator anim) {
+ if (anim != null) {
+ return new Builder(anim);
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Note that canceling a <code>AnimatorSet</code> also cancels all of the animations that it
+ * is responsible for.</p>
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public void cancel() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
+ if (isStarted()) {
+ ArrayList<AnimatorListener> tmpListeners = null;
+ if (mListeners != null) {
+ tmpListeners = (ArrayList<AnimatorListener>) mListeners.clone();
+ int size = tmpListeners.size();
+ for (int i = 0; i < size; i++) {
+ tmpListeners.get(i).onAnimationCancel(this);
+ }
+ }
+ ArrayList<Node> playingSet = new ArrayList<>(mPlayingSet);
+ int setSize = playingSet.size();
+ for (int i = 0; i < setSize; i++) {
+ playingSet.get(i).mAnimation.cancel();
+ }
+ mPlayingSet.clear();
+ endAnimation();
+ }
+ }
+
+ // Force all the animations to end when the duration scale is 0.
+ private void forceToEnd() {
+ if (mEndCanBeCalled) {
+ end();
+ return;
+ }
+
+ // Note: we don't want to combine this case with the end() method below because in
+ // the case of developer calling end(), we still need to make sure end() is explicitly
+ // called on the child animators to maintain the old behavior.
+ if (mReversing) {
+ handleAnimationEvents(mLastEventId, 0, getTotalDuration());
+ } else {
+ long zeroScalePlayTime = getTotalDuration();
+ if (zeroScalePlayTime == DURATION_INFINITE) {
+ // Use a large number for the play time.
+ zeroScalePlayTime = Integer.MAX_VALUE;
+ }
+ handleAnimationEvents(mLastEventId, mEvents.size() - 1, zeroScalePlayTime);
+ }
+ mPlayingSet.clear();
+ endAnimation();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Note that ending a <code>AnimatorSet</code> also ends all of the animations that it is
+ * responsible for.</p>
+ */
+ @Override
+ public void end() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
+ if (mShouldIgnoreEndWithoutStart && !isStarted()) {
+ return;
+ }
+ if (isStarted()) {
+ // Iterate the animations that haven't finished or haven't started, and end them.
+ if (mReversing) {
+ // Between start() and first frame, mLastEventId would be unset (i.e. -1)
+ mLastEventId = mLastEventId == -1 ? mEvents.size() : mLastEventId;
+ while (mLastEventId > 0) {
+ mLastEventId = mLastEventId - 1;
+ AnimationEvent event = mEvents.get(mLastEventId);
+ Animator anim = event.mNode.mAnimation;
+ if (mNodeMap.get(anim).mEnded) {
+ continue;
+ }
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ anim.reverse();
+ } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED
+ && anim.isStarted()) {
+ // Make sure anim hasn't finished before calling end() so that we don't end
+ // already ended animations, which will cause start and end callbacks to be
+ // triggered again.
+ anim.end();
+ }
+ }
+ } else {
+ while (mLastEventId < mEvents.size() - 1) {
+ // Avoid potential reentrant loop caused by child animators manipulating
+ // AnimatorSet's lifecycle (i.e. not a recommended approach).
+ mLastEventId = mLastEventId + 1;
+ AnimationEvent event = mEvents.get(mLastEventId);
+ Animator anim = event.mNode.mAnimation;
+ if (mNodeMap.get(anim).mEnded) {
+ continue;
+ }
+ if (event.mEvent == AnimationEvent.ANIMATION_START) {
+ anim.start();
+ } else if (event.mEvent == AnimationEvent.ANIMATION_END && anim.isStarted()) {
+ // Make sure anim hasn't finished before calling end() so that we don't end
+ // already ended animations, which will cause start and end callbacks to be
+ // triggered again.
+ anim.end();
+ }
+ }
+ }
+ mPlayingSet.clear();
+ }
+ endAnimation();
+ }
+
+ /**
+ * Returns true if any of the child animations of this AnimatorSet have been started and have
+ * not yet ended. Child animations will not be started until the AnimatorSet has gone past
+ * its initial delay set through {@link #setStartDelay(long)}.
+ *
+ * @return Whether this AnimatorSet has gone past the initial delay, and at least one child
+ * animation has been started and not yet ended.
+ */
+ @Override
+ public boolean isRunning() {
+ if (mStartDelay == 0) {
+ return mStarted;
+ }
+ return mLastFrameTime > 0;
+ }
+
+ @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. Note that the start delay should always be non-negative. Any
+ * negative start delay will be clamped to 0 on N and above.
+ *
+ * @param startDelay The amount of the delay, in milliseconds
+ */
+ @Override
+ public void setStartDelay(long startDelay) {
+ // Clamp start delay to non-negative range.
+ if (startDelay < 0) {
+ Log.w(TAG, "Start delay should always be non-negative");
+ startDelay = 0;
+ }
+ long delta = startDelay - mStartDelay;
+ if (delta == 0) {
+ return;
+ }
+ mStartDelay = startDelay;
+ if (!mDependencyDirty) {
+ // Dependency graph already constructed, update all the nodes' start/end time
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ if (node == mRootNode) {
+ node.mEndTime = mStartDelay;
+ } else {
+ node.mStartTime = node.mStartTime == DURATION_INFINITE ?
+ DURATION_INFINITE : node.mStartTime + delta;
+ node.mEndTime = node.mEndTime == DURATION_INFINITE ?
+ DURATION_INFINITE : node.mEndTime + delta;
+ }
+ }
+ // Update total duration, if necessary.
+ if (mTotalDuration != DURATION_INFINITE) {
+ mTotalDuration += delta;
+ }
+ }
+ }
+
+ /**
+ * 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");
+ }
+ mDependencyDirty = true;
+ // Just record the value for now - it will be used later when the AnimatorSet starts
+ mDuration = duration;
+ return this;
+ }
+
+ @Override
+ public void setupStartValues() {
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ if (node != mRootNode) {
+ node.mAnimation.setupStartValues();
+ }
+ }
+ }
+
+ @Override
+ public void setupEndValues() {
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ if (node != mRootNode) {
+ node.mAnimation.setupEndValues();
+ }
+ }
+ }
+
+ @Override
+ public void pause() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
+ boolean previouslyPaused = mPaused;
+ super.pause();
+ if (!previouslyPaused && mPaused) {
+ mPauseTime = -1;
+ }
+ }
+
+ @Override
+ public void resume() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
+ boolean previouslyPaused = mPaused;
+ super.resume();
+ if (previouslyPaused && !mPaused) {
+ if (mPauseTime >= 0) {
+ addAnimationCallback(0);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Starting this <code>AnimatorSet</code> 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.
+ *
+ * <b>Note:</b> Manipulating AnimatorSet's lifecycle in the child animators' listener callbacks
+ * will lead to undefined behaviors. Also, AnimatorSet will ignore any seeking in the child
+ * animators once {@link #start()} is called.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public void start() {
+ start(false, true);
+ }
+
+ @Override
+ void startWithoutPulsing(boolean inReverse) {
+ start(inReverse, false);
+ }
+
+ private void initAnimation() {
+ if (mInterpolator != null) {
+ for (int i = 0; i < mNodes.size(); i++) {
+ Node node = mNodes.get(i);
+ node.mAnimation.setInterpolator(mInterpolator);
+ }
+ }
+ updateAnimatorsDuration();
+ createDependencyGraph();
+ }
+
+ private void start(boolean inReverse, boolean selfPulse) {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
+ mStarted = true;
+ mSelfPulse = selfPulse;
+ mPaused = false;
+ mPauseTime = -1;
+
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ node.mEnded = false;
+ node.mAnimation.setAllowRunningAsynchronously(false);
+ }
+
+ initAnimation();
+ if (inReverse && !canReverse()) {
+ throw new UnsupportedOperationException("Cannot reverse infinite AnimatorSet");
+ }
+
+ mReversing = inReverse;
+
+ // Now that all dependencies are set up, start the animations that should be started.
+ boolean isEmptySet = isEmptySet(this);
+ if (!isEmptySet) {
+ startAnimation();
+ }
+
+ if (mListeners != null) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationStart(this, inReverse);
+ }
+ }
+ if (isEmptySet) {
+ // In the case of empty AnimatorSet, or 0 duration scale, we will trigger the
+ // onAnimationEnd() right away.
+ end();
+ }
+ }
+
+ // Returns true if set is empty or contains nothing but animator sets with no start delay.
+ private static boolean isEmptySet(AnimatorSet set) {
+ if (set.getStartDelay() > 0) {
+ return false;
+ }
+ for (int i = 0; i < set.getChildAnimations().size(); i++) {
+ Animator anim = set.getChildAnimations().get(i);
+ if (!(anim instanceof AnimatorSet)) {
+ // Contains non-AnimatorSet, not empty.
+ return false;
+ } else {
+ if (!isEmptySet((AnimatorSet) anim)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private void updateAnimatorsDuration() {
+ if (mDuration >= 0) {
+ // If the duration was set on this AnimatorSet, pass it along to all child animations
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ // TODO: don't set the duration of the timing-only nodes created by AnimatorSet to
+ // insert "play-after" delays
+ node.mAnimation.setDuration(mDuration);
+ }
+ }
+ mDelayAnim.setDuration(mStartDelay);
+ }
+
+ @Override
+ void skipToEndValue(boolean inReverse) {
+ if (!isInitialized()) {
+ throw new UnsupportedOperationException("Children must be initialized.");
+ }
+
+ // This makes sure the animation events are sorted an up to date.
+ initAnimation();
+
+ // Calling skip to the end in the sequence that they would be called in a forward/reverse
+ // run, such that the sequential animations modifying the same property would have
+ // the right value in the end.
+ if (inReverse) {
+ for (int i = mEvents.size() - 1; i >= 0; i--) {
+ if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ mEvents.get(i).mNode.mAnimation.skipToEndValue(true);
+ }
+ }
+ } else {
+ for (int i = 0; i < mEvents.size(); i++) {
+ if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_END) {
+ mEvents.get(i).mNode.mAnimation.skipToEndValue(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Internal only.
+ *
+ * This method sets the animation values based on the play time. It also fast forward or
+ * backward all the child animations progress accordingly.
+ *
+ * This method is also responsible for calling
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationRepeat(Animation)},
+ * as needed, based on the last play time and current play time.
+ */
+ @Override
+ void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {
+ if (currentPlayTime < 0 || lastPlayTime < 0) {
+ throw new UnsupportedOperationException("Error: Play time should never be negative.");
+ }
+ // TODO: take into account repeat counts and repeat callback when repeat is implemented.
+ // Clamp currentPlayTime and lastPlayTime
+
+ // TODO: Make this more efficient
+
+ // Convert the play times to the forward direction.
+ if (inReverse) {
+ if (getTotalDuration() == DURATION_INFINITE) {
+ throw new UnsupportedOperationException("Cannot reverse AnimatorSet with infinite"
+ + " duration");
+ }
+ long duration = getTotalDuration() - mStartDelay;
+ currentPlayTime = Math.min(currentPlayTime, duration);
+ currentPlayTime = duration - currentPlayTime;
+ lastPlayTime = duration - lastPlayTime;
+ inReverse = false;
+ }
+ // Skip all values to start, and iterate mEvents to get animations to the right fraction.
+ skipToStartValue(false);
+
+ ArrayList<Node> unfinishedNodes = new ArrayList<>();
+ // Assumes forward playing from here on.
+ for (int i = 0; i < mEvents.size(); i++) {
+ AnimationEvent event = mEvents.get(i);
+ if (event.getTime() > currentPlayTime) {
+ break;
+ }
+
+ // This animation started prior to the current play time, and won't finish before the
+ // play time, add to the unfinished list.
+ if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ if (event.mNode.mEndTime == DURATION_INFINITE
+ || event.mNode.mEndTime > currentPlayTime) {
+ unfinishedNodes.add(event.mNode);
+ }
+ }
+ // For animations that do finish before the play time, end them in the sequence that
+ // they would in a normal run.
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ // Skip to the end of the animation.
+ event.mNode.mAnimation.skipToEndValue(false);
+ }
+ }
+
+ // Seek unfinished animation to the right time.
+ for (int i = 0; i < unfinishedNodes.size(); i++) {
+ Node node = unfinishedNodes.get(i);
+ long playTime = getPlayTimeForNode(currentPlayTime, node, inReverse);
+ if (!inReverse) {
+ playTime -= node.mAnimation.getStartDelay();
+ }
+ node.mAnimation.animateBasedOnPlayTime(playTime, lastPlayTime, inReverse);
+ }
+ }
+
+ @Override
+ boolean isInitialized() {
+ if (mChildrenInitialized) {
+ return true;
+ }
+
+ boolean allInitialized = true;
+ for (int i = 0; i < mNodes.size(); i++) {
+ if (!mNodes.get(i).mAnimation.isInitialized()) {
+ allInitialized = false;
+ break;
+ }
+ }
+ mChildrenInitialized = allInitialized;
+ return mChildrenInitialized;
+ }
+
+ private void skipToStartValue(boolean inReverse) {
+ skipToEndValue(!inReverse);
+ }
+
+ /**
+ * 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.
+ * Unless the animation is reversing, the playtime is considered the time since
+ * the end of the start delay of the AnimatorSet in a forward playing direction.
+ *
+ */
+ public void setCurrentPlayTime(long playTime) {
+ if (mReversing && getTotalDuration() == DURATION_INFINITE) {
+ // Should never get here
+ throw new UnsupportedOperationException("Error: Cannot seek in reverse in an infinite"
+ + " AnimatorSet");
+ }
+
+ if ((getTotalDuration() != DURATION_INFINITE && playTime > getTotalDuration() - mStartDelay)
+ || playTime < 0) {
+ throw new UnsupportedOperationException("Error: Play time should always be in between"
+ + "0 and duration.");
+ }
+
+ initAnimation();
+
+ if (!isStarted()) {
+ if (mReversing) {
+ throw new UnsupportedOperationException("Error: Something went wrong. mReversing"
+ + " should not be set when AnimatorSet is not started.");
+ }
+ if (!mSeekState.isActive()) {
+ findLatestEventIdForTime(0);
+ // Set all the values to start values.
+ initChildren();
+ skipToStartValue(mReversing);
+ mSeekState.setPlayTime(0, mReversing);
+ }
+ animateBasedOnPlayTime(playTime, 0, mReversing);
+ mSeekState.setPlayTime(playTime, mReversing);
+ } else {
+ // If the animation is running, just set the seek time and wait until the next frame
+ // (i.e. doAnimationFrame(...)) to advance the animation.
+ mSeekState.setPlayTime(playTime, mReversing);
+ }
+ }
+
+ /**
+ * Returns the milliseconds elapsed since the start of the animation.
+ *
+ * <p>For ongoing animations, this method returns the current progress of the animation in
+ * terms of play time. For an animation that has not yet been started: if the animation has been
+ * seeked to a certain time via {@link #setCurrentPlayTime(long)}, the seeked play time will
+ * be returned; otherwise, this method will return 0.
+ *
+ * @return the current position in time of the animation in milliseconds
+ */
+ public long getCurrentPlayTime() {
+ if (mSeekState.isActive()) {
+ return mSeekState.getPlayTime();
+ }
+ if (mLastFrameTime == -1) {
+ // Not yet started or during start delay
+ return 0;
+ }
+ float durationScale = ValueAnimator.getDurationScale();
+ durationScale = durationScale == 0 ? 1 : durationScale;
+ if (mReversing) {
+ return (long) ((mLastFrameTime - mFirstFrame) / durationScale);
+ } else {
+ return (long) ((mLastFrameTime - mFirstFrame - mStartDelay) / durationScale);
+ }
+ }
+
+ private void initChildren() {
+ if (!isInitialized()) {
+ mChildrenInitialized = true;
+ // Forcefully initialize all children based on their end time, so that if the start
+ // value of a child is dependent on a previous animation, the animation will be
+ // initialized after the the previous animations have been advanced to the end.
+ skipToEndValue(false);
+ }
+ }
+
+ /**
+ * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
+ * base.
+ * @return
+ * @hide
+ */
+ @Override
+ public boolean doAnimationFrame(long frameTime) {
+ float durationScale = ValueAnimator.getDurationScale();
+ if (durationScale == 0f) {
+ // Duration scale is 0, end the animation right away.
+ forceToEnd();
+ return true;
+ }
+
+ // After the first frame comes in, we need to wait for start delay to pass before updating
+ // any animation values.
+ if (mFirstFrame < 0) {
+ mFirstFrame = frameTime;
+ }
+
+ // Handle pause/resume
+ if (mPaused) {
+ // Note: Child animations don't receive pause events. Since it's never a contract that
+ // the child animators will be paused when set is paused, this is unlikely to be an
+ // issue.
+ mPauseTime = frameTime;
+ removeAnimationCallback();
+ return false;
+ } else if (mPauseTime > 0) {
+ // Offset by the duration that the animation was paused
+ mFirstFrame += (frameTime - mPauseTime);
+ mPauseTime = -1;
+ }
+
+ // Continue at seeked position
+ if (mSeekState.isActive()) {
+ mSeekState.updateSeekDirection(mReversing);
+ if (mReversing) {
+ mFirstFrame = (long) (frameTime - mSeekState.getPlayTime() * durationScale);
+ } else {
+ mFirstFrame = (long) (frameTime - (mSeekState.getPlayTime() + mStartDelay)
+ * durationScale);
+ }
+ mSeekState.reset();
+ }
+
+ if (!mReversing && frameTime < mFirstFrame + mStartDelay * durationScale) {
+ // Still during start delay in a forward playing case.
+ return false;
+ }
+
+ // From here on, we always use unscaled play time. Note this unscaled playtime includes
+ // the start delay.
+ long unscaledPlayTime = (long) ((frameTime - mFirstFrame) / durationScale);
+ mLastFrameTime = frameTime;
+
+ // 1. Pulse the animators that will start or end in this frame
+ // 2. Pulse the animators that will finish in a later frame
+ int latestId = findLatestEventIdForTime(unscaledPlayTime);
+ int startId = mLastEventId;
+
+ handleAnimationEvents(startId, latestId, unscaledPlayTime);
+
+ mLastEventId = latestId;
+
+ // Pump a frame to the on-going animators
+ for (int i = 0; i < mPlayingSet.size(); i++) {
+ Node node = mPlayingSet.get(i);
+ if (!node.mEnded) {
+ pulseFrame(node, getPlayTimeForNode(unscaledPlayTime, node));
+ }
+ }
+
+ // Remove all the finished anims
+ for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
+ if (mPlayingSet.get(i).mEnded) {
+ mPlayingSet.remove(i);
+ }
+ }
+
+ boolean finished = false;
+ if (mReversing) {
+ if (mPlayingSet.size() == 1 && mPlayingSet.get(0) == mRootNode) {
+ // The only animation that is running is the delay animation.
+ finished = true;
+ } else if (mPlayingSet.isEmpty() && mLastEventId < 3) {
+ // The only remaining animation is the delay animation
+ finished = true;
+ }
+ } else {
+ finished = mPlayingSet.isEmpty() && mLastEventId == mEvents.size() - 1;
+ }
+
+ if (finished) {
+ endAnimation();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void commitAnimationFrame(long frameTime) {
+ // No op.
+ }
+
+ @Override
+ boolean pulseAnimationFrame(long frameTime) {
+ return doAnimationFrame(frameTime);
+ }
+
+ /**
+ * When playing forward, we call start() at the animation's scheduled start time, and make sure
+ * to pump a frame at the animation's scheduled end time.
+ *
+ * When playing in reverse, we should reverse the animation when we hit animation's end event,
+ * and expect the animation to end at the its delay ended event, rather than start event.
+ */
+ private void handleAnimationEvents(int startId, int latestId, long playTime) {
+ if (mReversing) {
+ startId = startId == -1 ? mEvents.size() : startId;
+ for (int i = startId - 1; i >= latestId; i--) {
+ AnimationEvent event = mEvents.get(i);
+ Node node = event.mNode;
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ if (node.mAnimation.isStarted()) {
+ // If the animation has already been started before its due time (i.e.
+ // the child animator is being manipulated outside of the AnimatorSet), we
+ // need to cancel the animation to reset the internal state (e.g. frame
+ // time tracking) and remove the self pulsing callbacks
+ node.mAnimation.cancel();
+ }
+ node.mEnded = false;
+ mPlayingSet.add(event.mNode);
+ node.mAnimation.startWithoutPulsing(true);
+ pulseFrame(node, 0);
+ } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED && !node.mEnded) {
+ // end event:
+ pulseFrame(node, getPlayTimeForNode(playTime, node));
+ }
+ }
+ } else {
+ for (int i = startId + 1; i <= latestId; i++) {
+ AnimationEvent event = mEvents.get(i);
+ Node node = event.mNode;
+ if (event.mEvent == AnimationEvent.ANIMATION_START) {
+ mPlayingSet.add(event.mNode);
+ if (node.mAnimation.isStarted()) {
+ // If the animation has already been started before its due time (i.e.
+ // the child animator is being manipulated outside of the AnimatorSet), we
+ // need to cancel the animation to reset the internal state (e.g. frame
+ // time tracking) and remove the self pulsing callbacks
+ node.mAnimation.cancel();
+ }
+ node.mEnded = false;
+ node.mAnimation.startWithoutPulsing(false);
+ pulseFrame(node, 0);
+ } else if (event.mEvent == AnimationEvent.ANIMATION_END && !node.mEnded) {
+ // start event:
+ pulseFrame(node, getPlayTimeForNode(playTime, node));
+ }
+ }
+ }
+ }
+
+ /**
+ * This method pulses frames into child animations. It scales the input animation play time
+ * with the duration scale and pass that to the child animation via pulseAnimationFrame(long).
+ *
+ * @param node child animator node
+ * @param animPlayTime unscaled play time (including start delay) for the child animator
+ */
+ private void pulseFrame(Node node, long animPlayTime) {
+ if (!node.mEnded) {
+ float durationScale = ValueAnimator.getDurationScale();
+ durationScale = durationScale == 0 ? 1 : durationScale;
+ node.mEnded = node.mAnimation.pulseAnimationFrame(
+ (long) (animPlayTime * durationScale));
+ }
+ }
+
+ private long getPlayTimeForNode(long overallPlayTime, Node node) {
+ return getPlayTimeForNode(overallPlayTime, node, mReversing);
+ }
+
+ private long getPlayTimeForNode(long overallPlayTime, Node node, boolean inReverse) {
+ if (inReverse) {
+ overallPlayTime = getTotalDuration() - overallPlayTime;
+ return node.mEndTime - overallPlayTime;
+ } else {
+ return overallPlayTime - node.mStartTime;
+ }
+ }
+
+ private void startAnimation() {
+ addDummyListener();
+
+ // Register animation callback
+ addAnimationCallback(0);
+
+ if (mSeekState.getPlayTimeNormalized() == 0 && mReversing) {
+ // Maintain old behavior, if seeked to 0 then call reverse, we'll treat the case
+ // the same as no seeking at all.
+ mSeekState.reset();
+ }
+ // Set the child animators to the right end:
+ if (mShouldResetValuesAtStart) {
+ if (isInitialized()) {
+ skipToEndValue(!mReversing);
+ } else if (mReversing) {
+ // Reversing but haven't initialized all the children yet.
+ initChildren();
+ skipToEndValue(!mReversing);
+ } else {
+ // If not all children are initialized and play direction is forward
+ for (int i = mEvents.size() - 1; i >= 0; i--) {
+ if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ Animator anim = mEvents.get(i).mNode.mAnimation;
+ // Only reset the animations that have been initialized to start value,
+ // so that if they are defined without a start value, they will get the
+ // values set at the right time (i.e. the next animation run)
+ if (anim.isInitialized()) {
+ anim.skipToEndValue(true);
+ }
+ }
+ }
+ }
+ }
+
+ if (mReversing || mStartDelay == 0 || mSeekState.isActive()) {
+ long playTime;
+ // If no delay, we need to call start on the first animations to be consistent with old
+ // behavior.
+ if (mSeekState.isActive()) {
+ mSeekState.updateSeekDirection(mReversing);
+ playTime = mSeekState.getPlayTime();
+ } else {
+ playTime = 0;
+ }
+ int toId = findLatestEventIdForTime(playTime);
+ handleAnimationEvents(-1, toId, playTime);
+ for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
+ if (mPlayingSet.get(i).mEnded) {
+ mPlayingSet.remove(i);
+ }
+ }
+ mLastEventId = toId;
+ }
+ }
+
+ // This is to work around the issue in b/34736819, as the old behavior in AnimatorSet had
+ // masked a real bug in play movies. TODO: remove this and below once the root cause is fixed.
+ private void addDummyListener() {
+ for (int i = 1; i < mNodes.size(); i++) {
+ mNodes.get(i).mAnimation.addListener(mDummyListener);
+ }
+ }
+
+ private void removeDummyListener() {
+ for (int i = 1; i < mNodes.size(); i++) {
+ mNodes.get(i).mAnimation.removeListener(mDummyListener);
+ }
+ }
+
+ private int findLatestEventIdForTime(long currentPlayTime) {
+ int size = mEvents.size();
+ int latestId = mLastEventId;
+ // Call start on the first animations now to be consistent with the old behavior
+ if (mReversing) {
+ currentPlayTime = getTotalDuration() - currentPlayTime;
+ mLastEventId = mLastEventId == -1 ? size : mLastEventId;
+ for (int j = mLastEventId - 1; j >= 0; j--) {
+ AnimationEvent event = mEvents.get(j);
+ if (event.getTime() >= currentPlayTime) {
+ latestId = j;
+ }
+ }
+ } else {
+ for (int i = mLastEventId + 1; i < size; i++) {
+ AnimationEvent event = mEvents.get(i);
+ if (event.getTime() <= currentPlayTime) {
+ latestId = i;
+ }
+ }
+ }
+ return latestId;
+ }
+
+ private void endAnimation() {
+ mStarted = false;
+ mLastFrameTime = -1;
+ mFirstFrame = -1;
+ mLastEventId = -1;
+ mPaused = false;
+ mPauseTime = -1;
+ mSeekState.reset();
+ mPlayingSet.clear();
+
+ // No longer receive callbacks
+ removeAnimationCallback();
+ // Call end listener
+ if (mListeners != null) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationEnd(this, mReversing);
+ }
+ }
+ removeDummyListener();
+ mSelfPulse = true;
+ mReversing = false;
+ }
+
+ private void removeAnimationCallback() {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.removeCallback(this);
+ }
+
+ private void addAnimationCallback(long delay) {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.addAnimationFrameCallback(this, delay);
+ }
+
+ @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.
+ */
+ final int nodeCount = mNodes.size();
+ anim.mStarted = false;
+ anim.mLastFrameTime = -1;
+ anim.mFirstFrame = -1;
+ anim.mLastEventId = -1;
+ anim.mPaused = false;
+ anim.mPauseTime = -1;
+ anim.mSeekState = new SeekState();
+ anim.mSelfPulse = true;
+ anim.mPlayingSet = new ArrayList<Node>();
+ anim.mNodeMap = new ArrayMap<Animator, Node>();
+ anim.mNodes = new ArrayList<Node>(nodeCount);
+ anim.mEvents = new ArrayList<AnimationEvent>();
+ anim.mDummyListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (anim.mNodeMap.get(animation) == null) {
+ throw new AndroidRuntimeException("Error: animation ended is not in the node"
+ + " map");
+ }
+ anim.mNodeMap.get(animation).mEnded = true;
+
+ }
+ };
+ anim.mReversing = false;
+ anim.mDependencyDirty = true;
+
+ // 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<Node, Node> clonesMap = new HashMap<>(nodeCount);
+ for (int n = 0; n < nodeCount; n++) {
+ final Node node = mNodes.get(n);
+ Node nodeClone = node.clone();
+ // Remove the old internal listener from the cloned child
+ nodeClone.mAnimation.removeListener(mDummyListener);
+ clonesMap.put(node, nodeClone);
+ anim.mNodes.add(nodeClone);
+ anim.mNodeMap.put(nodeClone.mAnimation, nodeClone);
+ }
+
+ anim.mRootNode = clonesMap.get(mRootNode);
+ anim.mDelayAnim = (ValueAnimator) anim.mRootNode.mAnimation;
+
+ // 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 (int i = 0; i < nodeCount; i++) {
+ Node node = mNodes.get(i);
+ // Update dependencies for node's clone
+ Node nodeClone = clonesMap.get(node);
+ nodeClone.mLatestParent = node.mLatestParent == null
+ ? null : clonesMap.get(node.mLatestParent);
+ int size = node.mChildNodes == null ? 0 : node.mChildNodes.size();
+ for (int j = 0; j < size; j++) {
+ nodeClone.mChildNodes.set(j, clonesMap.get(node.mChildNodes.get(j)));
+ }
+ size = node.mSiblings == null ? 0 : node.mSiblings.size();
+ for (int j = 0; j < size; j++) {
+ nodeClone.mSiblings.set(j, clonesMap.get(node.mSiblings.get(j)));
+ }
+ size = node.mParents == null ? 0 : node.mParents.size();
+ for (int j = 0; j < size; j++) {
+ nodeClone.mParents.set(j, clonesMap.get(node.mParents.get(j)));
+ }
+ }
+ return anim;
+ }
+
+
+ /**
+ * AnimatorSet is only reversible when the set contains no sequential animation, and no child
+ * animators have a start delay.
+ * @hide
+ */
+ @Override
+ public boolean canReverse() {
+ return getTotalDuration() != DURATION_INFINITE;
+ }
+
+ /**
+ * Plays the AnimatorSet in reverse. If the animation has been seeked to a specific play time
+ * using {@link #setCurrentPlayTime(long)}, it will play backwards from the point seeked when
+ * reverse was called. Otherwise, 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.
+ * <p>
+ * Note: reverse is not supported for infinite AnimatorSet.
+ */
+ @Override
+ public void reverse() {
+ start(true, true);
+ }
+
+ @Override
+ public String toString() {
+ String returnVal = "AnimatorSet@" + Integer.toHexString(hashCode()) + "{";
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ returnVal += "\n " + node.mAnimation.toString();
+ }
+ return returnVal + "\n}";
+ }
+
+ private void printChildCount() {
+ // Print out the child count through a level traverse.
+ ArrayList<Node> list = new ArrayList<>(mNodes.size());
+ list.add(mRootNode);
+ Log.d(TAG, "Current tree: ");
+ int index = 0;
+ while (index < list.size()) {
+ int listSize = list.size();
+ StringBuilder builder = new StringBuilder();
+ for (; index < listSize; index++) {
+ Node node = list.get(index);
+ int num = 0;
+ if (node.mChildNodes != null) {
+ for (int i = 0; i < node.mChildNodes.size(); i++) {
+ Node child = node.mChildNodes.get(i);
+ if (child.mLatestParent == node) {
+ num++;
+ list.add(child);
+ }
+ }
+ }
+ builder.append(" ");
+ builder.append(num);
+ }
+ Log.d(TAG, builder.toString());
+ }
+ }
+
+ private void createDependencyGraph() {
+ if (!mDependencyDirty) {
+ // Check whether any duration of the child animations has changed
+ boolean durationChanged = false;
+ for (int i = 0; i < mNodes.size(); i++) {
+ Animator anim = mNodes.get(i).mAnimation;
+ if (mNodes.get(i).mTotalDuration != anim.getTotalDuration()) {
+ durationChanged = true;
+ break;
+ }
+ }
+ if (!durationChanged) {
+ return;
+ }
+ }
+
+ mDependencyDirty = false;
+ // Traverse all the siblings and make sure they have all the parents
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ mNodes.get(i).mParentsAdded = false;
+ }
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ if (node.mParentsAdded) {
+ continue;
+ }
+
+ node.mParentsAdded = true;
+ if (node.mSiblings == null) {
+ continue;
+ }
+
+ // Find all the siblings
+ findSiblings(node, node.mSiblings);
+ node.mSiblings.remove(node);
+
+ // Get parents from all siblings
+ int siblingSize = node.mSiblings.size();
+ for (int j = 0; j < siblingSize; j++) {
+ node.addParents(node.mSiblings.get(j).mParents);
+ }
+
+ // Now make sure all siblings share the same set of parents
+ for (int j = 0; j < siblingSize; j++) {
+ Node sibling = node.mSiblings.get(j);
+ sibling.addParents(node.mParents);
+ sibling.mParentsAdded = true;
+ }
+ }
+
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ if (node != mRootNode && node.mParents == null) {
+ node.addParent(mRootNode);
+ }
+ }
+
+ // Do a DFS on the tree
+ ArrayList<Node> visited = new ArrayList<Node>(mNodes.size());
+ // Assign start/end time
+ mRootNode.mStartTime = 0;
+ mRootNode.mEndTime = mDelayAnim.getDuration();
+ updatePlayTime(mRootNode, visited);
+
+ sortAnimationEvents();
+ mTotalDuration = mEvents.get(mEvents.size() - 1).getTime();
+ }
+
+ private void sortAnimationEvents() {
+ // Sort the list of events in ascending order of their time
+ // Create the list including the delay animation.
+ mEvents.clear();
+ for (int i = 1; i < mNodes.size(); i++) {
+ Node node = mNodes.get(i);
+ mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_START));
+ mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_DELAY_ENDED));
+ mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_END));
+ }
+ mEvents.sort(new Comparator<AnimationEvent>() {
+ @Override
+ public int compare(AnimationEvent e1, AnimationEvent e2) {
+ long t1 = e1.getTime();
+ long t2 = e2.getTime();
+ if (t1 == t2) {
+ // For events that happen at the same time, we need them to be in the sequence
+ // (end, start, start delay ended)
+ if (e2.mEvent + e1.mEvent == AnimationEvent.ANIMATION_START
+ + AnimationEvent.ANIMATION_DELAY_ENDED) {
+ // Ensure start delay happens after start
+ return e1.mEvent - e2.mEvent;
+ } else {
+ return e2.mEvent - e1.mEvent;
+ }
+ }
+ if (t2 == DURATION_INFINITE) {
+ return -1;
+ }
+ if (t1 == DURATION_INFINITE) {
+ return 1;
+ }
+ // When neither event happens at INFINITE time:
+ return (int) (t1 - t2);
+ }
+ });
+
+ int eventSize = mEvents.size();
+ // For the same animation, start event has to happen before end.
+ for (int i = 0; i < eventSize;) {
+ AnimationEvent event = mEvents.get(i);
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ boolean needToSwapStart;
+ if (event.mNode.mStartTime == event.mNode.mEndTime) {
+ needToSwapStart = true;
+ } else if (event.mNode.mEndTime == event.mNode.mStartTime
+ + event.mNode.mAnimation.getStartDelay()) {
+ // Swapping start delay
+ needToSwapStart = false;
+ } else {
+ i++;
+ continue;
+ }
+
+ int startEventId = eventSize;
+ int startDelayEndId = eventSize;
+ for (int j = i + 1; j < eventSize; j++) {
+ if (startEventId < eventSize && startDelayEndId < eventSize) {
+ break;
+ }
+ if (mEvents.get(j).mNode == event.mNode) {
+ if (mEvents.get(j).mEvent == AnimationEvent.ANIMATION_START) {
+ // Found start event
+ startEventId = j;
+ } else if (mEvents.get(j).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ startDelayEndId = j;
+ }
+ }
+
+ }
+ if (needToSwapStart && startEventId == mEvents.size()) {
+ throw new UnsupportedOperationException("Something went wrong, no start is"
+ + "found after stop for an animation that has the same start and end"
+ + "time.");
+
+ }
+ if (startDelayEndId == mEvents.size()) {
+ throw new UnsupportedOperationException("Something went wrong, no start"
+ + "delay end is found after stop for an animation");
+
+ }
+
+ // We need to make sure start is inserted before start delay ended event,
+ // because otherwise inserting start delay ended events first would change
+ // the start event index.
+ if (needToSwapStart) {
+ AnimationEvent startEvent = mEvents.remove(startEventId);
+ mEvents.add(i, startEvent);
+ i++;
+ }
+
+ AnimationEvent startDelayEndEvent = mEvents.remove(startDelayEndId);
+ mEvents.add(i, startDelayEndEvent);
+ i += 2;
+ } else {
+ i++;
+ }
+ }
+
+ if (!mEvents.isEmpty() && mEvents.get(0).mEvent != AnimationEvent.ANIMATION_START) {
+ throw new UnsupportedOperationException(
+ "Sorting went bad, the start event should always be at index 0");
+ }
+
+ // Add AnimatorSet's start delay node to the beginning
+ mEvents.add(0, new AnimationEvent(mRootNode, AnimationEvent.ANIMATION_START));
+ mEvents.add(1, new AnimationEvent(mRootNode, AnimationEvent.ANIMATION_DELAY_ENDED));
+ mEvents.add(2, new AnimationEvent(mRootNode, AnimationEvent.ANIMATION_END));
+
+ if (mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_START
+ || mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ throw new UnsupportedOperationException(
+ "Something went wrong, the last event is not an end event");
+ }
+ }
+
+ /**
+ * Based on parent's start/end time, calculate children's start/end time. If cycle exists in
+ * the graph, all the nodes on the cycle will be marked to start at {@link #DURATION_INFINITE},
+ * meaning they will ever play.
+ */
+ private void updatePlayTime(Node parent, ArrayList<Node> visited) {
+ if (parent.mChildNodes == null) {
+ if (parent == mRootNode) {
+ // All the animators are in a cycle
+ for (int i = 0; i < mNodes.size(); i++) {
+ Node node = mNodes.get(i);
+ if (node != mRootNode) {
+ node.mStartTime = DURATION_INFINITE;
+ node.mEndTime = DURATION_INFINITE;
+ }
+ }
+ }
+ return;
+ }
+
+ visited.add(parent);
+ int childrenSize = parent.mChildNodes.size();
+ for (int i = 0; i < childrenSize; i++) {
+ Node child = parent.mChildNodes.get(i);
+ child.mTotalDuration = child.mAnimation.getTotalDuration(); // Update cached duration.
+
+ int index = visited.indexOf(child);
+ if (index >= 0) {
+ // Child has been visited, cycle found. Mark all the nodes in the cycle.
+ for (int j = index; j < visited.size(); j++) {
+ visited.get(j).mLatestParent = null;
+ visited.get(j).mStartTime = DURATION_INFINITE;
+ visited.get(j).mEndTime = DURATION_INFINITE;
+ }
+ child.mStartTime = DURATION_INFINITE;
+ child.mEndTime = DURATION_INFINITE;
+ child.mLatestParent = null;
+ Log.w(TAG, "Cycle found in AnimatorSet: " + this);
+ continue;
+ }
+
+ if (child.mStartTime != DURATION_INFINITE) {
+ if (parent.mEndTime == DURATION_INFINITE) {
+ child.mLatestParent = parent;
+ child.mStartTime = DURATION_INFINITE;
+ child.mEndTime = DURATION_INFINITE;
+ } else {
+ if (parent.mEndTime >= child.mStartTime) {
+ child.mLatestParent = parent;
+ child.mStartTime = parent.mEndTime;
+ }
+
+ child.mEndTime = child.mTotalDuration == DURATION_INFINITE
+ ? DURATION_INFINITE : child.mStartTime + child.mTotalDuration;
+ }
+ }
+ updatePlayTime(child, visited);
+ }
+ visited.remove(parent);
+ }
+
+ // Recursively find all the siblings
+ private void findSiblings(Node node, ArrayList<Node> siblings) {
+ if (!siblings.contains(node)) {
+ siblings.add(node);
+ if (node.mSiblings == null) {
+ return;
+ }
+ for (int i = 0; i < node.mSiblings.size(); i++) {
+ findSiblings(node.mSiblings.get(i), siblings);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ * TODO: For animatorSet defined in XML, we can use a flag to indicate what the play order
+ * if defined (i.e. sequential or together), then we can use the flag instead of calculating
+ * dynamically. Note that when AnimatorSet is empty this method returns true.
+ * @return whether all the animators in the set are supposed to play together
+ */
+ public boolean shouldPlayTogether() {
+ updateAnimatorsDuration();
+ createDependencyGraph();
+ // All the child nodes are set out to play right after the delay animation
+ return mRootNode.mChildNodes == null || mRootNode.mChildNodes.size() == mNodes.size() - 1;
+ }
+
+ @Override
+ public long getTotalDuration() {
+ updateAnimatorsDuration();
+ createDependencyGraph();
+ return mTotalDuration;
+ }
+
+ private Node getNodeForAnimation(Animator anim) {
+ Node node = mNodeMap.get(anim);
+ if (node == null) {
+ node = new Node(anim);
+ mNodeMap.put(anim, node);
+ mNodes.add(node);
+ }
+ return node;
+ }
+
+ /**
+ * 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 {
+ Animator mAnimation;
+
+ /**
+ * Child nodes are the nodes associated with animations that will be played immediately
+ * after current node.
+ */
+ ArrayList<Node> mChildNodes = 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 mEnded and it's time to send out an end event for the entire AnimatorSet.
+ */
+ boolean mEnded = false;
+
+ /**
+ * Nodes with animations that are defined to play simultaneously with the animation
+ * associated with this current node.
+ */
+ ArrayList<Node> mSiblings;
+
+ /**
+ * Parent nodes are the nodes with animations preceding current node's animation. Parent
+ * nodes here are derived from user defined animation sequence.
+ */
+ ArrayList<Node> mParents;
+
+ /**
+ * Latest parent is the parent node associated with a animation that finishes after all
+ * the other parents' animations.
+ */
+ Node mLatestParent = null;
+
+ boolean mParentsAdded = false;
+ long mStartTime = 0;
+ long mEndTime = 0;
+ long mTotalDuration = 0;
+
+ /**
+ * 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.mAnimation = animation;
+ }
+
+ @Override
+ public Node clone() {
+ try {
+ Node node = (Node) super.clone();
+ node.mAnimation = mAnimation.clone();
+ if (mChildNodes != null) {
+ node.mChildNodes = new ArrayList<>(mChildNodes);
+ }
+ if (mSiblings != null) {
+ node.mSiblings = new ArrayList<>(mSiblings);
+ }
+ if (mParents != null) {
+ node.mParents = new ArrayList<>(mParents);
+ }
+ node.mEnded = false;
+ return node;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+
+ void addChild(Node node) {
+ if (mChildNodes == null) {
+ mChildNodes = new ArrayList<>();
+ }
+ if (!mChildNodes.contains(node)) {
+ mChildNodes.add(node);
+ node.addParent(this);
+ }
+ }
+
+ public void addSibling(Node node) {
+ if (mSiblings == null) {
+ mSiblings = new ArrayList<Node>();
+ }
+ if (!mSiblings.contains(node)) {
+ mSiblings.add(node);
+ node.addSibling(this);
+ }
+ }
+
+ public void addParent(Node node) {
+ if (mParents == null) {
+ mParents = new ArrayList<Node>();
+ }
+ if (!mParents.contains(node)) {
+ mParents.add(node);
+ node.addChild(this);
+ }
+ }
+
+ public void addParents(ArrayList<Node> parents) {
+ if (parents == null) {
+ return;
+ }
+ int size = parents.size();
+ for (int i = 0; i < size; i++) {
+ addParent(parents.get(i));
+ }
+ }
+ }
+
+ /**
+ * This class is a wrapper around a node and an event for the animation corresponding to the
+ * node. The 3 types of events represent the start of an animation, the end of a start delay of
+ * an animation, and the end of an animation. When playing forward (i.e. in the non-reverse
+ * direction), start event marks when start() should be called, and end event corresponds to
+ * when the animation should finish. When playing in reverse, start delay will not be a part
+ * of the animation. Therefore, reverse() is called at the end event, and animation should end
+ * at the delay ended event.
+ */
+ private static class AnimationEvent {
+ static final int ANIMATION_START = 0;
+ static final int ANIMATION_DELAY_ENDED = 1;
+ static final int ANIMATION_END = 2;
+ final Node mNode;
+ final int mEvent;
+
+ AnimationEvent(Node node, int event) {
+ mNode = node;
+ mEvent = event;
+ }
+
+ long getTime() {
+ if (mEvent == ANIMATION_START) {
+ return mNode.mStartTime;
+ } else if (mEvent == ANIMATION_DELAY_ENDED) {
+ return mNode.mStartTime == DURATION_INFINITE
+ ? DURATION_INFINITE : mNode.mStartTime + mNode.mAnimation.getStartDelay();
+ } else {
+ return mNode.mEndTime;
+ }
+ }
+
+ public String toString() {
+ String eventStr = mEvent == ANIMATION_START ? "start" : (
+ mEvent == ANIMATION_DELAY_ENDED ? "delay ended" : "end");
+ return eventStr + " " + mNode.mAnimation.toString();
+ }
+ }
+
+ private class SeekState {
+ private long mPlayTime = -1;
+ private boolean mSeekingInReverse = false;
+ void reset() {
+ mPlayTime = -1;
+ mSeekingInReverse = false;
+ }
+
+ void setPlayTime(long playTime, boolean inReverse) {
+ // TODO: This can be simplified.
+
+ // Clamp the play time
+ if (getTotalDuration() != DURATION_INFINITE) {
+ mPlayTime = Math.min(playTime, getTotalDuration() - mStartDelay);
+ }
+ mPlayTime = Math.max(0, mPlayTime);
+ mSeekingInReverse = inReverse;
+ }
+
+ void updateSeekDirection(boolean inReverse) {
+ // Change seek direction without changing the overall fraction
+ if (inReverse && getTotalDuration() == DURATION_INFINITE) {
+ throw new UnsupportedOperationException("Error: Cannot reverse infinite animator"
+ + " set");
+ }
+ if (mPlayTime >= 0) {
+ if (inReverse != mSeekingInReverse) {
+ mPlayTime = getTotalDuration() - mStartDelay - mPlayTime;
+ mSeekingInReverse = inReverse;
+ }
+ }
+ }
+
+ long getPlayTime() {
+ return mPlayTime;
+ }
+
+ /**
+ * Returns the playtime assuming the animation is forward playing
+ */
+ long getPlayTimeNormalized() {
+ if (mReversing) {
+ return getTotalDuration() - mStartDelay - mPlayTime;
+ }
+ return mPlayTime;
+ }
+
+ boolean isActive() {
+ return mPlayTime != -1;
+ }
+ }
+
+ /**
+ * The <code>Builder</code> object is a utility class to facilitate adding animations to a
+ * <code>AnimatorSet</code> along with the relationships between the various animations. The
+ * intention of the <code>Builder</code> methods, along with the {@link
+ * AnimatorSet#play(Animator) play()} method of <code>AnimatorSet</code> is to make it possible
+ * to express the dependency relationships of animations in a natural way. Developers can also
+ * use the {@link AnimatorSet#playTogether(Animator[]) playTogether()} and {@link
+ * AnimatorSet#playSequentially(Animator[]) playSequentially()} methods if these suit the need,
+ * but it might be easier in some situations to express the AnimatorSet of animations in pairs.
+ * <p/>
+ * <p>The <code>Builder</code> object cannot be constructed directly, but is rather constructed
+ * internally via a call to {@link AnimatorSet#play(Animator)}.</p>
+ * <p/>
+ * <p>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:</p>
+ * <pre>
+ * AnimatorSet s = new AnimatorSet();
+ * s.play(anim1).with(anim2);
+ * s.play(anim2).before(anim3);
+ * s.play(anim4).after(anim3);
+ * </pre>
+ * <p/>
+ * <p>Note in the example that both {@link Builder#before(Animator)} and {@link
+ * Builder#after(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.</p>
+ * <p/>
+ * <p>It is possible to make several calls into the same <code>Builder</code> object to express
+ * multiple relationships. However, note that it is only the animation passed into the initial
+ * {@link AnimatorSet#play(Animator)} method that is the dependency in any of the successive
+ * calls to the <code>Builder</code> object. For example, the following code starts both anim2
+ * and anim3 when anim1 ends; there is no direct dependency relationship between anim2 and
+ * anim3:
+ * <pre>
+ * AnimatorSet s = new AnimatorSet();
+ * s.play(anim1).before(anim2).before(anim3);
+ * </pre>
+ * If the desired result is to play anim1 then anim2 then anim3, this code expresses the
+ * relationship correctly:</p>
+ * <pre>
+ * AnimatorSet s = new AnimatorSet();
+ * s.play(anim1).before(anim2);
+ * s.play(anim2).before(anim3);
+ * </pre>
+ * <p/>
+ * <p>Note that it is possible to express relationships that cannot be resolved and will not
+ * result in sensible results. For example, <code>play(anim1).after(anim1)</code> 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.</p>
+ */
+ 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) {
+ mDependencyDirty = true;
+ mCurrentNode = getNodeForAnimation(anim);
+ }
+
+ /**
+ * Sets up the given animation to play at the same time as the animation supplied in the
+ * {@link AnimatorSet#play(Animator)} call that created this <code>Builder</code> object.
+ *
+ * @param anim The animation that will play when the animation supplied to the
+ * {@link AnimatorSet#play(Animator)} method starts.
+ */
+ public Builder with(Animator anim) {
+ Node node = getNodeForAnimation(anim);
+ mCurrentNode.addSibling(node);
+ return this;
+ }
+
+ /**
+ * Sets up the given animation to play when the animation supplied in the
+ * {@link AnimatorSet#play(Animator)} call that created this <code>Builder</code> object
+ * ends.
+ *
+ * @param anim The animation that will play when the animation supplied to the
+ * {@link AnimatorSet#play(Animator)} method ends.
+ */
+ public Builder before(Animator anim) {
+ Node node = getNodeForAnimation(anim);
+ mCurrentNode.addChild(node);
+ return this;
+ }
+
+ /**
+ * Sets up the given animation to play when the animation supplied in the
+ * {@link AnimatorSet#play(Animator)} call that created this <code>Builder</code> 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 AnimatorSet#play(Animator)} method to play.
+ */
+ public Builder after(Animator anim) {
+ Node node = getNodeForAnimation(anim);
+ mCurrentNode.addParent(node);
+ return this;
+ }
+
+ /**
+ * Sets up the animation supplied in the
+ * {@link AnimatorSet#play(Animator)} call that created this <code>Builder</code> 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/android/animation/ArgbEvaluator.java b/android/animation/ArgbEvaluator.java
new file mode 100644
index 00000000..a96bee6a
--- /dev/null
+++ b/android/animation/ArgbEvaluator.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * 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 <code>ArgbEvaluator</code> that may be used in
+ * {@link ValueAnimator#setEvaluator(TypeEvaluator)}. The same instance may
+ * be used in multiple <code>Animator</code>s because it holds no state.
+ * @return An instance of <code>ArgbEvalutor</code>.
+ *
+ * @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;
+ float startA = ((startInt >> 24) & 0xff) / 255.0f;
+ float startR = ((startInt >> 16) & 0xff) / 255.0f;
+ float startG = ((startInt >> 8) & 0xff) / 255.0f;
+ float startB = ( startInt & 0xff) / 255.0f;
+
+ int endInt = (Integer) endValue;
+ float endA = ((endInt >> 24) & 0xff) / 255.0f;
+ float endR = ((endInt >> 16) & 0xff) / 255.0f;
+ float endG = ((endInt >> 8) & 0xff) / 255.0f;
+ float endB = ( endInt & 0xff) / 255.0f;
+
+ // convert from sRGB to linear
+ startR = (float) Math.pow(startR, 2.2);
+ startG = (float) Math.pow(startG, 2.2);
+ startB = (float) Math.pow(startB, 2.2);
+
+ endR = (float) Math.pow(endR, 2.2);
+ endG = (float) Math.pow(endG, 2.2);
+ endB = (float) Math.pow(endB, 2.2);
+
+ // compute the interpolated color in linear space
+ float a = startA + fraction * (endA - startA);
+ float r = startR + fraction * (endR - startR);
+ float g = startG + fraction * (endG - startG);
+ float b = startB + fraction * (endB - startB);
+
+ // convert back to sRGB in the [0..255] range
+ a = a * 255.0f;
+ r = (float) Math.pow(r, 1.0 / 2.2) * 255.0f;
+ g = (float) Math.pow(g, 1.0 / 2.2) * 255.0f;
+ b = (float) Math.pow(b, 1.0 / 2.2) * 255.0f;
+
+ return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
+ }
+} \ No newline at end of file
diff --git a/android/animation/BidirectionalTypeConverter.java b/android/animation/BidirectionalTypeConverter.java
new file mode 100644
index 00000000..960650ed
--- /dev/null
+++ b/android/animation/BidirectionalTypeConverter.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.animation;
+
+/**
+ * Abstract base class used convert type T to another type V and back again. This
+ * is necessary when the value types of in animation are different from the property
+ * type. BidirectionalTypeConverter is needed when only the final value for the
+ * animation is supplied to animators.
+ * @see PropertyValuesHolder#setConverter(TypeConverter)
+ */
+public abstract class BidirectionalTypeConverter<T, V> extends TypeConverter<T, V> {
+ private BidirectionalTypeConverter mInvertedConverter;
+
+ public BidirectionalTypeConverter(Class<T> fromClass, Class<V> toClass) {
+ super(fromClass, toClass);
+ }
+
+ /**
+ * Does a conversion from the target type back to the source type. The subclass
+ * must implement this when a TypeConverter is used in animations and current
+ * values will need to be read for an animation.
+ * @param value The Object to convert.
+ * @return A value of type T, converted from <code>value</code>.
+ */
+ public abstract T convertBack(V value);
+
+ /**
+ * Returns the inverse of this converter, where the from and to classes are reversed.
+ * The inverted converter uses this convert to call {@link #convertBack(Object)} for
+ * {@link #convert(Object)} calls and {@link #convert(Object)} for
+ * {@link #convertBack(Object)} calls.
+ * @return The inverse of this converter, where the from and to classes are reversed.
+ */
+ public BidirectionalTypeConverter<V, T> invert() {
+ if (mInvertedConverter == null) {
+ mInvertedConverter = new InvertedConverter(this);
+ }
+ return mInvertedConverter;
+ }
+
+ private static class InvertedConverter<From, To> extends BidirectionalTypeConverter<From, To> {
+ private BidirectionalTypeConverter<To, From> mConverter;
+
+ public InvertedConverter(BidirectionalTypeConverter<To, From> converter) {
+ super(converter.getTargetType(), converter.getSourceType());
+ mConverter = converter;
+ }
+
+ @Override
+ public From convertBack(To value) {
+ return mConverter.convert(value);
+ }
+
+ @Override
+ public To convert(From value) {
+ return mConverter.convertBack(value);
+ }
+ }
+}
diff --git a/android/animation/FloatArrayEvaluator.java b/android/animation/FloatArrayEvaluator.java
new file mode 100644
index 00000000..9ae1197b
--- /dev/null
+++ b/android/animation/FloatArrayEvaluator.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>float[]</code> values.
+ * Each index into the array is treated as a separate value to interpolate. For example,
+ * evaluating <code>{100, 200}</code> and <code>{300, 400}</code> will interpolate the value at
+ * the first index between 100 and 300 and the value at the second index value between 200 and 400.
+ */
+public class FloatArrayEvaluator implements TypeEvaluator<float[]> {
+
+ private float[] mArray;
+
+ /**
+ * Create a FloatArrayEvaluator that does not reuse the animated value. Care must be taken
+ * when using this option because on every evaluation a new <code>float[]</code> will be
+ * allocated.
+ *
+ * @see #FloatArrayEvaluator(float[])
+ */
+ public FloatArrayEvaluator() {
+ }
+
+ /**
+ * Create a FloatArrayEvaluator that reuses <code>reuseArray</code> for every evaluate() call.
+ * Caution must be taken to ensure that the value returned from
+ * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or
+ * used across threads. The value will be modified on each <code>evaluate()</code> call.
+ *
+ * @param reuseArray The array to modify and return from <code>evaluate</code>.
+ */
+ public FloatArrayEvaluator(float[] reuseArray) {
+ mArray = reuseArray;
+ }
+
+ /**
+ * Interpolates the value at each index by the fraction. If
+ * {@link #FloatArrayEvaluator(float[])} was used to construct this object,
+ * <code>reuseArray</code> will be returned, otherwise a new <code>float[]</code>
+ * will be returned.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value.
+ * @param endValue The end value.
+ * @return A <code>float[]</code> where each element is an interpolation between
+ * the same index in startValue and endValue.
+ */
+ @Override
+ public float[] evaluate(float fraction, float[] startValue, float[] endValue) {
+ float[] array = mArray;
+ if (array == null) {
+ array = new float[startValue.length];
+ }
+
+ for (int i = 0; i < array.length; i++) {
+ float start = startValue[i];
+ float end = endValue[i];
+ array[i] = start + (fraction * (end - start));
+ }
+ return array;
+ }
+}
diff --git a/android/animation/FloatEvaluator.java b/android/animation/FloatEvaluator.java
new file mode 100644
index 00000000..9463aa12
--- /dev/null
+++ b/android/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 android.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>float</code> values.
+ */
+public class FloatEvaluator implements TypeEvaluator<Number> {
+
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * <code>fraction</code> representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>,
+ * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
+ * and <code>t</code> is <code>fraction</code>.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value; should be of type <code>float</code> or
+ * <code>Float</code>
+ * @param endValue The end value; should be of type <code>float</code> or <code>Float</code>
+ * @return A linear interpolation between the start and end values, given the
+ * <code>fraction</code> 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/android/animation/FloatKeyframeSet.java b/android/animation/FloatKeyframeSet.java
new file mode 100644
index 00000000..11837b5c
--- /dev/null
+++ b/android/animation/FloatKeyframeSet.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.animation.Keyframe.FloatKeyframe;
+
+import java.util.List;
+
+/**
+ * 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.
+ *
+ * <p>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.</p>
+ */
+class FloatKeyframeSet extends KeyframeSet implements Keyframes.FloatKeyframes {
+ public FloatKeyframeSet(FloatKeyframe... keyframes) {
+ super(keyframes);
+ }
+
+ @Override
+ public Object getValue(float fraction) {
+ return getFloatValue(fraction);
+ }
+
+ @Override
+ public FloatKeyframeSet clone() {
+ final List<Keyframe> keyframes = mKeyframes;
+ final 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;
+ }
+
+ @Override
+ public float getFloatValue(float fraction) {
+ 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 TimeInterpolator 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 TimeInterpolator 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 TimeInterpolator interpolator = nextKeyframe.getInterpolator();
+ float intervalFraction = (fraction - prevKeyframe.getFraction()) /
+ (nextKeyframe.getFraction() - prevKeyframe.getFraction());
+ float prevValue = prevKeyframe.getFloatValue();
+ float nextValue = nextKeyframe.getFloatValue();
+ // Apply interpolator on the proportional duration.
+ if (interpolator != null) {
+ intervalFraction = interpolator.getInterpolation(intervalFraction);
+ }
+ 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();
+ }
+
+ @Override
+ public Class getType() {
+ return Float.class;
+ }
+}
+
diff --git a/android/animation/IntArrayEvaluator.java b/android/animation/IntArrayEvaluator.java
new file mode 100644
index 00000000..d7f10f3a
--- /dev/null
+++ b/android/animation/IntArrayEvaluator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>int[]</code> values.
+ * Each index into the array is treated as a separate value to interpolate. For example,
+ * evaluating <code>{100, 200}</code> and <code>{300, 400}</code> will interpolate the value at
+ * the first index between 100 and 300 and the value at the second index value between 200 and 400.
+ */
+public class IntArrayEvaluator implements TypeEvaluator<int[]> {
+
+ private int[] mArray;
+
+ /**
+ * Create an IntArrayEvaluator that does not reuse the animated value. Care must be taken
+ * when using this option because on every evaluation a new <code>int[]</code> will be
+ * allocated.
+ *
+ * @see #IntArrayEvaluator(int[])
+ */
+ public IntArrayEvaluator() {
+ }
+
+ /**
+ * Create an IntArrayEvaluator that reuses <code>reuseArray</code> for every evaluate() call.
+ * Caution must be taken to ensure that the value returned from
+ * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or
+ * used across threads. The value will be modified on each <code>evaluate()</code> call.
+ *
+ * @param reuseArray The array to modify and return from <code>evaluate</code>.
+ */
+ public IntArrayEvaluator(int[] reuseArray) {
+ mArray = reuseArray;
+ }
+
+ /**
+ * Interpolates the value at each index by the fraction. If {@link #IntArrayEvaluator(int[])}
+ * was used to construct this object, <code>reuseArray</code> will be returned, otherwise
+ * a new <code>int[]</code> will be returned.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value.
+ * @param endValue The end value.
+ * @return An <code>int[]</code> where each element is an interpolation between
+ * the same index in startValue and endValue.
+ */
+ @Override
+ public int[] evaluate(float fraction, int[] startValue, int[] endValue) {
+ int[] array = mArray;
+ if (array == null) {
+ array = new int[startValue.length];
+ }
+ for (int i = 0; i < array.length; i++) {
+ int start = startValue[i];
+ int end = endValue[i];
+ array[i] = (int) (start + (fraction * (end - start)));
+ }
+ return array;
+ }
+}
diff --git a/android/animation/IntEvaluator.java b/android/animation/IntEvaluator.java
new file mode 100644
index 00000000..34fb0dc5
--- /dev/null
+++ b/android/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 android.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>int</code> values.
+ */
+public class IntEvaluator implements TypeEvaluator<Integer> {
+
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * <code>fraction</code> representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>,
+ * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
+ * and <code>t</code> is <code>fraction</code>.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value; should be of type <code>int</code> or
+ * <code>Integer</code>
+ * @param endValue The end value; should be of type <code>int</code> or <code>Integer</code>
+ * @return A linear interpolation between the start and end values, given the
+ * <code>fraction</code> 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/android/animation/IntKeyframeSet.java b/android/animation/IntKeyframeSet.java
new file mode 100644
index 00000000..f1e146e0
--- /dev/null
+++ b/android/animation/IntKeyframeSet.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.animation.Keyframe.IntKeyframe;
+
+import java.util.List;
+
+/**
+ * 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.
+ *
+ * <p>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.</p>
+ */
+class IntKeyframeSet extends KeyframeSet implements Keyframes.IntKeyframes {
+ public IntKeyframeSet(IntKeyframe... keyframes) {
+ super(keyframes);
+ }
+
+ @Override
+ public Object getValue(float fraction) {
+ return getIntValue(fraction);
+ }
+
+ @Override
+ public IntKeyframeSet clone() {
+ List<Keyframe> 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;
+ }
+
+ @Override
+ public int getIntValue(float fraction) {
+ 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 TimeInterpolator 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 TimeInterpolator 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 TimeInterpolator interpolator = nextKeyframe.getInterpolator();
+ float intervalFraction = (fraction - prevKeyframe.getFraction()) /
+ (nextKeyframe.getFraction() - prevKeyframe.getFraction());
+ int prevValue = prevKeyframe.getIntValue();
+ int nextValue = nextKeyframe.getIntValue();
+ // Apply interpolator on the proportional duration.
+ if (interpolator != null) {
+ intervalFraction = interpolator.getInterpolation(intervalFraction);
+ }
+ 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();
+ }
+
+ @Override
+ public Class getType() {
+ return Integer.class;
+ }
+}
+
diff --git a/android/animation/Keyframe.java b/android/animation/Keyframe.java
new file mode 100644
index 00000000..5483c49a
--- /dev/null
+++ b/android/animation/Keyframe.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * 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.
+ *
+ * <p>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., <code>float</code> and
+ * <code>int</code> 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.</p>
+ */
+public abstract class Keyframe implements Cloneable {
+ /**
+ * 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;
+
+ /**
+ * Flag to indicate whether the value in the keyframe was read from the target object or not.
+ * If so, its value will be recalculated if target changes.
+ */
+ boolean mValueWasSetOnStart;
+
+
+ /**
+ * 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 <code>value</code> 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 TimeInterpolator mInterpolator = null;
+
+
+
+ /**
+ * 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;
+ }
+
+ /**
+ * If the Keyframe's value was acquired from the target object, this flag should be set so that,
+ * if target changes, value will be reset.
+ *
+ * @return boolean Whether this Keyframe's value was retieved from the target object or not.
+ */
+ boolean valueWasSetOnStart() {
+ return mValueWasSetOnStart;
+ }
+
+ void setValueWasSetOnStart(boolean valueWasSetOnStart) {
+ mValueWasSetOnStart = valueWasSetOnStart;
+ }
+
+ /**
+ * 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 <code>null</code> indicates
+ * that there is no interpolation, which is the same as linear interpolation.
+ *
+ * @return The optional interpolator for this Keyframe.
+ */
+ public TimeInterpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * Sets the optional interpolator for this Keyframe. A value of <code>null</code> indicates
+ * that there is no interpolation, which is the same as linear interpolation.
+ *
+ * @return The optional interpolator for this Keyframe.
+ */
+ public void setInterpolator(TimeInterpolator 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(), hasValue() ? mValue : null);
+ kfClone.mValueWasSetOnStart = mValueWasSetOnStart;
+ 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 = mHasValue ?
+ new IntKeyframe(getFraction(), mValue) :
+ new IntKeyframe(getFraction());
+ kfClone.setInterpolator(getInterpolator());
+ kfClone.mValueWasSetOnStart = mValueWasSetOnStart;
+ 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 = mHasValue ?
+ new FloatKeyframe(getFraction(), mValue) :
+ new FloatKeyframe(getFraction());
+ kfClone.setInterpolator(getInterpolator());
+ kfClone.mValueWasSetOnStart = mValueWasSetOnStart;
+ return kfClone;
+ }
+ }
+}
diff --git a/android/animation/KeyframeSet.java b/android/animation/KeyframeSet.java
new file mode 100644
index 00000000..116d0631
--- /dev/null
+++ b/android/animation/KeyframeSet.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.animation.Keyframe.FloatKeyframe;
+import android.animation.Keyframe.IntKeyframe;
+import android.animation.Keyframe.ObjectKeyframe;
+import android.graphics.Path;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 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.
+ * @hide
+ */
+public class KeyframeSet implements Keyframes {
+
+ int mNumKeyframes;
+
+ Keyframe mFirstKeyframe;
+ Keyframe mLastKeyframe;
+ TimeInterpolator mInterpolator; // only used in the 2-keyframe case
+ List<Keyframe> mKeyframes; // only used when there are not 2 keyframes
+ TypeEvaluator mEvaluator;
+
+
+ public KeyframeSet(Keyframe... keyframes) {
+ mNumKeyframes = keyframes.length;
+ // immutable list
+ mKeyframes = Arrays.asList(keyframes);
+ mFirstKeyframe = keyframes[0];
+ mLastKeyframe = keyframes[mNumKeyframes - 1];
+ mInterpolator = mLastKeyframe.getInterpolator();
+ }
+
+ public List<Keyframe> getKeyframes() {
+ return mKeyframes;
+ }
+
+ 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) {
+ boolean badValue = false;
+ 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]);
+ if (Float.isNaN(values[0])) {
+ badValue = true;
+ }
+ } 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]);
+ if (Float.isNaN(values[i])) {
+ badValue = true;
+ }
+ }
+ }
+ if (badValue) {
+ Log.w("Animator", "Bad value (NaN) in float animator");
+ }
+ 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);
+ }
+
+ public static PathKeyframes ofPath(Path path) {
+ return new PathKeyframes(path);
+ }
+
+ public static PathKeyframes ofPath(Path path, float error) {
+ return new PathKeyframes(path, error);
+ }
+
+ /**
+ * 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 Class getType() {
+ return mFirstKeyframe.getType();
+ }
+
+ @Override
+ public KeyframeSet clone() {
+ List<Keyframe> keyframes = mKeyframes;
+ int numKeyframes = mKeyframes.size();
+ final 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 TimeInterpolator 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 TimeInterpolator 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 TimeInterpolator interpolator = nextKeyframe.getInterpolator();
+ final float prevFraction = prevKeyframe.getFraction();
+ float intervalFraction = (fraction - prevFraction) /
+ (nextKeyframe.getFraction() - prevFraction);
+ // Apply interpolator on the proportional duration.
+ if (interpolator != null) {
+ intervalFraction = interpolator.getInterpolation(intervalFraction);
+ }
+ 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/android/animation/Keyframes.java b/android/animation/Keyframes.java
new file mode 100644
index 00000000..66662940
--- /dev/null
+++ b/android/animation/Keyframes.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.animation;
+
+import java.util.List;
+
+/**
+ * This interface abstracts a collection of Keyframe objects and is called by
+ * ValueAnimator to calculate values between those keyframes for a given animation.
+ * @hide
+ */
+public interface Keyframes extends Cloneable {
+
+ /**
+ * Sets the TypeEvaluator to be used when calculating animated values. This object
+ * is required only for Keyframes that are not either IntKeyframes or FloatKeyframes,
+ * 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.
+ */
+ void setEvaluator(TypeEvaluator evaluator);
+
+ /**
+ * @return The value type contained by the contained Keyframes.
+ */
+ Class getType();
+
+ /**
+ * 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.
+ */
+ Object getValue(float fraction);
+
+ /**
+ * @return A list of all Keyframes contained by this. This may return null if this is
+ * not made up of Keyframes.
+ */
+ List<Keyframe> getKeyframes();
+
+ Keyframes clone();
+
+ /**
+ * A specialization of Keyframes that has integer primitive value calculation.
+ */
+ public interface IntKeyframes extends Keyframes {
+
+ /**
+ * Works like {@link #getValue(float)}, but returning a primitive.
+ * @param fraction The elapsed fraction of the animation
+ * @return The animated value.
+ */
+ int getIntValue(float fraction);
+ }
+
+ /**
+ * A specialization of Keyframes that has float primitive value calculation.
+ */
+ public interface FloatKeyframes extends Keyframes {
+
+ /**
+ * Works like {@link #getValue(float)}, but returning a primitive.
+ * @param fraction The elapsed fraction of the animation
+ * @return The animated value.
+ */
+ float getFloatValue(float fraction);
+ }
+}
diff --git a/android/animation/LayoutTransition.java b/android/animation/LayoutTransition.java
new file mode 100644
index 00000000..5a23fddf
--- /dev/null
+++ b/android/animation/LayoutTransition.java
@@ -0,0 +1,1541 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class enables automatic animations on layout changes in ViewGroup objects. To enable
+ * transitions for a layout container, create a LayoutTransition object and set it on any
+ * ViewGroup by calling {@link ViewGroup#setLayoutTransition(LayoutTransition)}. This will cause
+ * default animations to run whenever items are added to or removed from that container. To specify
+ * custom animations, use the {@link LayoutTransition#setAnimator(int, Animator)
+ * setAnimator()} method.
+ *
+ * <p>One of the core concepts of these transition animations is that there are two types of
+ * changes that cause the transition and four different animations that run because of
+ * those changes. The changes that trigger the transition are items being added to a container
+ * (referred to as an "appearing" transition) or removed from a container (also known as
+ * "disappearing"). Setting the visibility of views (between GONE and VISIBLE) will trigger
+ * the same add/remove logic. The animations that run due to those events are one that animates
+ * items being added, one that animates items being removed, and two that animate the other
+ * items in the container that change due to the add/remove occurrence. Users of
+ * the transition may want different animations for the changing items depending on whether
+ * they are changing due to an appearing or disappearing event, so there is one animation for
+ * each of these variations of the changing event. Most of the API of this class is concerned
+ * with setting up the basic properties of the animations used in these four situations,
+ * or with setting up custom animations for any or all of the four.</p>
+ *
+ * <p>By default, the DISAPPEARING animation begins immediately, as does the CHANGE_APPEARING
+ * animation. The other animations begin after a delay that is set to the default duration
+ * of the animations. This behavior facilitates a sequence of animations in transitions as
+ * follows: when an item is being added to a layout, the other children of that container will
+ * move first (thus creating space for the new item), then the appearing animation will run to
+ * animate the item being added. Conversely, when an item is removed from a container, the
+ * animation to remove it will run first, then the animations of the other children in the
+ * layout will run (closing the gap created in the layout when the item was removed). If this
+ * default choreography behavior is not desired, the {@link #setDuration(int, long)} and
+ * {@link #setStartDelay(int, long)} of any or all of the animations can be changed as
+ * appropriate. Keep in mind, however, that if you start an APPEARING animation before a
+ * DISAPPEARING animation is completed, the DISAPPEARING animation stops, and any effects from
+ * the DISAPPEARING animation are reverted. If you instead start a DISAPPEARING animation
+ * before an APPEARING animation is completed, a similar set of effects occurs for the
+ * APPEARING animation.</p>
+ *
+ * <p>The animations specified for the transition, both the defaults and any custom animations
+ * set on the transition object, are templates only. That is, these animations exist to hold the
+ * basic animation properties, such as the duration, start delay, and properties being animated.
+ * But the actual target object, as well as the start and end values for those properties, are
+ * set automatically in the process of setting up the transition each time it runs. Each of the
+ * animations is cloned from the original copy and the clone is then populated with the dynamic
+ * values of the target being animated (such as one of the items in a layout container that is
+ * moving as a result of the layout event) as well as the values that are changing (such as the
+ * position and size of that object). The actual values that are pushed to each animation
+ * depends on what properties are specified for the animation. For example, the default
+ * CHANGE_APPEARING animation animates the <code>left</code>, <code>top</code>, <code>right</code>,
+ * <code>bottom</code>, <code>scrollX</code>, and <code>scrollY</code> properties.
+ * Values for these properties are updated with the pre- and post-layout
+ * values when the transition begins. Custom animations will be similarly populated with
+ * the target and values being animated, assuming they use ObjectAnimator objects with
+ * property names that are known on the target object.</p>
+ *
+ * <p>This class, and the associated XML flag for containers, animateLayoutChanges="true",
+ * provides a simple utility meant for automating changes in straightforward situations.
+ * Using LayoutTransition at multiple levels of a nested view hierarchy may not work due to the
+ * interrelationship of the various levels of layout. Also, a container that is being scrolled
+ * at the same time as items are being added or removed is probably not a good candidate for
+ * this utility, because the before/after locations calculated by LayoutTransition
+ * may not match the actual locations when the animations finish due to the container
+ * being scrolled as the animations are running. You can work around that
+ * particular issue by disabling the 'changing' animations by setting the CHANGE_APPEARING
+ * and CHANGE_DISAPPEARING animations to null, and setting the startDelay of the
+ * other animations appropriately.</p>
+ */
+public class LayoutTransition {
+
+ /**
+ * A flag indicating the animation that runs on those items that are changing
+ * due to a new item appearing in the container.
+ */
+ public static final int CHANGE_APPEARING = 0;
+
+ /**
+ * A flag indicating the animation that runs on those items that are changing
+ * due to an item disappearing from the container.
+ */
+ public static final int CHANGE_DISAPPEARING = 1;
+
+ /**
+ * A flag indicating the animation that runs on those items that are appearing
+ * in the container.
+ */
+ public static final int APPEARING = 2;
+
+ /**
+ * A flag indicating the animation that runs on those items that are disappearing
+ * from the container.
+ */
+ public static final int DISAPPEARING = 3;
+
+ /**
+ * A flag indicating the animation that runs on those items that are changing
+ * due to a layout change not caused by items being added to or removed
+ * from the container. This transition type is not enabled by default; it can be
+ * enabled via {@link #enableTransitionType(int)}.
+ */
+ public static final int CHANGING = 4;
+
+ /**
+ * Private bit fields used to set the collection of enabled transition types for
+ * mTransitionTypes.
+ */
+ private static final int FLAG_APPEARING = 0x01;
+ private static final int FLAG_DISAPPEARING = 0x02;
+ private static final int FLAG_CHANGE_APPEARING = 0x04;
+ private static final int FLAG_CHANGE_DISAPPEARING = 0x08;
+ private static final int FLAG_CHANGING = 0x10;
+
+ /**
+ * These variables hold the animations that are currently used to run the transition effects.
+ * These animations are set to defaults, but can be changed to custom animations by
+ * calls to setAnimator().
+ */
+ private Animator mDisappearingAnim = null;
+ private Animator mAppearingAnim = null;
+ private Animator mChangingAppearingAnim = null;
+ private Animator mChangingDisappearingAnim = null;
+ private Animator mChangingAnim = null;
+
+ /**
+ * These are the default animations, defined in the constructor, that will be used
+ * unless the user specifies custom animations.
+ */
+ private static ObjectAnimator defaultChange;
+ private static ObjectAnimator defaultChangeIn;
+ private static ObjectAnimator defaultChangeOut;
+ private static ObjectAnimator defaultFadeIn;
+ private static ObjectAnimator defaultFadeOut;
+
+ /**
+ * The default duration used by all animations.
+ */
+ private static long DEFAULT_DURATION = 300;
+
+ /**
+ * The durations of the different animations
+ */
+ private long mChangingAppearingDuration = DEFAULT_DURATION;
+ private long mChangingDisappearingDuration = DEFAULT_DURATION;
+ private long mChangingDuration = DEFAULT_DURATION;
+ private long mAppearingDuration = DEFAULT_DURATION;
+ private long mDisappearingDuration = DEFAULT_DURATION;
+
+ /**
+ * The start delays of the different animations. Note that the default behavior of
+ * the appearing item is the default duration, since it should wait for the items to move
+ * before fading it. Same for the changing animation when disappearing; it waits for the item
+ * to fade out before moving the other items.
+ */
+ private long mAppearingDelay = DEFAULT_DURATION;
+ private long mDisappearingDelay = 0;
+ private long mChangingAppearingDelay = 0;
+ private long mChangingDisappearingDelay = DEFAULT_DURATION;
+ private long mChangingDelay = 0;
+
+ /**
+ * The inter-animation delays used on the changing animations
+ */
+ private long mChangingAppearingStagger = 0;
+ private long mChangingDisappearingStagger = 0;
+ private long mChangingStagger = 0;
+
+ /**
+ * Static interpolators - these are stateless and can be shared across the instances
+ */
+ private static TimeInterpolator ACCEL_DECEL_INTERPOLATOR =
+ new AccelerateDecelerateInterpolator();
+ private static TimeInterpolator DECEL_INTERPOLATOR = new DecelerateInterpolator();
+ private static TimeInterpolator sAppearingInterpolator = ACCEL_DECEL_INTERPOLATOR;
+ private static TimeInterpolator sDisappearingInterpolator = ACCEL_DECEL_INTERPOLATOR;
+ private static TimeInterpolator sChangingAppearingInterpolator = DECEL_INTERPOLATOR;
+ private static TimeInterpolator sChangingDisappearingInterpolator = DECEL_INTERPOLATOR;
+ private static TimeInterpolator sChangingInterpolator = DECEL_INTERPOLATOR;
+
+ /**
+ * The default interpolators used for the animations
+ */
+ private TimeInterpolator mAppearingInterpolator = sAppearingInterpolator;
+ private TimeInterpolator mDisappearingInterpolator = sDisappearingInterpolator;
+ private TimeInterpolator mChangingAppearingInterpolator = sChangingAppearingInterpolator;
+ private TimeInterpolator mChangingDisappearingInterpolator = sChangingDisappearingInterpolator;
+ private TimeInterpolator mChangingInterpolator = sChangingInterpolator;
+
+ /**
+ * These hashmaps are used to store the animations that are currently running as part of
+ * the transition. The reason for this is that a further layout event should cause
+ * existing animations to stop where they are prior to starting new animations. So
+ * we cache all of the current animations in this map for possible cancellation on
+ * another layout event. LinkedHashMaps are used to preserve the order in which animations
+ * are inserted, so that we process events (such as setting up start values) in the same order.
+ */
+ private final HashMap<View, Animator> pendingAnimations =
+ new HashMap<View, Animator>();
+ private final LinkedHashMap<View, Animator> currentChangingAnimations =
+ new LinkedHashMap<View, Animator>();
+ private final LinkedHashMap<View, Animator> currentAppearingAnimations =
+ new LinkedHashMap<View, Animator>();
+ private final LinkedHashMap<View, Animator> currentDisappearingAnimations =
+ new LinkedHashMap<View, Animator>();
+
+ /**
+ * This hashmap is used to track the listeners that have been added to the children of
+ * a container. When a layout change occurs, an animation is created for each View, so that
+ * the pre-layout values can be cached in that animation. Then a listener is added to the
+ * view to see whether the layout changes the bounds of that view. If so, the animation
+ * is set with the final values and then run. If not, the animation is not started. When
+ * the process of setting up and running all appropriate animations is done, we need to
+ * remove these listeners and clear out the map.
+ */
+ private final HashMap<View, View.OnLayoutChangeListener> layoutChangeListenerMap =
+ new HashMap<View, View.OnLayoutChangeListener>();
+
+ /**
+ * Used to track the current delay being assigned to successive animations as they are
+ * started. This value is incremented for each new animation, then zeroed before the next
+ * transition begins.
+ */
+ private long staggerDelay;
+
+ /**
+ * These are the types of transition animations that the LayoutTransition is reacting
+ * to. By default, appearing/disappearing and the change animations related to them are
+ * enabled (not CHANGING).
+ */
+ private int mTransitionTypes = FLAG_CHANGE_APPEARING | FLAG_CHANGE_DISAPPEARING |
+ FLAG_APPEARING | FLAG_DISAPPEARING;
+ /**
+ * The set of listeners that should be notified when APPEARING/DISAPPEARING transitions
+ * start and end.
+ */
+ private ArrayList<TransitionListener> mListeners;
+
+ /**
+ * Controls whether changing animations automatically animate the parent hierarchy as well.
+ * This behavior prevents artifacts when wrap_content layouts snap to the end state as the
+ * transition begins, causing visual glitches and clipping.
+ * Default value is true.
+ */
+ private boolean mAnimateParentHierarchy = true;
+
+
+ /**
+ * Constructs a LayoutTransition object. By default, the object will listen to layout
+ * events on any ViewGroup that it is set on and will run default animations for each
+ * type of layout event.
+ */
+ public LayoutTransition() {
+ if (defaultChangeIn == null) {
+ // "left" is just a placeholder; we'll put real properties/values in when needed
+ PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt("left", 0, 1);
+ PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top", 0, 1);
+ PropertyValuesHolder pvhRight = PropertyValuesHolder.ofInt("right", 0, 1);
+ PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt("bottom", 0, 1);
+ PropertyValuesHolder pvhScrollX = PropertyValuesHolder.ofInt("scrollX", 0, 1);
+ PropertyValuesHolder pvhScrollY = PropertyValuesHolder.ofInt("scrollY", 0, 1);
+ defaultChangeIn = ObjectAnimator.ofPropertyValuesHolder((Object)null,
+ pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY);
+ defaultChangeIn.setDuration(DEFAULT_DURATION);
+ defaultChangeIn.setStartDelay(mChangingAppearingDelay);
+ defaultChangeIn.setInterpolator(mChangingAppearingInterpolator);
+ defaultChangeOut = defaultChangeIn.clone();
+ defaultChangeOut.setStartDelay(mChangingDisappearingDelay);
+ defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator);
+ defaultChange = defaultChangeIn.clone();
+ defaultChange.setStartDelay(mChangingDelay);
+ defaultChange.setInterpolator(mChangingInterpolator);
+
+ defaultFadeIn = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
+ defaultFadeIn.setDuration(DEFAULT_DURATION);
+ defaultFadeIn.setStartDelay(mAppearingDelay);
+ defaultFadeIn.setInterpolator(mAppearingInterpolator);
+ defaultFadeOut = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
+ defaultFadeOut.setDuration(DEFAULT_DURATION);
+ defaultFadeOut.setStartDelay(mDisappearingDelay);
+ defaultFadeOut.setInterpolator(mDisappearingInterpolator);
+ }
+ mChangingAppearingAnim = defaultChangeIn;
+ mChangingDisappearingAnim = defaultChangeOut;
+ mChangingAnim = defaultChange;
+ mAppearingAnim = defaultFadeIn;
+ mDisappearingAnim = defaultFadeOut;
+ }
+
+ /**
+ * Sets the duration to be used by all animations of this transition object. If you want to
+ * set the duration of just one of the animations in particular, use the
+ * {@link #setDuration(int, long)} method.
+ *
+ * @param duration The length of time, in milliseconds, that the transition animations
+ * should last.
+ */
+ public void setDuration(long duration) {
+ mChangingAppearingDuration = duration;
+ mChangingDisappearingDuration = duration;
+ mChangingDuration = duration;
+ mAppearingDuration = duration;
+ mDisappearingDuration = duration;
+ }
+
+ /**
+ * Enables the specified transitionType for this LayoutTransition object.
+ * By default, a LayoutTransition listens for changes in children being
+ * added/remove/hidden/shown in the container, and runs the animations associated with
+ * those events. That is, all transition types besides {@link #CHANGING} are enabled by default.
+ * You can also enable {@link #CHANGING} animations by calling this method with the
+ * {@link #CHANGING} transitionType.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}.
+ */
+ public void enableTransitionType(int transitionType) {
+ switch (transitionType) {
+ case APPEARING:
+ mTransitionTypes |= FLAG_APPEARING;
+ break;
+ case DISAPPEARING:
+ mTransitionTypes |= FLAG_DISAPPEARING;
+ break;
+ case CHANGE_APPEARING:
+ mTransitionTypes |= FLAG_CHANGE_APPEARING;
+ break;
+ case CHANGE_DISAPPEARING:
+ mTransitionTypes |= FLAG_CHANGE_DISAPPEARING;
+ break;
+ case CHANGING:
+ mTransitionTypes |= FLAG_CHANGING;
+ break;
+ }
+ }
+
+ /**
+ * Disables the specified transitionType for this LayoutTransition object.
+ * By default, all transition types except {@link #CHANGING} are enabled.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}.
+ */
+ public void disableTransitionType(int transitionType) {
+ switch (transitionType) {
+ case APPEARING:
+ mTransitionTypes &= ~FLAG_APPEARING;
+ break;
+ case DISAPPEARING:
+ mTransitionTypes &= ~FLAG_DISAPPEARING;
+ break;
+ case CHANGE_APPEARING:
+ mTransitionTypes &= ~FLAG_CHANGE_APPEARING;
+ break;
+ case CHANGE_DISAPPEARING:
+ mTransitionTypes &= ~FLAG_CHANGE_DISAPPEARING;
+ break;
+ case CHANGING:
+ mTransitionTypes &= ~FLAG_CHANGING;
+ break;
+ }
+ }
+
+ /**
+ * Returns whether the specified transitionType is enabled for this LayoutTransition object.
+ * By default, all transition types except {@link #CHANGING} are enabled.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}.
+ * @return true if the specified transitionType is currently enabled, false otherwise.
+ */
+ public boolean isTransitionTypeEnabled(int transitionType) {
+ switch (transitionType) {
+ case APPEARING:
+ return (mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING;
+ case DISAPPEARING:
+ return (mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING;
+ case CHANGE_APPEARING:
+ return (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING;
+ case CHANGE_DISAPPEARING:
+ return (mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING;
+ case CHANGING:
+ return (mTransitionTypes & FLAG_CHANGING) == FLAG_CHANGING;
+ }
+ return false;
+ }
+
+ /**
+ * Sets the start delay on one of the animation objects used by this transition. The
+ * <code>transitionType</code> parameter determines the animation whose start delay
+ * is being set.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines
+ * the animation whose start delay is being set.
+ * @param delay The length of time, in milliseconds, to delay before starting the animation.
+ * @see Animator#setStartDelay(long)
+ */
+ public void setStartDelay(int transitionType, long delay) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ mChangingAppearingDelay = delay;
+ break;
+ case CHANGE_DISAPPEARING:
+ mChangingDisappearingDelay = delay;
+ break;
+ case CHANGING:
+ mChangingDelay = delay;
+ break;
+ case APPEARING:
+ mAppearingDelay = delay;
+ break;
+ case DISAPPEARING:
+ mDisappearingDelay = delay;
+ break;
+ }
+ }
+
+ /**
+ * Gets the start delay on one of the animation objects used by this transition. The
+ * <code>transitionType</code> parameter determines the animation whose start delay
+ * is returned.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines
+ * the animation whose start delay is returned.
+ * @return long The start delay of the specified animation.
+ * @see Animator#getStartDelay()
+ */
+ public long getStartDelay(int transitionType) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ return mChangingAppearingDelay;
+ case CHANGE_DISAPPEARING:
+ return mChangingDisappearingDelay;
+ case CHANGING:
+ return mChangingDelay;
+ case APPEARING:
+ return mAppearingDelay;
+ case DISAPPEARING:
+ return mDisappearingDelay;
+ }
+ // shouldn't reach here
+ return 0;
+ }
+
+ /**
+ * Sets the duration on one of the animation objects used by this transition. The
+ * <code>transitionType</code> parameter determines the animation whose duration
+ * is being set.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines
+ * the animation whose duration is being set.
+ * @param duration The length of time, in milliseconds, that the specified animation should run.
+ * @see Animator#setDuration(long)
+ */
+ public void setDuration(int transitionType, long duration) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ mChangingAppearingDuration = duration;
+ break;
+ case CHANGE_DISAPPEARING:
+ mChangingDisappearingDuration = duration;
+ break;
+ case CHANGING:
+ mChangingDuration = duration;
+ break;
+ case APPEARING:
+ mAppearingDuration = duration;
+ break;
+ case DISAPPEARING:
+ mDisappearingDuration = duration;
+ break;
+ }
+ }
+
+ /**
+ * Gets the duration on one of the animation objects used by this transition. The
+ * <code>transitionType</code> parameter determines the animation whose duration
+ * is returned.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines
+ * the animation whose duration is returned.
+ * @return long The duration of the specified animation.
+ * @see Animator#getDuration()
+ */
+ public long getDuration(int transitionType) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ return mChangingAppearingDuration;
+ case CHANGE_DISAPPEARING:
+ return mChangingDisappearingDuration;
+ case CHANGING:
+ return mChangingDuration;
+ case APPEARING:
+ return mAppearingDuration;
+ case DISAPPEARING:
+ return mDisappearingDuration;
+ }
+ // shouldn't reach here
+ return 0;
+ }
+
+ /**
+ * Sets the length of time to delay between starting each animation during one of the
+ * change animations.
+ *
+ * @param transitionType A value of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, or
+ * {@link #CHANGING}.
+ * @param duration The length of time, in milliseconds, to delay before launching the next
+ * animation in the sequence.
+ */
+ public void setStagger(int transitionType, long duration) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ mChangingAppearingStagger = duration;
+ break;
+ case CHANGE_DISAPPEARING:
+ mChangingDisappearingStagger = duration;
+ break;
+ case CHANGING:
+ mChangingStagger = duration;
+ break;
+ // noop other cases
+ }
+ }
+
+ /**
+ * Gets the length of time to delay between starting each animation during one of the
+ * change animations.
+ *
+ * @param transitionType A value of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, or
+ * {@link #CHANGING}.
+ * @return long The length of time, in milliseconds, to delay before launching the next
+ * animation in the sequence.
+ */
+ public long getStagger(int transitionType) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ return mChangingAppearingStagger;
+ case CHANGE_DISAPPEARING:
+ return mChangingDisappearingStagger;
+ case CHANGING:
+ return mChangingStagger;
+ }
+ // shouldn't reach here
+ return 0;
+ }
+
+ /**
+ * Sets the interpolator on one of the animation objects used by this transition. The
+ * <code>transitionType</code> parameter determines the animation whose interpolator
+ * is being set.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines
+ * the animation whose interpolator is being set.
+ * @param interpolator The interpolator that the specified animation should use.
+ * @see Animator#setInterpolator(TimeInterpolator)
+ */
+ public void setInterpolator(int transitionType, TimeInterpolator interpolator) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ mChangingAppearingInterpolator = interpolator;
+ break;
+ case CHANGE_DISAPPEARING:
+ mChangingDisappearingInterpolator = interpolator;
+ break;
+ case CHANGING:
+ mChangingInterpolator = interpolator;
+ break;
+ case APPEARING:
+ mAppearingInterpolator = interpolator;
+ break;
+ case DISAPPEARING:
+ mDisappearingInterpolator = interpolator;
+ break;
+ }
+ }
+
+ /**
+ * Gets the interpolator on one of the animation objects used by this transition. The
+ * <code>transitionType</code> parameter determines the animation whose interpolator
+ * is returned.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines
+ * the animation whose interpolator is being returned.
+ * @return TimeInterpolator The interpolator that the specified animation uses.
+ * @see Animator#setInterpolator(TimeInterpolator)
+ */
+ public TimeInterpolator getInterpolator(int transitionType) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ return mChangingAppearingInterpolator;
+ case CHANGE_DISAPPEARING:
+ return mChangingDisappearingInterpolator;
+ case CHANGING:
+ return mChangingInterpolator;
+ case APPEARING:
+ return mAppearingInterpolator;
+ case DISAPPEARING:
+ return mDisappearingInterpolator;
+ }
+ // shouldn't reach here
+ return null;
+ }
+
+ /**
+ * Sets the animation used during one of the transition types that may run. Any
+ * Animator object can be used, but to be most useful in the context of layout
+ * transitions, the animation should either be a ObjectAnimator or a AnimatorSet
+ * of animations including PropertyAnimators. Also, these ObjectAnimator objects
+ * should be able to get and set values on their target objects automatically. For
+ * example, a ObjectAnimator that animates the property "left" is able to set and get the
+ * <code>left</code> property from the View objects being animated by the layout
+ * transition. The transition works by setting target objects and properties
+ * dynamically, according to the pre- and post-layoout values of those objects, so
+ * having animations that can handle those properties appropriately will work best
+ * for custom animation. The dynamic setting of values is only the case for the
+ * CHANGE animations; the APPEARING and DISAPPEARING animations are simply run with
+ * the values they have.
+ *
+ * <p>It is also worth noting that any and all animations (and their underlying
+ * PropertyValuesHolder objects) will have their start and end values set according
+ * to the pre- and post-layout values. So, for example, a custom animation on "alpha"
+ * as the CHANGE_APPEARING animation will inherit the real value of alpha on the target
+ * object (presumably 1) as its starting and ending value when the animation begins.
+ * Animations which need to use values at the beginning and end that may not match the
+ * values queried when the transition begins may need to use a different mechanism
+ * than a standard ObjectAnimator object.</p>
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the
+ * animation whose animator is being set.
+ * @param animator The animation being assigned. A value of <code>null</code> means that no
+ * animation will be run for the specified transitionType.
+ */
+ public void setAnimator(int transitionType, Animator animator) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ mChangingAppearingAnim = animator;
+ break;
+ case CHANGE_DISAPPEARING:
+ mChangingDisappearingAnim = animator;
+ break;
+ case CHANGING:
+ mChangingAnim = animator;
+ break;
+ case APPEARING:
+ mAppearingAnim = animator;
+ break;
+ case DISAPPEARING:
+ mDisappearingAnim = animator;
+ break;
+ }
+ }
+
+ /**
+ * Gets the animation used during one of the transition types that may run.
+ *
+ * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING},
+ * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines
+ * the animation whose animator is being returned.
+ * @return Animator The animation being used for the given transition type.
+ * @see #setAnimator(int, Animator)
+ */
+ public Animator getAnimator(int transitionType) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ return mChangingAppearingAnim;
+ case CHANGE_DISAPPEARING:
+ return mChangingDisappearingAnim;
+ case CHANGING:
+ return mChangingAnim;
+ case APPEARING:
+ return mAppearingAnim;
+ case DISAPPEARING:
+ return mDisappearingAnim;
+ }
+ // shouldn't reach here
+ return null;
+ }
+
+ /**
+ * This function sets up animations on all of the views that change during layout.
+ * For every child in the parent, we create a change animation of the appropriate
+ * type (appearing, disappearing, or changing) and ask it to populate its start values from its
+ * target view. We add layout listeners to all child views and listen for changes. For
+ * those views that change, we populate the end values for those animations and start them.
+ * Animations are not run on unchanging views.
+ *
+ * @param parent The container which is undergoing a change.
+ * @param newView The view being added to or removed from the parent. May be null if the
+ * changeReason is CHANGING.
+ * @param changeReason A value of APPEARING, DISAPPEARING, or CHANGING, indicating whether the
+ * transition is occurring because an item is being added to or removed from the parent, or
+ * if it is running in response to a layout operation (that is, if the value is CHANGING).
+ */
+ private void runChangeTransition(final ViewGroup parent, View newView, final int changeReason) {
+
+ Animator baseAnimator = null;
+ Animator parentAnimator = null;
+ final long duration;
+ switch (changeReason) {
+ case APPEARING:
+ baseAnimator = mChangingAppearingAnim;
+ duration = mChangingAppearingDuration;
+ parentAnimator = defaultChangeIn;
+ break;
+ case DISAPPEARING:
+ baseAnimator = mChangingDisappearingAnim;
+ duration = mChangingDisappearingDuration;
+ parentAnimator = defaultChangeOut;
+ break;
+ case CHANGING:
+ baseAnimator = mChangingAnim;
+ duration = mChangingDuration;
+ parentAnimator = defaultChange;
+ break;
+ default:
+ // Shouldn't reach here
+ duration = 0;
+ break;
+ }
+ // If the animation is null, there's nothing to do
+ if (baseAnimator == null) {
+ return;
+ }
+
+ // reset the inter-animation delay, in case we use it later
+ staggerDelay = 0;
+
+ final ViewTreeObserver observer = parent.getViewTreeObserver();
+ if (!observer.isAlive()) {
+ // If the observer's not in a good state, skip the transition
+ return;
+ }
+ int numChildren = parent.getChildCount();
+
+ for (int i = 0; i < numChildren; ++i) {
+ final View child = parent.getChildAt(i);
+
+ // only animate the views not being added or removed
+ if (child != newView) {
+ setupChangeAnimation(parent, changeReason, baseAnimator, duration, child);
+ }
+ }
+ if (mAnimateParentHierarchy) {
+ ViewGroup tempParent = parent;
+ while (tempParent != null) {
+ ViewParent parentParent = tempParent.getParent();
+ if (parentParent instanceof ViewGroup) {
+ setupChangeAnimation((ViewGroup)parentParent, changeReason, parentAnimator,
+ duration, tempParent);
+ tempParent = (ViewGroup) parentParent;
+ } else {
+ tempParent = null;
+ }
+
+ }
+ }
+
+ // This is the cleanup step. When we get this rendering event, we know that all of
+ // the appropriate animations have been set up and run. Now we can clear out the
+ // layout listeners.
+ CleanupCallback callback = new CleanupCallback(layoutChangeListenerMap, parent);
+ observer.addOnPreDrawListener(callback);
+ parent.addOnAttachStateChangeListener(callback);
+ }
+
+ /**
+ * This flag controls whether CHANGE_APPEARING or CHANGE_DISAPPEARING animations will
+ * cause the default changing animation to be run on the parent hierarchy as well. This allows
+ * containers of transitioning views to also transition, which may be necessary in situations
+ * where the containers bounds change between the before/after states and may clip their
+ * children during the transition animations. For example, layouts with wrap_content will
+ * adjust their bounds according to the dimensions of their children.
+ *
+ * <p>The default changing transitions animate the bounds and scroll positions of the
+ * target views. These are the animations that will run on the parent hierarchy, not
+ * the custom animations that happen to be set on the transition. This allows custom
+ * behavior for the children of the transitioning container, but uses standard behavior
+ * of resizing/rescrolling on any changing parents.
+ *
+ * @param animateParentHierarchy A boolean value indicating whether the parents of
+ * transitioning views should also be animated during the transition. Default value is true.
+ */
+ public void setAnimateParentHierarchy(boolean animateParentHierarchy) {
+ mAnimateParentHierarchy = animateParentHierarchy;
+ }
+
+ /**
+ * Utility function called by runChangingTransition for both the children and the parent
+ * hierarchy.
+ */
+ private void setupChangeAnimation(final ViewGroup parent, final int changeReason,
+ Animator baseAnimator, final long duration, final View child) {
+
+ // If we already have a listener for this child, then we've already set up the
+ // changing animation we need. Multiple calls for a child may occur when several
+ // add/remove operations are run at once on a container; each one will trigger
+ // changes for the existing children in the container.
+ if (layoutChangeListenerMap.get(child) != null) {
+ return;
+ }
+
+ // Don't animate items up from size(0,0); this is likely because the objects
+ // were offscreen/invisible or otherwise measured to be infinitely small. We don't
+ // want to see them animate into their real size; just ignore animation requests
+ // on these views
+ if (child.getWidth() == 0 && child.getHeight() == 0) {
+ return;
+ }
+
+ // Make a copy of the appropriate animation
+ final Animator anim = baseAnimator.clone();
+
+ // Set the target object for the animation
+ anim.setTarget(child);
+
+ // A ObjectAnimator (or AnimatorSet of them) can extract start values from
+ // its target object
+ anim.setupStartValues();
+
+ // If there's an animation running on this view already, cancel it
+ Animator currentAnimation = pendingAnimations.get(child);
+ if (currentAnimation != null) {
+ currentAnimation.cancel();
+ pendingAnimations.remove(child);
+ }
+ // Cache the animation in case we need to cancel it later
+ pendingAnimations.put(child, anim);
+
+ // For the animations which don't get started, we have to have a means of
+ // removing them from the cache, lest we leak them and their target objects.
+ // We run an animator for the default duration+100 (an arbitrary time, but one
+ // which should far surpass the delay between setting them up here and
+ // handling layout events which start them.
+ ValueAnimator pendingAnimRemover = ValueAnimator.ofFloat(0f, 1f).
+ setDuration(duration + 100);
+ pendingAnimRemover.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ pendingAnimations.remove(child);
+ }
+ });
+ pendingAnimRemover.start();
+
+ // Add a listener to track layout changes on this view. If we don't get a callback,
+ // then there's nothing to animate.
+ final View.OnLayoutChangeListener listener = new View.OnLayoutChangeListener() {
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+
+ // Tell the animation to extract end values from the changed object
+ anim.setupEndValues();
+ if (anim instanceof ValueAnimator) {
+ boolean valuesDiffer = false;
+ ValueAnimator valueAnim = (ValueAnimator)anim;
+ PropertyValuesHolder[] oldValues = valueAnim.getValues();
+ for (int i = 0; i < oldValues.length; ++i) {
+ PropertyValuesHolder pvh = oldValues[i];
+ if (pvh.mKeyframes instanceof KeyframeSet) {
+ KeyframeSet keyframeSet = (KeyframeSet) pvh.mKeyframes;
+ if (keyframeSet.mFirstKeyframe == null ||
+ keyframeSet.mLastKeyframe == null ||
+ !keyframeSet.mFirstKeyframe.getValue().equals(
+ keyframeSet.mLastKeyframe.getValue())) {
+ valuesDiffer = true;
+ }
+ } else if (!pvh.mKeyframes.getValue(0).equals(pvh.mKeyframes.getValue(1))) {
+ valuesDiffer = true;
+ }
+ }
+ if (!valuesDiffer) {
+ return;
+ }
+ }
+
+ long startDelay = 0;
+ switch (changeReason) {
+ case APPEARING:
+ startDelay = mChangingAppearingDelay + staggerDelay;
+ staggerDelay += mChangingAppearingStagger;
+ if (mChangingAppearingInterpolator != sChangingAppearingInterpolator) {
+ anim.setInterpolator(mChangingAppearingInterpolator);
+ }
+ break;
+ case DISAPPEARING:
+ startDelay = mChangingDisappearingDelay + staggerDelay;
+ staggerDelay += mChangingDisappearingStagger;
+ if (mChangingDisappearingInterpolator !=
+ sChangingDisappearingInterpolator) {
+ anim.setInterpolator(mChangingDisappearingInterpolator);
+ }
+ break;
+ case CHANGING:
+ startDelay = mChangingDelay + staggerDelay;
+ staggerDelay += mChangingStagger;
+ if (mChangingInterpolator != sChangingInterpolator) {
+ anim.setInterpolator(mChangingInterpolator);
+ }
+ break;
+ }
+ anim.setStartDelay(startDelay);
+ anim.setDuration(duration);
+
+ Animator prevAnimation = currentChangingAnimations.get(child);
+ if (prevAnimation != null) {
+ prevAnimation.cancel();
+ }
+ Animator pendingAnimation = pendingAnimations.get(child);
+ if (pendingAnimation != null) {
+ pendingAnimations.remove(child);
+ }
+ // Cache the animation in case we need to cancel it later
+ currentChangingAnimations.put(child, anim);
+
+ parent.requestTransitionStart(LayoutTransition.this);
+
+ // this only removes listeners whose views changed - must clear the
+ // other listeners later
+ child.removeOnLayoutChangeListener(this);
+ layoutChangeListenerMap.remove(child);
+ }
+ };
+ // Remove the animation from the cache when it ends
+ anim.addListener(new AnimatorListenerAdapter() {
+
+ @Override
+ public void onAnimationStart(Animator animator) {
+ if (hasListeners()) {
+ ArrayList<TransitionListener> listeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ for (TransitionListener listener : listeners) {
+ listener.startTransition(LayoutTransition.this, parent, child,
+ changeReason == APPEARING ?
+ CHANGE_APPEARING : changeReason == DISAPPEARING ?
+ CHANGE_DISAPPEARING : CHANGING);
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ child.removeOnLayoutChangeListener(listener);
+ layoutChangeListenerMap.remove(child);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ currentChangingAnimations.remove(child);
+ if (hasListeners()) {
+ ArrayList<TransitionListener> listeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ for (TransitionListener listener : listeners) {
+ listener.endTransition(LayoutTransition.this, parent, child,
+ changeReason == APPEARING ?
+ CHANGE_APPEARING : changeReason == DISAPPEARING ?
+ CHANGE_DISAPPEARING : CHANGING);
+ }
+ }
+ }
+ });
+
+ child.addOnLayoutChangeListener(listener);
+ // cache the listener for later removal
+ layoutChangeListenerMap.put(child, listener);
+ }
+
+ /**
+ * Starts the animations set up for a CHANGING transition. We separate the setup of these
+ * animations from actually starting them, to avoid side-effects that starting the animations
+ * may have on the properties of the affected objects. After setup, we tell the affected parent
+ * that this transition should be started. The parent informs its ViewAncestor, which then
+ * starts the transition after the current layout/measurement phase, just prior to drawing
+ * the view hierarchy.
+ *
+ * @hide
+ */
+ public void startChangingAnimations() {
+ LinkedHashMap<View, Animator> currentAnimCopy =
+ (LinkedHashMap<View, Animator>) currentChangingAnimations.clone();
+ for (Animator anim : currentAnimCopy.values()) {
+ if (anim instanceof ObjectAnimator) {
+ ((ObjectAnimator) anim).setCurrentPlayTime(0);
+ }
+ anim.start();
+ }
+ }
+
+ /**
+ * Ends the animations that are set up for a CHANGING transition. This is a variant of
+ * startChangingAnimations() which is called when the window the transition is playing in
+ * is not visible. We need to make sure the animations put their targets in their end states
+ * and that the transition finishes to remove any mid-process state (such as isRunning()).
+ *
+ * @hide
+ */
+ public void endChangingAnimations() {
+ LinkedHashMap<View, Animator> currentAnimCopy =
+ (LinkedHashMap<View, Animator>) currentChangingAnimations.clone();
+ for (Animator anim : currentAnimCopy.values()) {
+ anim.start();
+ anim.end();
+ }
+ // listeners should clean up the currentChangingAnimations list, but just in case...
+ currentChangingAnimations.clear();
+ }
+
+ /**
+ * Returns true if animations are running which animate layout-related properties. This
+ * essentially means that either CHANGE_APPEARING or CHANGE_DISAPPEARING animations
+ * are running, since these animations operate on layout-related properties.
+ *
+ * @return true if CHANGE_APPEARING or CHANGE_DISAPPEARING animations are currently
+ * running.
+ */
+ public boolean isChangingLayout() {
+ return (currentChangingAnimations.size() > 0);
+ }
+
+ /**
+ * Returns true if any of the animations in this transition are currently running.
+ *
+ * @return true if any animations in the transition are running.
+ */
+ public boolean isRunning() {
+ return (currentChangingAnimations.size() > 0 || currentAppearingAnimations.size() > 0 ||
+ currentDisappearingAnimations.size() > 0);
+ }
+
+ /**
+ * Cancels the currently running transition. Note that we cancel() the changing animations
+ * but end() the visibility animations. This is because this method is currently called
+ * in the context of starting a new transition, so we want to move things from their mid-
+ * transition positions, but we want them to have their end-transition visibility.
+ *
+ * @hide
+ */
+ public void cancel() {
+ if (currentChangingAnimations.size() > 0) {
+ LinkedHashMap<View, Animator> currentAnimCopy =
+ (LinkedHashMap<View, Animator>) currentChangingAnimations.clone();
+ for (Animator anim : currentAnimCopy.values()) {
+ anim.cancel();
+ }
+ currentChangingAnimations.clear();
+ }
+ if (currentAppearingAnimations.size() > 0) {
+ LinkedHashMap<View, Animator> currentAnimCopy =
+ (LinkedHashMap<View, Animator>) currentAppearingAnimations.clone();
+ for (Animator anim : currentAnimCopy.values()) {
+ anim.end();
+ }
+ currentAppearingAnimations.clear();
+ }
+ if (currentDisappearingAnimations.size() > 0) {
+ LinkedHashMap<View, Animator> currentAnimCopy =
+ (LinkedHashMap<View, Animator>) currentDisappearingAnimations.clone();
+ for (Animator anim : currentAnimCopy.values()) {
+ anim.end();
+ }
+ currentDisappearingAnimations.clear();
+ }
+ }
+
+ /**
+ * Cancels the specified type of transition. Note that we cancel() the changing animations
+ * but end() the visibility animations. This is because this method is currently called
+ * in the context of starting a new transition, so we want to move things from their mid-
+ * transition positions, but we want them to have their end-transition visibility.
+ *
+ * @hide
+ */
+ public void cancel(int transitionType) {
+ switch (transitionType) {
+ case CHANGE_APPEARING:
+ case CHANGE_DISAPPEARING:
+ case CHANGING:
+ if (currentChangingAnimations.size() > 0) {
+ LinkedHashMap<View, Animator> currentAnimCopy =
+ (LinkedHashMap<View, Animator>) currentChangingAnimations.clone();
+ for (Animator anim : currentAnimCopy.values()) {
+ anim.cancel();
+ }
+ currentChangingAnimations.clear();
+ }
+ break;
+ case APPEARING:
+ if (currentAppearingAnimations.size() > 0) {
+ LinkedHashMap<View, Animator> currentAnimCopy =
+ (LinkedHashMap<View, Animator>) currentAppearingAnimations.clone();
+ for (Animator anim : currentAnimCopy.values()) {
+ anim.end();
+ }
+ currentAppearingAnimations.clear();
+ }
+ break;
+ case DISAPPEARING:
+ if (currentDisappearingAnimations.size() > 0) {
+ LinkedHashMap<View, Animator> currentAnimCopy =
+ (LinkedHashMap<View, Animator>) currentDisappearingAnimations.clone();
+ for (Animator anim : currentAnimCopy.values()) {
+ anim.end();
+ }
+ currentDisappearingAnimations.clear();
+ }
+ break;
+ }
+ }
+
+ /**
+ * This method runs the animation that makes an added item appear.
+ *
+ * @param parent The ViewGroup to which the View is being added.
+ * @param child The View being added to the ViewGroup.
+ */
+ private void runAppearingTransition(final ViewGroup parent, final View child) {
+ Animator currentAnimation = currentDisappearingAnimations.get(child);
+ if (currentAnimation != null) {
+ currentAnimation.cancel();
+ }
+ if (mAppearingAnim == null) {
+ if (hasListeners()) {
+ ArrayList<TransitionListener> listeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ for (TransitionListener listener : listeners) {
+ listener.endTransition(LayoutTransition.this, parent, child, APPEARING);
+ }
+ }
+ return;
+ }
+ Animator anim = mAppearingAnim.clone();
+ anim.setTarget(child);
+ anim.setStartDelay(mAppearingDelay);
+ anim.setDuration(mAppearingDuration);
+ if (mAppearingInterpolator != sAppearingInterpolator) {
+ anim.setInterpolator(mAppearingInterpolator);
+ }
+ if (anim instanceof ObjectAnimator) {
+ ((ObjectAnimator) anim).setCurrentPlayTime(0);
+ }
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ currentAppearingAnimations.remove(child);
+ if (hasListeners()) {
+ ArrayList<TransitionListener> listeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ for (TransitionListener listener : listeners) {
+ listener.endTransition(LayoutTransition.this, parent, child, APPEARING);
+ }
+ }
+ }
+ });
+ currentAppearingAnimations.put(child, anim);
+ anim.start();
+ }
+
+ /**
+ * This method runs the animation that makes a removed item disappear.
+ *
+ * @param parent The ViewGroup from which the View is being removed.
+ * @param child The View being removed from the ViewGroup.
+ */
+ private void runDisappearingTransition(final ViewGroup parent, final View child) {
+ Animator currentAnimation = currentAppearingAnimations.get(child);
+ if (currentAnimation != null) {
+ currentAnimation.cancel();
+ }
+ if (mDisappearingAnim == null) {
+ if (hasListeners()) {
+ ArrayList<TransitionListener> listeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ for (TransitionListener listener : listeners) {
+ listener.endTransition(LayoutTransition.this, parent, child, DISAPPEARING);
+ }
+ }
+ return;
+ }
+ Animator anim = mDisappearingAnim.clone();
+ anim.setStartDelay(mDisappearingDelay);
+ anim.setDuration(mDisappearingDuration);
+ if (mDisappearingInterpolator != sDisappearingInterpolator) {
+ anim.setInterpolator(mDisappearingInterpolator);
+ }
+ anim.setTarget(child);
+ final float preAnimAlpha = child.getAlpha();
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ currentDisappearingAnimations.remove(child);
+ child.setAlpha(preAnimAlpha);
+ if (hasListeners()) {
+ ArrayList<TransitionListener> listeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ for (TransitionListener listener : listeners) {
+ listener.endTransition(LayoutTransition.this, parent, child, DISAPPEARING);
+ }
+ }
+ }
+ });
+ if (anim instanceof ObjectAnimator) {
+ ((ObjectAnimator) anim).setCurrentPlayTime(0);
+ }
+ currentDisappearingAnimations.put(child, anim);
+ anim.start();
+ }
+
+ /**
+ * This method is called by ViewGroup when a child view is about to be added to the
+ * container. This callback starts the process of a transition; we grab the starting
+ * values, listen for changes to all of the children of the container, and start appropriate
+ * animations.
+ *
+ * @param parent The ViewGroup to which the View is being added.
+ * @param child The View being added to the ViewGroup.
+ * @param changesLayout Whether the removal will cause changes in the layout of other views
+ * in the container. INVISIBLE views becoming VISIBLE will not cause changes and thus will not
+ * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations.
+ */
+ private void addChild(ViewGroup parent, View child, boolean changesLayout) {
+ if (parent.getWindowVisibility() != View.VISIBLE) {
+ return;
+ }
+ if ((mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) {
+ // Want disappearing animations to finish up before proceeding
+ cancel(DISAPPEARING);
+ }
+ if (changesLayout && (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING) {
+ // Also, cancel changing animations so that we start fresh ones from current locations
+ cancel(CHANGE_APPEARING);
+ cancel(CHANGING);
+ }
+ if (hasListeners() && (mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) {
+ ArrayList<TransitionListener> listeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ for (TransitionListener listener : listeners) {
+ listener.startTransition(this, parent, child, APPEARING);
+ }
+ }
+ if (changesLayout && (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING) {
+ runChangeTransition(parent, child, APPEARING);
+ }
+ if ((mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) {
+ runAppearingTransition(parent, child);
+ }
+ }
+
+ private boolean hasListeners() {
+ return mListeners != null && mListeners.size() > 0;
+ }
+
+ /**
+ * This method is called by ViewGroup when there is a call to layout() on the container
+ * with this LayoutTransition. If the CHANGING transition is enabled and if there is no other
+ * transition currently running on the container, then this call runs a CHANGING transition.
+ * The transition does not start immediately; it just sets up the mechanism to run if any
+ * of the children of the container change their layout parameters (similar to
+ * the CHANGE_APPEARING and CHANGE_DISAPPEARING transitions).
+ *
+ * @param parent The ViewGroup whose layout() method has been called.
+ *
+ * @hide
+ */
+ public void layoutChange(ViewGroup parent) {
+ if (parent.getWindowVisibility() != View.VISIBLE) {
+ return;
+ }
+ if ((mTransitionTypes & FLAG_CHANGING) == FLAG_CHANGING && !isRunning()) {
+ // This method is called for all calls to layout() in the container, including
+ // those caused by add/remove/hide/show events, which will already have set up
+ // transition animations. Avoid setting up CHANGING animations in this case; only
+ // do so when there is not a transition already running on the container.
+ runChangeTransition(parent, null, CHANGING);
+ }
+ }
+
+ /**
+ * This method is called by ViewGroup when a child view is about to be added to the
+ * container. This callback starts the process of a transition; we grab the starting
+ * values, listen for changes to all of the children of the container, and start appropriate
+ * animations.
+ *
+ * @param parent The ViewGroup to which the View is being added.
+ * @param child The View being added to the ViewGroup.
+ */
+ public void addChild(ViewGroup parent, View child) {
+ addChild(parent, child, true);
+ }
+
+ /**
+ * @deprecated Use {@link #showChild(android.view.ViewGroup, android.view.View, int)}.
+ */
+ @Deprecated
+ public void showChild(ViewGroup parent, View child) {
+ addChild(parent, child, true);
+ }
+
+ /**
+ * This method is called by ViewGroup when a child view is about to be made visible in the
+ * container. This callback starts the process of a transition; we grab the starting
+ * values, listen for changes to all of the children of the container, and start appropriate
+ * animations.
+ *
+ * @param parent The ViewGroup in which the View is being made visible.
+ * @param child The View being made visible.
+ * @param oldVisibility The previous visibility value of the child View, either
+ * {@link View#GONE} or {@link View#INVISIBLE}.
+ */
+ public void showChild(ViewGroup parent, View child, int oldVisibility) {
+ addChild(parent, child, oldVisibility == View.GONE);
+ }
+
+ /**
+ * This method is called by ViewGroup when a child view is about to be removed from the
+ * container. This callback starts the process of a transition; we grab the starting
+ * values, listen for changes to all of the children of the container, and start appropriate
+ * animations.
+ *
+ * @param parent The ViewGroup from which the View is being removed.
+ * @param child The View being removed from the ViewGroup.
+ * @param changesLayout Whether the removal will cause changes in the layout of other views
+ * in the container. Views becoming INVISIBLE will not cause changes and thus will not
+ * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations.
+ */
+ private void removeChild(ViewGroup parent, View child, boolean changesLayout) {
+ if (parent.getWindowVisibility() != View.VISIBLE) {
+ return;
+ }
+ if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) {
+ // Want appearing animations to finish up before proceeding
+ cancel(APPEARING);
+ }
+ if (changesLayout &&
+ (mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) {
+ // Also, cancel changing animations so that we start fresh ones from current locations
+ cancel(CHANGE_DISAPPEARING);
+ cancel(CHANGING);
+ }
+ if (hasListeners() && (mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) {
+ ArrayList<TransitionListener> listeners = (ArrayList<TransitionListener>) mListeners
+ .clone();
+ for (TransitionListener listener : listeners) {
+ listener.startTransition(this, parent, child, DISAPPEARING);
+ }
+ }
+ if (changesLayout &&
+ (mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) {
+ runChangeTransition(parent, child, DISAPPEARING);
+ }
+ if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) {
+ runDisappearingTransition(parent, child);
+ }
+ }
+
+ /**
+ * This method is called by ViewGroup when a child view is about to be removed from the
+ * container. This callback starts the process of a transition; we grab the starting
+ * values, listen for changes to all of the children of the container, and start appropriate
+ * animations.
+ *
+ * @param parent The ViewGroup from which the View is being removed.
+ * @param child The View being removed from the ViewGroup.
+ */
+ public void removeChild(ViewGroup parent, View child) {
+ removeChild(parent, child, true);
+ }
+
+ /**
+ * @deprecated Use {@link #hideChild(android.view.ViewGroup, android.view.View, int)}.
+ */
+ @Deprecated
+ public void hideChild(ViewGroup parent, View child) {
+ removeChild(parent, child, true);
+ }
+
+ /**
+ * This method is called by ViewGroup when a child view is about to be hidden in
+ * container. This callback starts the process of a transition; we grab the starting
+ * values, listen for changes to all of the children of the container, and start appropriate
+ * animations.
+ *
+ * @param parent The parent ViewGroup of the View being hidden.
+ * @param child The View being hidden.
+ * @param newVisibility The new visibility value of the child View, either
+ * {@link View#GONE} or {@link View#INVISIBLE}.
+ */
+ public void hideChild(ViewGroup parent, View child, int newVisibility) {
+ removeChild(parent, child, newVisibility == View.GONE);
+ }
+
+ /**
+ * Add a listener that will be called when the bounds of the view change due to
+ * layout processing.
+ *
+ * @param listener The listener that will be called when layout bounds change.
+ */
+ public void addTransitionListener(TransitionListener listener) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<TransitionListener>();
+ }
+ mListeners.add(listener);
+ }
+
+ /**
+ * Remove a listener for layout changes.
+ *
+ * @param listener The listener for layout bounds change.
+ */
+ public void removeTransitionListener(TransitionListener listener) {
+ if (mListeners == null) {
+ return;
+ }
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Gets the current list of listeners for layout changes.
+ * @return
+ */
+ public List<TransitionListener> getTransitionListeners() {
+ return mListeners;
+ }
+
+ /**
+ * This interface is used for listening to starting and ending events for transitions.
+ */
+ public interface TransitionListener {
+
+ /**
+ * This event is sent to listeners when any type of transition animation begins.
+ *
+ * @param transition The LayoutTransition sending out the event.
+ * @param container The ViewGroup on which the transition is playing.
+ * @param view The View object being affected by the transition animation.
+ * @param transitionType The type of transition that is beginning,
+ * {@link android.animation.LayoutTransition#APPEARING},
+ * {@link android.animation.LayoutTransition#DISAPPEARING},
+ * {@link android.animation.LayoutTransition#CHANGE_APPEARING}, or
+ * {@link android.animation.LayoutTransition#CHANGE_DISAPPEARING}.
+ */
+ public void startTransition(LayoutTransition transition, ViewGroup container,
+ View view, int transitionType);
+
+ /**
+ * This event is sent to listeners when any type of transition animation ends.
+ *
+ * @param transition The LayoutTransition sending out the event.
+ * @param container The ViewGroup on which the transition is playing.
+ * @param view The View object being affected by the transition animation.
+ * @param transitionType The type of transition that is ending,
+ * {@link android.animation.LayoutTransition#APPEARING},
+ * {@link android.animation.LayoutTransition#DISAPPEARING},
+ * {@link android.animation.LayoutTransition#CHANGE_APPEARING}, or
+ * {@link android.animation.LayoutTransition#CHANGE_DISAPPEARING}.
+ */
+ public void endTransition(LayoutTransition transition, ViewGroup container,
+ View view, int transitionType);
+ }
+
+ /**
+ * Utility class to clean up listeners after animations are setup. Cleanup happens
+ * when either the OnPreDrawListener method is called or when the parent is detached,
+ * whichever comes first.
+ */
+ private static final class CleanupCallback implements ViewTreeObserver.OnPreDrawListener,
+ View.OnAttachStateChangeListener {
+
+ final Map<View, View.OnLayoutChangeListener> layoutChangeListenerMap;
+ final ViewGroup parent;
+
+ CleanupCallback(Map<View, View.OnLayoutChangeListener> listenerMap, ViewGroup parent) {
+ this.layoutChangeListenerMap = listenerMap;
+ this.parent = parent;
+ }
+
+ private void cleanup() {
+ parent.getViewTreeObserver().removeOnPreDrawListener(this);
+ parent.removeOnAttachStateChangeListener(this);
+ int count = layoutChangeListenerMap.size();
+ if (count > 0) {
+ Collection<View> views = layoutChangeListenerMap.keySet();
+ for (View view : views) {
+ View.OnLayoutChangeListener listener = layoutChangeListenerMap.get(view);
+ view.removeOnLayoutChangeListener(listener);
+ }
+ layoutChangeListenerMap.clear();
+ }
+ }
+
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ cleanup();
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ cleanup();
+ return true;
+ }
+ };
+
+}
diff --git a/android/animation/ObjectAnimator.java b/android/animation/ObjectAnimator.java
new file mode 100644
index 00000000..1e1f1554
--- /dev/null
+++ b/android/animation/ObjectAnimator.java
@@ -0,0 +1,1017 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.util.Log;
+import android.util.Property;
+import android.view.animation.AccelerateDecelerateInterpolator;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This subclass of {@link 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.
+ *
+ * <p>Animators can be created from either code or resource files, as shown here:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/object_animator.xml ObjectAnimatorResources}
+ *
+ * <p>Starting from API 23, it is possible to use {@link PropertyValuesHolder} and
+ * {@link Keyframe} in resource files to create more complex animations. Using PropertyValuesHolders
+ * allows animators to animate several properties in parallel, as shown in this sample:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh.xml
+ * PropertyValuesHolderResources}
+ *
+ * <p>Using Keyframes allows animations to follow more complex paths from the start
+ * to the end values. Note that you can specify explicit fractional values (from 0 to 1) for
+ * each keyframe to determine when, in the overall duration, the animation should arrive at that
+ * value. Alternatively, you can leave the fractions off and the keyframes will be equally
+ * distributed within the total duration. Also, a keyframe with no value will derive its value
+ * from the target object when the animator starts, just like animators with only one
+ * value specified. In addition, an optional interpolator can be specified. The interpolator will
+ * be applied on the interval between the keyframe that the interpolator is set on and the previous
+ * keyframe. When no interpolator is supplied, the default {@link AccelerateDecelerateInterpolator}
+ * will be used. </p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh_kf_interpolated.xml KeyframeResources}
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about animating with {@code ObjectAnimator}, read the
+ * <a href="{@docRoot}guide/topics/graphics/prop-animation.html#object-animator">Property
+ * Animation</a> developer guide.</p>
+ * </div>
+ *
+ * @see #setPropertyName(String)
+ *
+ */
+public final class ObjectAnimator extends ValueAnimator {
+ private static final String LOG_TAG = "ObjectAnimator";
+
+ private static final boolean DBG = false;
+
+ /**
+ * A weak reference to the target object on which the property exists, set
+ * in the constructor. We'll cancel the animation if this goes away.
+ */
+ private WeakReference<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 <code>foo</code> will result
+ * in a call to the function <code>setFoo()</code> on the target object. If either
+ * <code>valueFrom</code> or <code>valueTo</code> is null, then a getter function will
+ * also be derived and called.
+ *
+ * <p>For best performance of the mechanism that calls the setter function determined by the
+ * name of the property being animated, use <code>float</code> or <code>int</code> typed values,
+ * and make the setter function for those properties have a <code>void</code> 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.</p>
+ *
+ * <p>Note that the setter function derived from this property name
+ * must take the same parameter type as the
+ * <code>valueFrom</code> and <code>valueTo</code> properties, otherwise the call to
+ * the setter function will fail.</p>
+ *
+ * <p>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.</p>
+ *
+ * @param propertyName The name of the property being animated. Should not be null.
+ */
+ public void setPropertyName(@NonNull 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(@NonNull 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 <code>foo</code> will result
+ * in a call to the function <code>setFoo()</code> on the target object. If either
+ * <code>valueFrom</code> or <code>valueTo</code> is null, then a getter function will
+ * also be derived and called.
+ *
+ * <p>If this animator was created with a {@link Property} object instead of the
+ * string name of a property, then this method will return the {@link
+ * Property#getName() name} of that Property object instead. If this animator was
+ * created with one or more {@link PropertyValuesHolder} objects, then this method
+ * will return the {@link PropertyValuesHolder#getPropertyName() name} of that
+ * object (if there was just one) or a comma-separated list of all of the
+ * names (if there are more than one).</p>
+ */
+ @Nullable
+ public String getPropertyName() {
+ String propertyName = null;
+ if (mPropertyName != null) {
+ propertyName = mPropertyName;
+ } else if (mProperty != null) {
+ propertyName = mProperty.getName();
+ } else if (mValues != null && mValues.length > 0) {
+ for (int i = 0; i < mValues.length; ++i) {
+ if (i == 0) {
+ propertyName = "";
+ } else {
+ propertyName += ",";
+ }
+ propertyName += mValues[i].getPropertyName();
+ }
+ }
+ return propertyName;
+ }
+
+ @Override
+ String getNameForTrace() {
+ return "animator:" + getPropertyName();
+ }
+
+ /**
+ * 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 <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> parameter.
+ * @param propertyName The name of the property being animated.
+ */
+ private ObjectAnimator(Object target, String propertyName) {
+ setTarget(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 <T> ObjectAnimator(T target, Property<T, ?> property) {
+ setTarget(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, in which case the start value
+ * will be derived from the property being animated and the target object when {@link #start()}
+ * is called for the first time. Two values imply 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 <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> 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 coordinates along a <code>Path</code>
+ * using two properties. A <code>Path</code></> animation moves in two dimensions, animating
+ * coordinates <code>(x, y)</code> together to follow the line. In this variation, the
+ * coordinates are integers that are set to separate properties designated by
+ * <code>xPropertyName</code> and <code>yPropertyName</code>.
+ *
+ * @param target The object whose properties are to be animated. This object should
+ * have public methods on it called <code>setNameX()</code> and
+ * <code>setNameY</code>, where <code>nameX</code> and <code>nameY</code>
+ * are the value of <code>xPropertyName</code> and <code>yPropertyName</code>
+ * parameters, respectively.
+ * @param xPropertyName The name of the property for the x coordinate being animated.
+ * @param yPropertyName The name of the property for the y coordinate being animated.
+ * @param path The <code>Path</code> to animate values along.
+ * @return An ObjectAnimator object that is set up to animate along <code>path</code>.
+ */
+ public static ObjectAnimator ofInt(Object target, String xPropertyName, String yPropertyName,
+ Path path) {
+ PathKeyframes keyframes = KeyframeSet.ofPath(path);
+ PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xPropertyName,
+ keyframes.createXIntKeyframes());
+ PropertyValuesHolder y = PropertyValuesHolder.ofKeyframes(yPropertyName,
+ keyframes.createYIntKeyframes());
+ return ofPropertyValuesHolder(target, x, y);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between int values. A single
+ * value implies that that value is the one being animated to, in which case the start value
+ * will be derived from the property being animated and the target object when {@link #start()}
+ * is called for the first time. Two values imply 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 <T> ObjectAnimator ofInt(T target, Property<T, Integer> property, int... values) {
+ ObjectAnimator anim = new ObjectAnimator(target, property);
+ anim.setIntValues(values);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code>
+ * using two properties. A <code>Path</code></> animation moves in two dimensions, animating
+ * coordinates <code>(x, y)</code> together to follow the line. In this variation, the
+ * coordinates are integers that are set to separate properties, <code>xProperty</code> and
+ * <code>yProperty</code>.
+ *
+ * @param target The object whose properties are to be animated.
+ * @param xProperty The property for the x coordinate being animated.
+ * @param yProperty The property for the y coordinate being animated.
+ * @param path The <code>Path</code> to animate values along.
+ * @return An ObjectAnimator object that is set up to animate along <code>path</code>.
+ */
+ public static <T> ObjectAnimator ofInt(T target, Property<T, Integer> xProperty,
+ Property<T, Integer> yProperty, Path path) {
+ PathKeyframes keyframes = KeyframeSet.ofPath(path);
+ PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xProperty,
+ keyframes.createXIntKeyframes());
+ PropertyValuesHolder y = PropertyValuesHolder.ofKeyframes(yProperty,
+ keyframes.createYIntKeyframes());
+ return ofPropertyValuesHolder(target, x, y);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates over int values for a multiple
+ * parameters setter. Only public methods that take only int parameters are supported.
+ * Each <code>int[]</code> contains a complete set of parameters to the setter method.
+ * At least two <code>int[]</code> values must be provided, a start and end. 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 may
+ * have a public method on it called <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also
+ * be the case-sensitive complete name of the public setter method.
+ * @param propertyName The name of the property being animated or the name of the setter method.
+ * @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 ofMultiInt(Object target, String propertyName, int[][] values) {
+ PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, values);
+ return ofPropertyValuesHolder(target, pvh);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates the target using a multi-int setter
+ * along the given <code>Path</code>. A <code>Path</code></> animation moves in two dimensions,
+ * animating coordinates <code>(x, y)</code> together to follow the line. In this variation, the
+ * coordinates are integer x and y coordinates used in the first and second parameter of the
+ * setter, respectively.
+ *
+ * @param target The object whose property is to be animated. This object may
+ * have a public method on it called <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also
+ * be the case-sensitive complete name of the public setter method.
+ * @param propertyName The name of the property being animated or the name of the setter method.
+ * @param path The <code>Path</code> to animate values along.
+ * @return An ObjectAnimator object that is set up to animate along <code>path</code>.
+ */
+ public static ObjectAnimator ofMultiInt(Object target, String propertyName, Path path) {
+ PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, path);
+ return ofPropertyValuesHolder(target, pvh);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates over values for a multiple int
+ * parameters setter. Only public methods that take only int parameters are supported.
+ * <p>At least two values must be provided, a start and end. 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).</p>
+ *
+ * @param target The object whose property is to be animated. This object may
+ * have a public method on it called <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also
+ * be the case-sensitive complete name of the public setter method.
+ * @param propertyName The name of the property being animated or the name of the setter method.
+ * @param converter Converts T objects into int parameters for the multi-value setter.
+ * @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.
+ */
+ @SafeVarargs
+ public static <T> ObjectAnimator ofMultiInt(Object target, String propertyName,
+ TypeConverter<T, int[]> converter, TypeEvaluator<T> evaluator, T... values) {
+ PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, converter,
+ evaluator, values);
+ return ObjectAnimator.ofPropertyValuesHolder(target, pvh);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between color values. A single
+ * value implies that that value is the one being animated to, in which case the start value
+ * will be derived from the property being animated and the target object when {@link #start()}
+ * is called for the first time. Two values imply 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 <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> 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 ofArgb(Object target, String propertyName, int... values) {
+ ObjectAnimator animator = ofInt(target, propertyName, values);
+ animator.setEvaluator(ArgbEvaluator.getInstance());
+ return animator;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between color values. A single
+ * value implies that that value is the one being animated to, in which case the start value
+ * will be derived from the property being animated and the target object when {@link #start()}
+ * is called for the first time. Two values imply 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 <T> ObjectAnimator ofArgb(T target, Property<T, Integer> property,
+ int... values) {
+ ObjectAnimator animator = ofInt(target, property, values);
+ animator.setEvaluator(ArgbEvaluator.getInstance());
+ return animator;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between float values. A single
+ * value implies that that value is the one being animated to, in which case the start value
+ * will be derived from the property being animated and the target object when {@link #start()}
+ * is called for the first time. Two values imply 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 <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> 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 coordinates along a <code>Path</code>
+ * using two properties. A <code>Path</code></> animation moves in two dimensions, animating
+ * coordinates <code>(x, y)</code> together to follow the line. In this variation, the
+ * coordinates are floats that are set to separate properties designated by
+ * <code>xPropertyName</code> and <code>yPropertyName</code>.
+ *
+ * @param target The object whose properties are to be animated. This object should
+ * have public methods on it called <code>setNameX()</code> and
+ * <code>setNameY</code>, where <code>nameX</code> and <code>nameY</code>
+ * are the value of the <code>xPropertyName</code> and <code>yPropertyName</code>
+ * parameters, respectively.
+ * @param xPropertyName The name of the property for the x coordinate being animated.
+ * @param yPropertyName The name of the property for the y coordinate being animated.
+ * @param path The <code>Path</code> to animate values along.
+ * @return An ObjectAnimator object that is set up to animate along <code>path</code>.
+ */
+ public static ObjectAnimator ofFloat(Object target, String xPropertyName, String yPropertyName,
+ Path path) {
+ PathKeyframes keyframes = KeyframeSet.ofPath(path);
+ PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xPropertyName,
+ keyframes.createXFloatKeyframes());
+ PropertyValuesHolder y = PropertyValuesHolder.ofKeyframes(yPropertyName,
+ keyframes.createYFloatKeyframes());
+ return ofPropertyValuesHolder(target, x, y);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between float values. A single
+ * value implies that that value is the one being animated to, in which case the start value
+ * will be derived from the property being animated and the target object when {@link #start()}
+ * is called for the first time. Two values imply 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 <T> ObjectAnimator ofFloat(T target, Property<T, Float> property,
+ float... values) {
+ ObjectAnimator anim = new ObjectAnimator(target, property);
+ anim.setFloatValues(values);
+ return anim;
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code>
+ * using two properties. A <code>Path</code></> animation moves in two dimensions, animating
+ * coordinates <code>(x, y)</code> together to follow the line. In this variation, the
+ * coordinates are floats that are set to separate properties, <code>xProperty</code> and
+ * <code>yProperty</code>.
+ *
+ * @param target The object whose properties are to be animated.
+ * @param xProperty The property for the x coordinate being animated.
+ * @param yProperty The property for the y coordinate being animated.
+ * @param path The <code>Path</code> to animate values along.
+ * @return An ObjectAnimator object that is set up to animate along <code>path</code>.
+ */
+ public static <T> ObjectAnimator ofFloat(T target, Property<T, Float> xProperty,
+ Property<T, Float> yProperty, Path path) {
+ PathKeyframes keyframes = KeyframeSet.ofPath(path);
+ PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xProperty,
+ keyframes.createXFloatKeyframes());
+ PropertyValuesHolder y = PropertyValuesHolder.ofKeyframes(yProperty,
+ keyframes.createYFloatKeyframes());
+ return ofPropertyValuesHolder(target, x, y);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates over float values for a multiple
+ * parameters setter. Only public methods that take only float parameters are supported.
+ * Each <code>float[]</code> contains a complete set of parameters to the setter method.
+ * At least two <code>float[]</code> values must be provided, a start and end. 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 may
+ * have a public method on it called <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also
+ * be the case-sensitive complete name of the public setter method.
+ * @param propertyName The name of the property being animated or the name of the setter method.
+ * @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 ofMultiFloat(Object target, String propertyName,
+ float[][] values) {
+ PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, values);
+ return ofPropertyValuesHolder(target, pvh);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates the target using a multi-float setter
+ * along the given <code>Path</code>. A <code>Path</code></> animation moves in two dimensions,
+ * animating coordinates <code>(x, y)</code> together to follow the line. In this variation, the
+ * coordinates are float x and y coordinates used in the first and second parameter of the
+ * setter, respectively.
+ *
+ * @param target The object whose property is to be animated. This object may
+ * have a public method on it called <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also
+ * be the case-sensitive complete name of the public setter method.
+ * @param propertyName The name of the property being animated or the name of the setter method.
+ * @param path The <code>Path</code> to animate values along.
+ * @return An ObjectAnimator object that is set up to animate along <code>path</code>.
+ */
+ public static ObjectAnimator ofMultiFloat(Object target, String propertyName, Path path) {
+ PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, path);
+ return ofPropertyValuesHolder(target, pvh);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates over values for a multiple float
+ * parameters setter. Only public methods that take only float parameters are supported.
+ * <p>At least two values must be provided, a start and end. 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).</p>
+ *
+ * @param target The object whose property is to be animated. This object may
+ * have a public method on it called <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also
+ * be the case-sensitive complete name of the public setter method.
+ * @param propertyName The name of the property being animated or the name of the setter method.
+ * @param converter Converts T objects into float parameters for the multi-value setter.
+ * @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.
+ */
+ @SafeVarargs
+ public static <T> ObjectAnimator ofMultiFloat(Object target, String propertyName,
+ TypeConverter<T, float[]> converter, TypeEvaluator<T> evaluator, T... values) {
+ PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, converter,
+ evaluator, values);
+ return ObjectAnimator.ofPropertyValuesHolder(target, pvh);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between Object values. A single
+ * value implies that that value is the one being animated to, in which case the start value
+ * will be derived from the property being animated and the target object when {@link #start()}
+ * is called for the first time. Two values imply 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).
+ *
+ * <p><strong>Note:</strong> The values are stored as references to the original
+ * objects, which means that changes to those objects after this method is called will
+ * affect the values on the animator. If the objects will be mutated externally after
+ * this method is called, callers should pass a copy of those objects instead.
+ *
+ * @param target The object whose property is to be animated. This object should
+ * have a public method on it called <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> 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 a property along a <code>Path</code>.
+ * A <code>Path</code></> animation moves in two dimensions, animating coordinates
+ * <code>(x, y)</code> together to follow the line. This variant animates the coordinates
+ * in a <code>PointF</code> to follow the <code>Path</code>. If the <code>Property</code>
+ * associated with <code>propertyName</code> uses a type other than <code>PointF</code>,
+ * <code>converter</code> can be used to change from <code>PointF</code> to the type
+ * associated with the <code>Property</code>.
+ *
+ * @param target The object whose property is to be animated. This object should
+ * have a public method on it called <code>setName()</code>, where <code>name</code> is
+ * the value of the <code>propertyName</code> parameter.
+ * @param propertyName The name of the property being animated.
+ * @param converter Converts a PointF to the type associated with the setter. May be
+ * null if conversion is unnecessary.
+ * @param path The <code>Path</code> to animate values along.
+ * @return An ObjectAnimator object that is set up to animate along <code>path</code>.
+ */
+ @NonNull
+ public static ObjectAnimator ofObject(Object target, String propertyName,
+ @Nullable TypeConverter<PointF, ?> converter, Path path) {
+ PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(propertyName, converter, path);
+ return ofPropertyValuesHolder(target, pvh);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between Object values. A single
+ * value implies that that value is the one being animated to, in which case the start value
+ * will be derived from the property being animated and the target object when {@link #start()}
+ * is called for the first time. Two values imply 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).
+ *
+ * <p><strong>Note:</strong> The values are stored as references to the original
+ * objects, which means that changes to those objects after this method is called will
+ * affect the values on the animator. If the objects will be mutated externally after
+ * this method is called, callers should pass a copy of those objects instead.
+ *
+ * @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.
+ */
+ @NonNull
+ @SafeVarargs
+ public static <T, V> ObjectAnimator ofObject(T target, Property<T, V> property,
+ TypeEvaluator<V> 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 Object values. A single
+ * value implies that that value is the one being animated to, in which case the start value
+ * will be derived from the property being animated and the target object when {@link #start()}
+ * is called for the first time. Two values imply 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).
+ * This variant supplies a <code>TypeConverter</code> to convert from the animated values to the
+ * type of the property. If only one value is supplied, the <code>TypeConverter</code> must be a
+ * {@link android.animation.BidirectionalTypeConverter} to retrieve the current value.
+ *
+ * <p><strong>Note:</strong> The values are stored as references to the original
+ * objects, which means that changes to those objects after this method is called will
+ * affect the values on the animator. If the objects will be mutated externally after
+ * this method is called, callers should pass a copy of those objects instead.
+ *
+ * @param target The object whose property is to be animated.
+ * @param property The property being animated.
+ * @param converter Converts the animated object to the Property type.
+ * @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.
+ */
+ @NonNull
+ @SafeVarargs
+ public static <T, V, P> ObjectAnimator ofObject(T target, Property<T, P> property,
+ TypeConverter<V, P> converter, TypeEvaluator<V> evaluator, V... values) {
+ PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(property, converter, evaluator,
+ values);
+ return ofPropertyValuesHolder(target, pvh);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates a property along a <code>Path</code>.
+ * A <code>Path</code></> animation moves in two dimensions, animating coordinates
+ * <code>(x, y)</code> together to follow the line. This variant animates the coordinates
+ * in a <code>PointF</code> to follow the <code>Path</code>. If <code>property</code>
+ * uses a type other than <code>PointF</code>, <code>converter</code> can be used to change
+ * from <code>PointF</code> to the type associated with the <code>Property</code>.
+ *
+ * <p>The PointF passed to <code>converter</code> or <code>property</code>, if
+ * <code>converter</code> is <code>null</code>, is reused on each animation frame and should
+ * not be stored by the setter or TypeConverter.</p>
+ *
+ * @param target The object whose property is to be animated.
+ * @param property The property being animated. Should not be null.
+ * @param converter Converts a PointF to the type associated with the setter. May be
+ * null if conversion is unnecessary.
+ * @param path The <code>Path</code> to animate values along.
+ * @return An ObjectAnimator object that is set up to animate along <code>path</code>.
+ */
+ @NonNull
+ public static <T, V> ObjectAnimator ofObject(T target, @NonNull Property<T, V> property,
+ @Nullable TypeConverter<PointF, V> converter, Path path) {
+ PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(property, converter, path);
+ return ofPropertyValuesHolder(target, pvh);
+ }
+
+ /**
+ * Constructs and returns an ObjectAnimator that animates between the sets of values specified
+ * in <code>PropertyValueHolder</code> 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 <code>setName()</code>, where <code>name</code> is the name of
+ * the property passed in as the <code>propertyName</code> 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.
+ */
+ @NonNull
+ public static ObjectAnimator ofPropertyValuesHolder(Object target,
+ PropertyValuesHolder... values) {
+ ObjectAnimator anim = new ObjectAnimator();
+ anim.setTarget(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(@Nullable 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() {
+ AnimationHandler.getInstance().autoCancelBasedOn(this);
+ if (DBG) {
+ Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
+ for (int i = 0; i < mValues.length; ++i) {
+ PropertyValuesHolder pvh = mValues[i];
+ Log.d(LOG_TAG, " Values[" + i + "]: " +
+ pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
+ pvh.mKeyframes.getValue(1));
+ }
+ }
+ super.start();
+ }
+
+ boolean shouldAutoCancel(AnimationHandler.AnimationFrameCallback anim) {
+ if (anim == null) {
+ return false;
+ }
+
+ if (anim instanceof ObjectAnimator) {
+ ObjectAnimator objAnim = (ObjectAnimator) anim;
+ if (objAnim.mAutoCancel && hasSameTargetAndProperties(objAnim)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This function is called immediately before processing the first animation
+ * frame of an animation. If there is a nonzero <code>startDelay</code>, 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.
+ *
+ * <p>Overriders of this method should call the superclass method to cause
+ * internal mechanisms to be set up correctly.</p>
+ */
+ @CallSuper
+ @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.
+ final Object target = getTarget();
+ if (target != null) {
+ final int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].setupSetterAndGetter(target);
+ }
+ }
+ 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
+ * <code>ObjectAnimator.ofInt(target, propertyName, 0, 10).setDuration(500).start()</code>.
+ */
+ @Override
+ @NonNull
+ 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
+ */
+ @Nullable
+ public Object getTarget() {
+ return mTarget == null ? null : mTarget.get();
+ }
+
+ @Override
+ public void setTarget(@Nullable Object target) {
+ final Object oldTarget = getTarget();
+ if (oldTarget != target) {
+ if (isStarted()) {
+ cancel();
+ }
+ mTarget = target == null ? null : new WeakReference<Object>(target);
+ // New target should cause re-initialization prior to starting
+ mInitialized = false;
+ }
+ }
+
+ @Override
+ public void setupStartValues() {
+ initAnimation();
+
+ final Object target = getTarget();
+ if (target != null) {
+ final int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].setupStartValue(target);
+ }
+ }
+ }
+
+ @Override
+ public void setupEndValues() {
+ initAnimation();
+
+ final Object target = getTarget();
+ if (target != null) {
+ final int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].setupEndValue(target);
+ }
+ }
+ }
+
+ /**
+ * 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 <code>end()</code>
+ * function is called, to set the final value on the property.
+ *
+ * <p>Overrides of this method must call the superclass to perform the calculation
+ * of the animated value.</p>
+ *
+ * @param fraction The elapsed fraction of the animation.
+ */
+ @CallSuper
+ @Override
+ void animateValue(float fraction) {
+ final Object target = getTarget();
+ if (mTarget != null && target == null) {
+ // We lost the target reference, cancel and clean up. Note: we allow null target if the
+ /// target has never been set.
+ cancel();
+ return;
+ }
+
+ super.animateValue(fraction);
+ int numValues = mValues.length;
+ for (int i = 0; i < numValues; ++i) {
+ mValues[i].setAnimatedValue(target);
+ }
+ }
+
+ @Override
+ boolean isInitialized() {
+ return mInitialized;
+ }
+
+ @Override
+ public ObjectAnimator clone() {
+ final ObjectAnimator anim = (ObjectAnimator) super.clone();
+ return anim;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ String returnVal = "ObjectAnimator@" + Integer.toHexString(hashCode()) + ", target " +
+ getTarget();
+ if (mValues != null) {
+ for (int i = 0; i < mValues.length; ++i) {
+ returnVal += "\n " + mValues[i].toString();
+ }
+ }
+ return returnVal;
+ }
+}
diff --git a/android/animation/PathKeyframes.java b/android/animation/PathKeyframes.java
new file mode 100644
index 00000000..b362904b
--- /dev/null
+++ b/android/animation/PathKeyframes.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.animation;
+
+import android.graphics.Path;
+import android.graphics.PointF;
+
+import java.util.ArrayList;
+
+/**
+ * PathKeyframes relies on approximating the Path as a series of line segments.
+ * The line segments are recursively divided until there is less than 1/2 pixel error
+ * between the lines and the curve. Each point of the line segment is converted
+ * to a Keyframe and a linear interpolation between Keyframes creates a good approximation
+ * of the curve.
+ * <p>
+ * PathKeyframes is optimized to reduce the number of objects created when there are
+ * many keyframes for a curve.
+ * </p>
+ * <p>
+ * Typically, the returned type is a PointF, but the individual components can be extracted
+ * as either an IntKeyframes or FloatKeyframes.
+ * </p>
+ * @hide
+ */
+public class PathKeyframes implements Keyframes {
+ private static final int FRACTION_OFFSET = 0;
+ private static final int X_OFFSET = 1;
+ private static final int Y_OFFSET = 2;
+ private static final int NUM_COMPONENTS = 3;
+ private static final ArrayList<Keyframe> EMPTY_KEYFRAMES = new ArrayList<Keyframe>();
+
+ private PointF mTempPointF = new PointF();
+ private float[] mKeyframeData;
+
+ public PathKeyframes(Path path) {
+ this(path, 0.5f);
+ }
+
+ public PathKeyframes(Path path, float error) {
+ if (path == null || path.isEmpty()) {
+ throw new IllegalArgumentException("The path must not be null or empty");
+ }
+ mKeyframeData = path.approximate(error);
+ }
+
+ @Override
+ public ArrayList<Keyframe> getKeyframes() {
+ return EMPTY_KEYFRAMES;
+ }
+
+ @Override
+ public Object getValue(float fraction) {
+ int numPoints = mKeyframeData.length / 3;
+ if (fraction < 0) {
+ return interpolateInRange(fraction, 0, 1);
+ } else if (fraction > 1) {
+ return interpolateInRange(fraction, numPoints - 2, numPoints - 1);
+ } else if (fraction == 0) {
+ return pointForIndex(0);
+ } else if (fraction == 1) {
+ return pointForIndex(numPoints - 1);
+ } else {
+ // Binary search for the correct section
+ int low = 0;
+ int high = numPoints - 1;
+
+ while (low <= high) {
+ int mid = (low + high) / 2;
+ float midFraction = mKeyframeData[(mid * NUM_COMPONENTS) + FRACTION_OFFSET];
+
+ if (fraction < midFraction) {
+ high = mid - 1;
+ } else if (fraction > midFraction) {
+ low = mid + 1;
+ } else {
+ return pointForIndex(mid);
+ }
+ }
+
+ // now high is below the fraction and low is above the fraction
+ return interpolateInRange(fraction, high, low);
+ }
+ }
+
+ private PointF interpolateInRange(float fraction, int startIndex, int endIndex) {
+ int startBase = (startIndex * NUM_COMPONENTS);
+ int endBase = (endIndex * NUM_COMPONENTS);
+
+ float startFraction = mKeyframeData[startBase + FRACTION_OFFSET];
+ float endFraction = mKeyframeData[endBase + FRACTION_OFFSET];
+
+ float intervalFraction = (fraction - startFraction)/(endFraction - startFraction);
+
+ float startX = mKeyframeData[startBase + X_OFFSET];
+ float endX = mKeyframeData[endBase + X_OFFSET];
+ float startY = mKeyframeData[startBase + Y_OFFSET];
+ float endY = mKeyframeData[endBase + Y_OFFSET];
+
+ float x = interpolate(intervalFraction, startX, endX);
+ float y = interpolate(intervalFraction, startY, endY);
+
+ mTempPointF.set(x, y);
+ return mTempPointF;
+ }
+
+ @Override
+ public void setEvaluator(TypeEvaluator evaluator) {
+ }
+
+ @Override
+ public Class getType() {
+ return PointF.class;
+ }
+
+ @Override
+ public Keyframes clone() {
+ Keyframes clone = null;
+ try {
+ clone = (Keyframes) super.clone();
+ } catch (CloneNotSupportedException e) {}
+ return clone;
+ }
+
+ private PointF pointForIndex(int index) {
+ int base = (index * NUM_COMPONENTS);
+ int xOffset = base + X_OFFSET;
+ int yOffset = base + Y_OFFSET;
+ mTempPointF.set(mKeyframeData[xOffset], mKeyframeData[yOffset]);
+ return mTempPointF;
+ }
+
+ private static float interpolate(float fraction, float startValue, float endValue) {
+ float diff = endValue - startValue;
+ return startValue + (diff * fraction);
+ }
+
+ /**
+ * Returns a FloatKeyframes for the X component of the Path.
+ * @return a FloatKeyframes for the X component of the Path.
+ */
+ public FloatKeyframes createXFloatKeyframes() {
+ return new FloatKeyframesBase() {
+ @Override
+ public float getFloatValue(float fraction) {
+ PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
+ return pointF.x;
+ }
+ };
+ }
+
+ /**
+ * Returns a FloatKeyframes for the Y component of the Path.
+ * @return a FloatKeyframes for the Y component of the Path.
+ */
+ public FloatKeyframes createYFloatKeyframes() {
+ return new FloatKeyframesBase() {
+ @Override
+ public float getFloatValue(float fraction) {
+ PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
+ return pointF.y;
+ }
+ };
+ }
+
+ /**
+ * Returns an IntKeyframes for the X component of the Path.
+ * @return an IntKeyframes for the X component of the Path.
+ */
+ public IntKeyframes createXIntKeyframes() {
+ return new IntKeyframesBase() {
+ @Override
+ public int getIntValue(float fraction) {
+ PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
+ return Math.round(pointF.x);
+ }
+ };
+ }
+
+ /**
+ * Returns an IntKeyframeSet for the Y component of the Path.
+ * @return an IntKeyframeSet for the Y component of the Path.
+ */
+ public IntKeyframes createYIntKeyframes() {
+ return new IntKeyframesBase() {
+ @Override
+ public int getIntValue(float fraction) {
+ PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
+ return Math.round(pointF.y);
+ }
+ };
+ }
+
+ private abstract static class SimpleKeyframes implements Keyframes {
+ @Override
+ public void setEvaluator(TypeEvaluator evaluator) {
+ }
+
+ @Override
+ public ArrayList<Keyframe> getKeyframes() {
+ return EMPTY_KEYFRAMES;
+ }
+
+ @Override
+ public Keyframes clone() {
+ Keyframes clone = null;
+ try {
+ clone = (Keyframes) super.clone();
+ } catch (CloneNotSupportedException e) {}
+ return clone;
+ }
+ }
+
+ abstract static class IntKeyframesBase extends SimpleKeyframes implements IntKeyframes {
+ @Override
+ public Class getType() {
+ return Integer.class;
+ }
+
+ @Override
+ public Object getValue(float fraction) {
+ return getIntValue(fraction);
+ }
+ }
+
+ abstract static class FloatKeyframesBase extends SimpleKeyframes
+ implements FloatKeyframes {
+ @Override
+ public Class getType() {
+ return Float.class;
+ }
+
+ @Override
+ public Object getValue(float fraction) {
+ return getFloatValue(fraction);
+ }
+ }
+}
diff --git a/android/animation/PointFEvaluator.java b/android/animation/PointFEvaluator.java
new file mode 100644
index 00000000..91d501fc
--- /dev/null
+++ b/android/animation/PointFEvaluator.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.animation;
+
+import android.graphics.PointF;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>PointF</code> values.
+ */
+public class PointFEvaluator implements TypeEvaluator<PointF> {
+
+ /**
+ * When null, a new PointF is returned on every evaluate call. When non-null,
+ * mPoint will be modified and returned on every evaluate.
+ */
+ private PointF mPoint;
+
+ /**
+ * Construct a PointFEvaluator that returns a new PointF on every evaluate call.
+ * To avoid creating an object for each evaluate call,
+ * {@link PointFEvaluator#PointFEvaluator(android.graphics.PointF)} should be used
+ * whenever possible.
+ */
+ public PointFEvaluator() {
+ }
+
+ /**
+ * Constructs a PointFEvaluator that modifies and returns <code>reuse</code>
+ * in {@link #evaluate(float, android.graphics.PointF, android.graphics.PointF)} calls.
+ * The value returned from
+ * {@link #evaluate(float, android.graphics.PointF, android.graphics.PointF)} should
+ * not be cached because it will change over time as the object is reused on each
+ * call.
+ *
+ * @param reuse A PointF to be modified and returned by evaluate.
+ */
+ public PointFEvaluator(PointF reuse) {
+ mPoint = reuse;
+ }
+
+ /**
+ * This function returns the result of linearly interpolating the start and
+ * end PointF values, with <code>fraction</code> representing the proportion
+ * between the start and end values. The calculation is a simple parametric
+ * calculation on each of the separate components in the PointF objects
+ * (x, y).
+ *
+ * <p>If {@link #PointFEvaluator(android.graphics.PointF)} was used to construct
+ * this PointFEvaluator, the object returned will be the <code>reuse</code>
+ * passed into the constructor.</p>
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start PointF
+ * @param endValue The end PointF
+ * @return A linear interpolation between the start and end values, given the
+ * <code>fraction</code> parameter.
+ */
+ @Override
+ public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
+ float x = startValue.x + (fraction * (endValue.x - startValue.x));
+ float y = startValue.y + (fraction * (endValue.y - startValue.y));
+
+ if (mPoint != null) {
+ mPoint.set(x, y);
+ return mPoint;
+ } else {
+ return new PointF(x, y);
+ }
+ }
+}
diff --git a/android/animation/PropertyValuesHolder.java b/android/animation/PropertyValuesHolder.java
new file mode 100644
index 00000000..76806a29
--- /dev/null
+++ b/android/animation/PropertyValuesHolder.java
@@ -0,0 +1,1729 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.util.FloatProperty;
+import android.util.IntProperty;
+import android.util.Log;
+import android.util.PathParser;
+import android.util.Property;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * 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.
+ */
+ Keyframes mKeyframes = 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<Class, HashMap<String, Method>> sSetterPropertyMap =
+ new HashMap<Class, HashMap<String, Method>>();
+ private static final HashMap<Class, HashMap<String, Method>> sGetterPropertyMap =
+ new HashMap<Class, HashMap<String, Method>>();
+
+ // 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;
+
+ /**
+ * Converts from the source Object type to the setter Object type.
+ */
+ private TypeConverter mConverter;
+
+ /**
+ * 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 <code>int[]</code> values. At least two <code>int[]</code> values must be supplied,
+ * a start and end value. If more values are supplied, the values will be animated from the
+ * start, through all intermediate values to the end value. When used with ObjectAnimator,
+ * the elements of the array represent the parameters of the setter function.
+ *
+ * @param propertyName The name of the property being animated. Can also be the
+ * case-sensitive name of the entire setter method. Should not be null.
+ * @param values The values that the property will animate between.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ * @see IntArrayEvaluator#IntArrayEvaluator(int[])
+ * @see ObjectAnimator#ofMultiInt(Object, String, TypeConverter, TypeEvaluator, Object[])
+ */
+ public static PropertyValuesHolder ofMultiInt(String propertyName, int[][] values) {
+ if (values.length < 2) {
+ throw new IllegalArgumentException("At least 2 values must be supplied");
+ }
+ int numParameters = 0;
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("values must not be null");
+ }
+ int length = values[i].length;
+ if (i == 0) {
+ numParameters = length;
+ } else if (length != numParameters) {
+ throw new IllegalArgumentException("Values must all have the same length");
+ }
+ }
+ IntArrayEvaluator evaluator = new IntArrayEvaluator(new int[numParameters]);
+ return new MultiIntValuesHolder(propertyName, null, evaluator, (Object[]) values);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property name to use
+ * as a multi-int setter. The values are animated along the path, with the first
+ * parameter of the setter set to the x coordinate and the second set to the y coordinate.
+ *
+ * @param propertyName The name of the property being animated. Can also be the
+ * case-sensitive name of the entire setter method. Should not be null.
+ * The setter must take exactly two <code>int</code> parameters.
+ * @param path The Path along which the values should be animated.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...)
+ */
+ public static PropertyValuesHolder ofMultiInt(String propertyName, Path path) {
+ Keyframes keyframes = KeyframeSet.ofPath(path);
+ PointFToIntArray converter = new PointFToIntArray();
+ return new MultiIntValuesHolder(propertyName, converter, null, keyframes);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property and
+ * set of Object values for use with ObjectAnimator multi-value setters. The Object
+ * values are converted to <code>int[]</code> using the converter.
+ *
+ * @param propertyName The property being animated or complete name of the setter.
+ * Should not be null.
+ * @param converter Used to convert the animated value to setter parameters.
+ * @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.
+ * @see ObjectAnimator#ofMultiInt(Object, String, TypeConverter, TypeEvaluator, Object[])
+ * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...)
+ */
+ @SafeVarargs
+ public static <V> PropertyValuesHolder ofMultiInt(String propertyName,
+ TypeConverter<V, int[]> converter, TypeEvaluator<V> evaluator, V... values) {
+ return new MultiIntValuesHolder(propertyName, converter, evaluator, values);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder object with the specified property name or
+ * setter name for use in a multi-int setter function using ObjectAnimator. The values can be
+ * of any type, but the type should be consistent so that the supplied
+ * {@link android.animation.TypeEvaluator} can be used to to evaluate the animated value. The
+ * <code>converter</code> converts the values to parameters in the setter function.
+ *
+ * <p>At least two values must be supplied, a start and an end value.</p>
+ *
+ * @param propertyName The name of the property to associate with the set of values. This
+ * may also be the complete name of a setter function.
+ * @param converter Converts <code>values</code> into int parameters for the setter.
+ * Can be null if the Keyframes have int[] values.
+ * @param evaluator Used to interpolate between values.
+ * @param values The values at specific fractional times to evaluate between
+ * @return A PropertyValuesHolder for a multi-int parameter setter.
+ */
+ public static <T> PropertyValuesHolder ofMultiInt(String propertyName,
+ TypeConverter<T, int[]> converter, TypeEvaluator<T> evaluator, Keyframe... values) {
+ KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
+ return new MultiIntValuesHolder(propertyName, converter, evaluator, keyframeSet);
+ }
+
+ /**
+ * 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 <code>float[]</code> values. At least two <code>float[]</code> values must be supplied,
+ * a start and end value. If more values are supplied, the values will be animated from the
+ * start, through all intermediate values to the end value. When used with ObjectAnimator,
+ * the elements of the array represent the parameters of the setter function.
+ *
+ * @param propertyName The name of the property being animated. Can also be the
+ * case-sensitive name of the entire setter method. Should not be null.
+ * @param values The values that the property will animate between.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ * @see FloatArrayEvaluator#FloatArrayEvaluator(float[])
+ * @see ObjectAnimator#ofMultiFloat(Object, String, TypeConverter, TypeEvaluator, Object[])
+ */
+ public static PropertyValuesHolder ofMultiFloat(String propertyName, float[][] values) {
+ if (values.length < 2) {
+ throw new IllegalArgumentException("At least 2 values must be supplied");
+ }
+ int numParameters = 0;
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("values must not be null");
+ }
+ int length = values[i].length;
+ if (i == 0) {
+ numParameters = length;
+ } else if (length != numParameters) {
+ throw new IllegalArgumentException("Values must all have the same length");
+ }
+ }
+ FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[numParameters]);
+ return new MultiFloatValuesHolder(propertyName, null, evaluator, (Object[]) values);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property name to use
+ * as a multi-float setter. The values are animated along the path, with the first
+ * parameter of the setter set to the x coordinate and the second set to the y coordinate.
+ *
+ * @param propertyName The name of the property being animated. Can also be the
+ * case-sensitive name of the entire setter method. Should not be null.
+ * The setter must take exactly two <code>float</code> parameters.
+ * @param path The Path along which the values should be animated.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...)
+ */
+ public static PropertyValuesHolder ofMultiFloat(String propertyName, Path path) {
+ Keyframes keyframes = KeyframeSet.ofPath(path);
+ PointFToFloatArray converter = new PointFToFloatArray();
+ return new MultiFloatValuesHolder(propertyName, converter, null, keyframes);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property and
+ * set of Object values for use with ObjectAnimator multi-value setters. The Object
+ * values are converted to <code>float[]</code> using the converter.
+ *
+ * @param propertyName The property being animated or complete name of the setter.
+ * Should not be null.
+ * @param converter Used to convert the animated value to setter parameters.
+ * @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.
+ * @see ObjectAnimator#ofMultiFloat(Object, String, TypeConverter, TypeEvaluator, Object[])
+ */
+ @SafeVarargs
+ public static <V> PropertyValuesHolder ofMultiFloat(String propertyName,
+ TypeConverter<V, float[]> converter, TypeEvaluator<V> evaluator, V... values) {
+ return new MultiFloatValuesHolder(propertyName, converter, evaluator, values);
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder object with the specified property name or
+ * setter name for use in a multi-float setter function using ObjectAnimator. The values can be
+ * of any type, but the type should be consistent so that the supplied
+ * {@link android.animation.TypeEvaluator} can be used to to evaluate the animated value. The
+ * <code>converter</code> converts the values to parameters in the setter function.
+ *
+ * <p>At least two values must be supplied, a start and an end value.</p>
+ *
+ * @param propertyName The name of the property to associate with the set of values. This
+ * may also be the complete name of a setter function.
+ * @param converter Converts <code>values</code> into float parameters for the setter.
+ * Can be null if the Keyframes have float[] values.
+ * @param evaluator Used to interpolate between values.
+ * @param values The values at specific fractional times to evaluate between
+ * @return A PropertyValuesHolder for a multi-float parameter setter.
+ */
+ public static <T> PropertyValuesHolder ofMultiFloat(String propertyName,
+ TypeConverter<T, float[]> converter, TypeEvaluator<T> evaluator, Keyframe... values) {
+ KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
+ return new MultiFloatValuesHolder(propertyName, converter, evaluator, keyframeSet);
+ }
+
+ /**
+ * 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.
+ *
+ * <p><strong>Note:</strong> The Object values are stored as references to the original
+ * objects, which means that changes to those objects after this method is called will
+ * affect the values on the PropertyValuesHolder. If the objects will be mutated externally
+ * after this method is called, callers should pass a copy of those objects instead.
+ *
+ * @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 name and
+ * a Path along which the values should be animated. This variant supports a
+ * <code>TypeConverter</code> to convert from <code>PointF</code> to the target
+ * type.
+ *
+ * <p>The PointF passed to <code>converter</code> or <code>property</code>, if
+ * <code>converter</code> is <code>null</code>, is reused on each animation frame and should
+ * not be stored by the setter or TypeConverter.</p>
+ *
+ * @param propertyName The name of the property being animated.
+ * @param converter Converts a PointF to the type associated with the setter. May be
+ * null if conversion is unnecessary.
+ * @param path The Path along which the values should be animated.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ */
+ public static PropertyValuesHolder ofObject(String propertyName,
+ TypeConverter<PointF, ?> converter, Path path) {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
+ pvh.mKeyframes = KeyframeSet.ofPath(path);
+ pvh.mValueType = PointF.class;
+ pvh.setConverter(converter);
+ 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.
+ *
+ * <p><strong>Note:</strong> The Object values are stored as references to the original
+ * objects, which means that changes to those objects after this method is called will
+ * affect the values on the PropertyValuesHolder. If the objects will be mutated externally
+ * after this method is called, callers should pass a copy of those objects instead.
+ *
+ * @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.
+ */
+ @SafeVarargs
+ public static <V> PropertyValuesHolder ofObject(Property property,
+ TypeEvaluator<V> evaluator, V... values) {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(property);
+ 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. This variant also
+ * takes a <code>TypeConverter</code> to convert from animated values to the type
+ * of the property. If only one value is supplied, the <code>TypeConverter</code>
+ * must be a {@link android.animation.BidirectionalTypeConverter} to retrieve the current
+ * value.
+ *
+ * <p><strong>Note:</strong> The Object values are stored as references to the original
+ * objects, which means that changes to those objects after this method is called will
+ * affect the values on the PropertyValuesHolder. If the objects will be mutated externally
+ * after this method is called, callers should pass a copy of those objects instead.
+ *
+ * @param property The property being animated. Should not be null.
+ * @param converter Converts the animated object to the Property type.
+ * @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.
+ * @see #setConverter(TypeConverter)
+ * @see TypeConverter
+ */
+ @SafeVarargs
+ public static <T, V> PropertyValuesHolder ofObject(Property<?, V> property,
+ TypeConverter<T, V> converter, TypeEvaluator<T> evaluator, T... values) {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(property);
+ pvh.setConverter(converter);
+ pvh.setObjectValues(values);
+ pvh.setEvaluator(evaluator);
+ return pvh;
+ }
+
+ /**
+ * Constructs and returns a PropertyValuesHolder with a given property and
+ * a Path along which the values should be animated. This variant supports a
+ * <code>TypeConverter</code> to convert from <code>PointF</code> to the target
+ * type.
+ *
+ * <p>The PointF passed to <code>converter</code> or <code>property</code>, if
+ * <code>converter</code> is <code>null</code>, is reused on each animation frame and should
+ * not be stored by the setter or TypeConverter.</p>
+ *
+ * @param property The property being animated. Should not be null.
+ * @param converter Converts a PointF to the type associated with the setter. May be
+ * null if conversion is unnecessary.
+ * @param path The Path along which the values should be animated.
+ * @return PropertyValuesHolder The constructed PropertyValuesHolder object.
+ */
+ public static <V> PropertyValuesHolder ofObject(Property<?, V> property,
+ TypeConverter<PointF, V> converter, Path path) {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(property);
+ pvh.mKeyframes = KeyframeSet.ofPath(path);
+ pvh.mValueType = PointF.class;
+ pvh.setConverter(converter);
+ 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.
+ * <p>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 ObjectAnimator}, and with a getter function
+ * derived automatically from <code>propertyName</code>, 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);
+ return ofKeyframes(propertyName, keyframeSet);
+ }
+
+ /**
+ * 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.
+ * <p>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 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);
+ return ofKeyframes(property, keyframeSet);
+ }
+
+ static PropertyValuesHolder ofKeyframes(String propertyName, Keyframes keyframes) {
+ if (keyframes instanceof Keyframes.IntKeyframes) {
+ return new IntPropertyValuesHolder(propertyName, (Keyframes.IntKeyframes) keyframes);
+ } else if (keyframes instanceof Keyframes.FloatKeyframes) {
+ return new FloatPropertyValuesHolder(propertyName,
+ (Keyframes.FloatKeyframes) keyframes);
+ } else {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
+ pvh.mKeyframes = keyframes;
+ pvh.mValueType = keyframes.getType();
+ return pvh;
+ }
+ }
+
+ static PropertyValuesHolder ofKeyframes(Property property, Keyframes keyframes) {
+ if (keyframes instanceof Keyframes.IntKeyframes) {
+ return new IntPropertyValuesHolder(property, (Keyframes.IntKeyframes) keyframes);
+ } else if (keyframes instanceof Keyframes.FloatKeyframes) {
+ return new FloatPropertyValuesHolder(property, (Keyframes.FloatKeyframes) keyframes);
+ } else {
+ PropertyValuesHolder pvh = new PropertyValuesHolder(property);
+ pvh.mKeyframes = keyframes;
+ pvh.mValueType = keyframes.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 ObjectAnimator}, and with a getter function
+ * derived automatically from <code>propertyName</code>, 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;
+ mKeyframes = 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 ObjectAnimator}, and with a getter function
+ * derived automatically from <code>propertyName</code>, 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;
+ mKeyframes = 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];
+ }
+ mKeyframes = 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 ObjectAnimator}, and with a getter function
+ * derived automatically from <code>propertyName</code>, since otherwise PropertyValuesHolder has
+ * no way of determining what the value should be.
+ *
+ * <p><strong>Note:</strong> The Object values are stored as references to the original
+ * objects, which means that changes to those objects after this method is called will
+ * affect the values on the PropertyValuesHolder. If the objects will be mutated externally
+ * after this method is called, callers should pass a copy of those objects instead.
+ *
+ * @param values One or more values that the animation will animate between.
+ */
+ public void setObjectValues(Object... values) {
+ mValueType = values[0].getClass();
+ mKeyframes = KeyframeSet.ofObject(values);
+ if (mEvaluator != null) {
+ mKeyframes.setEvaluator(mEvaluator);
+ }
+ }
+
+ /**
+ * Sets the converter to convert from the values type to the setter's parameter type.
+ * If only one value is supplied, <var>converter</var> must be a
+ * {@link android.animation.BidirectionalTypeConverter}.
+ * @param converter The converter to use to convert values.
+ */
+ public void setConverter(TypeConverter converter) {
+ mConverter = converter;
+ }
+
+ /**
+ * 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) {
+ // Swallow the error, log it later
+ }
+ } else {
+ args = new Class[1];
+ Class typeVariants[];
+ if (valueType.equals(Float.class)) {
+ typeVariants = FLOAT_VARIANTS;
+ } else if (valueType.equals(Integer.class)) {
+ typeVariants = INTEGER_VARIANTS;
+ } else if (valueType.equals(Double.class)) {
+ typeVariants = DOUBLE_VARIANTS;
+ } else {
+ typeVariants = new Class[1];
+ typeVariants[0] = valueType;
+ }
+ for (Class typeVariant : typeVariants) {
+ args[0] = typeVariant;
+ try {
+ returnVal = targetClass.getMethod(methodName, args);
+ if (mConverter == null) {
+ // change the value type to suit
+ mValueType = typeVariant;
+ }
+ return returnVal;
+ } catch (NoSuchMethodException e) {
+ // Swallow the error and keep trying other variants
+ }
+ }
+ // If we got here, then no appropriate function was found
+ }
+
+ if (returnVal == null) {
+ Log.w("PropertyValuesHolder", "Method " +
+ getMethodName(prefix, mPropertyName) + "() with type " + valueType +
+ " not found on target class " + targetClass);
+ }
+
+ 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<Class, HashMap<String, Method>> propertyMapMap,
+ String prefix, Class valueType) {
+ Method setterOrGetter = null;
+ synchronized(propertyMapMap) {
+ // 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
+ HashMap<String, Method> propertyMap = propertyMapMap.get(targetClass);
+ boolean wasInMap = false;
+ if (propertyMap != null) {
+ wasInMap = propertyMap.containsKey(mPropertyName);
+ if (wasInMap) {
+ setterOrGetter = propertyMap.get(mPropertyName);
+ }
+ }
+ if (!wasInMap) {
+ setterOrGetter = getPropertyFunction(targetClass, prefix, valueType);
+ if (propertyMap == null) {
+ propertyMap = new HashMap<String, Method>();
+ propertyMapMap.put(targetClass, propertyMap);
+ }
+ propertyMap.put(mPropertyName, setterOrGetter);
+ }
+ }
+ 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) {
+ Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType();
+ mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", propertyType);
+ }
+
+ /**
+ * 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 = null;
+ List<Keyframe> keyframes = mKeyframes.getKeyframes();
+ int keyframeCount = keyframes == null ? 0 : keyframes.size();
+ for (int i = 0; i < keyframeCount; i++) {
+ Keyframe kf = keyframes.get(i);
+ if (!kf.hasValue() || kf.valueWasSetOnStart()) {
+ if (testValue == null) {
+ testValue = convertBack(mProperty.get(target));
+ }
+ kf.setValue(testValue);
+ kf.setValueWasSetOnStart(true);
+ }
+ }
+ return;
+ } catch (ClassCastException e) {
+ Log.w("PropertyValuesHolder","No such property (" + mProperty.getName() +
+ ") on target object " + target + ". Trying reflection instead");
+ mProperty = null;
+ }
+ }
+ // We can't just say 'else' here because the catch statement sets mProperty to null.
+ if (mProperty == null) {
+ Class targetClass = target.getClass();
+ if (mSetter == null) {
+ setupSetter(targetClass);
+ }
+ List<Keyframe> keyframes = mKeyframes.getKeyframes();
+ int keyframeCount = keyframes == null ? 0 : keyframes.size();
+ for (int i = 0; i < keyframeCount; i++) {
+ Keyframe kf = keyframes.get(i);
+ if (!kf.hasValue() || kf.valueWasSetOnStart()) {
+ if (mGetter == null) {
+ setupGetter(targetClass);
+ if (mGetter == null) {
+ // Already logged the error - just return to avoid NPE
+ return;
+ }
+ }
+ try {
+ Object value = convertBack(mGetter.invoke(target));
+ kf.setValue(value);
+ kf.setValueWasSetOnStart(true);
+ } catch (InvocationTargetException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ } catch (IllegalAccessException e) {
+ Log.e("PropertyValuesHolder", e.toString());
+ }
+ }
+ }
+ }
+ }
+
+ private Object convertBack(Object value) {
+ if (mConverter != null) {
+ if (!(mConverter instanceof BidirectionalTypeConverter)) {
+ throw new IllegalArgumentException("Converter "
+ + mConverter.getClass().getName()
+ + " must be a BidirectionalTypeConverter");
+ }
+ value = ((BidirectionalTypeConverter) mConverter).convertBack(value);
+ }
+ return value;
+ }
+
+ /**
+ * 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) {
+ Object value = convertBack(mProperty.get(target));
+ kf.setValue(value);
+ } else {
+ try {
+ if (mGetter == null) {
+ Class targetClass = target.getClass();
+ setupGetter(targetClass);
+ if (mGetter == null) {
+ // Already logged the error - just return to avoid NPE
+ return;
+ }
+ }
+ Object value = convertBack(mGetter.invoke(target));
+ kf.setValue(value);
+ } 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) {
+ List<Keyframe> keyframes = mKeyframes.getKeyframes();
+ if (!keyframes.isEmpty()) {
+ setupValue(target, keyframes.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) {
+ List<Keyframe> keyframes = mKeyframes.getKeyframes();
+ if (!keyframes.isEmpty()) {
+ setupValue(target, keyframes.get(keyframes.size() - 1));
+ }
+ }
+
+ @Override
+ public PropertyValuesHolder clone() {
+ try {
+ PropertyValuesHolder newPVH = (PropertyValuesHolder) super.clone();
+ newPVH.mPropertyName = mPropertyName;
+ newPVH.mProperty = mProperty;
+ newPVH.mKeyframes = mKeyframes.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
+ mKeyframes.setEvaluator(mEvaluator);
+ }
+ }
+
+ /**
+ * The TypeEvaluator will be 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;
+ mKeyframes.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) {
+ Object value = mKeyframes.getValue(fraction);
+ mAnimatedValue = mConverter == null ? value : mConverter.convert(value);
+ }
+
+ /**
+ * 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 <code>foo</code> will result
+ * in a call to the function <code>setFoo()</code> on the target object. If either
+ * <code>valueFrom</code> or <code>valueTo</code> is null, then a getter function will
+ * also be derived and called.
+ *
+ * <p>Note that the setter function derived from this property name
+ * must take the same parameter type as the
+ * <code>valueFrom</code> and <code>valueTo</code> properties, otherwise the call to
+ * the setter function will fail.</p>
+ *
+ * @param propertyName The name of the property being animated.
+ */
+ public void setPropertyName(String propertyName) {
+ mPropertyName = propertyName;
+ }
+
+ /**
+ * Sets the property that will be animated.
+ *
+ * <p>Note that if this PropertyValuesHolder object is used with ObjectAnimator, the property
+ * must exist on the target object specified in that ObjectAnimator.</p>
+ *
+ * @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 <code>foo</code> will result
+ * in a call to the function <code>setFoo()</code> on the target object. If either
+ * <code>valueFrom</code> or <code>valueTo</code> 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;
+ }
+
+ /**
+ * PropertyValuesHolder is Animators use to hold internal animation related data.
+ * Therefore, in order to replicate the animation behavior, we need to get data out of
+ * PropertyValuesHolder.
+ * @hide
+ */
+ public void getPropertyValues(PropertyValues values) {
+ init();
+ values.propertyName = mPropertyName;
+ values.type = mValueType;
+ values.startValue = mKeyframes.getValue(0);
+ if (values.startValue instanceof PathParser.PathData) {
+ // PathData evaluator returns the same mutable PathData object when query fraction,
+ // so we have to make a copy here.
+ values.startValue = new PathParser.PathData((PathParser.PathData) values.startValue);
+ }
+ values.endValue = mKeyframes.getValue(1);
+ if (values.endValue instanceof PathParser.PathData) {
+ // PathData evaluator returns the same mutable PathData object when query fraction,
+ // so we have to make a copy here.
+ values.endValue = new PathParser.PathData((PathParser.PathData) values.endValue);
+ }
+ // TODO: We need a better way to get data out of keyframes.
+ if (mKeyframes instanceof PathKeyframes.FloatKeyframesBase
+ || mKeyframes instanceof PathKeyframes.IntKeyframesBase
+ || (mKeyframes.getKeyframes() != null && mKeyframes.getKeyframes().size() > 2)) {
+ // When a pvh has more than 2 keyframes, that means there are intermediate values in
+ // addition to start/end values defined for animators. Another case where such
+ // intermediate values are defined is when animator has a path to animate along. In
+ // these cases, a data source is needed to capture these intermediate values.
+ values.dataSource = new PropertyValues.DataSource() {
+ @Override
+ public Object getValueAtFraction(float fraction) {
+ return mKeyframes.getValue(fraction);
+ }
+ };
+ } else {
+ values.dataSource = null;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public Class getValueType() {
+ return mValueType;
+ }
+
+ @Override
+ public String toString() {
+ return mPropertyName + ": " + mKeyframes.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<Class, HashMap<String, Long>> sJNISetterPropertyMap =
+ new HashMap<Class, HashMap<String, Long>>();
+ long mJniSetter;
+ private IntProperty mIntProperty;
+
+ Keyframes.IntKeyframes mIntKeyframes;
+ int mIntAnimatedValue;
+
+ public IntPropertyValuesHolder(String propertyName, Keyframes.IntKeyframes keyframes) {
+ super(propertyName);
+ mValueType = int.class;
+ mKeyframes = keyframes;
+ mIntKeyframes = keyframes;
+ }
+
+ public IntPropertyValuesHolder(Property property, Keyframes.IntKeyframes keyframes) {
+ super(property);
+ mValueType = int.class;
+ mKeyframes = keyframes;
+ mIntKeyframes = keyframes;
+ 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 setProperty(Property property) {
+ if (property instanceof IntProperty) {
+ mIntProperty = (IntProperty) property;
+ } else {
+ super.setProperty(property);
+ }
+ }
+
+ @Override
+ public void setIntValues(int... values) {
+ super.setIntValues(values);
+ mIntKeyframes = (Keyframes.IntKeyframes) mKeyframes;
+ }
+
+ @Override
+ void calculateValue(float fraction) {
+ mIntAnimatedValue = mIntKeyframes.getIntValue(fraction);
+ }
+
+ @Override
+ Object getAnimatedValue() {
+ return mIntAnimatedValue;
+ }
+
+ @Override
+ public IntPropertyValuesHolder clone() {
+ IntPropertyValuesHolder newPVH = (IntPropertyValuesHolder) super.clone();
+ newPVH.mIntKeyframes = (Keyframes.IntKeyframes) newPVH.mKeyframes;
+ 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<propName, int> for setter method
+ synchronized(sJNISetterPropertyMap) {
+ HashMap<String, Long> propertyMap = sJNISetterPropertyMap.get(targetClass);
+ boolean wasInMap = false;
+ if (propertyMap != null) {
+ wasInMap = propertyMap.containsKey(mPropertyName);
+ if (wasInMap) {
+ Long jniSetter = propertyMap.get(mPropertyName);
+ if (jniSetter != null) {
+ mJniSetter = jniSetter;
+ }
+ }
+ }
+ if (!wasInMap) {
+ String methodName = getMethodName("set", mPropertyName);
+ try {
+ mJniSetter = nGetIntMethod(targetClass, methodName);
+ } catch (NoSuchMethodError e) {
+ // Couldn't find it via JNI - try reflection next. Probably means the method
+ // doesn't exist, or the type is wrong. An error will be logged later if
+ // reflection fails as well.
+ }
+ if (propertyMap == null) {
+ propertyMap = new HashMap<String, Long>();
+ sJNISetterPropertyMap.put(targetClass, propertyMap);
+ }
+ propertyMap.put(mPropertyName, mJniSetter);
+ }
+ }
+ 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<Class, HashMap<String, Long>> sJNISetterPropertyMap =
+ new HashMap<Class, HashMap<String, Long>>();
+ long mJniSetter;
+ private FloatProperty mFloatProperty;
+
+ Keyframes.FloatKeyframes mFloatKeyframes;
+ float mFloatAnimatedValue;
+
+ public FloatPropertyValuesHolder(String propertyName, Keyframes.FloatKeyframes keyframes) {
+ super(propertyName);
+ mValueType = float.class;
+ mKeyframes = keyframes;
+ mFloatKeyframes = keyframes;
+ }
+
+ public FloatPropertyValuesHolder(Property property, Keyframes.FloatKeyframes keyframes) {
+ super(property);
+ mValueType = float.class;
+ mKeyframes = keyframes;
+ mFloatKeyframes = keyframes;
+ 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 setProperty(Property property) {
+ if (property instanceof FloatProperty) {
+ mFloatProperty = (FloatProperty) property;
+ } else {
+ super.setProperty(property);
+ }
+ }
+
+ @Override
+ public void setFloatValues(float... values) {
+ super.setFloatValues(values);
+ mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
+ }
+
+ @Override
+ void calculateValue(float fraction) {
+ mFloatAnimatedValue = mFloatKeyframes.getFloatValue(fraction);
+ }
+
+ @Override
+ Object getAnimatedValue() {
+ return mFloatAnimatedValue;
+ }
+
+ @Override
+ public FloatPropertyValuesHolder clone() {
+ FloatPropertyValuesHolder newPVH = (FloatPropertyValuesHolder) super.clone();
+ newPVH.mFloatKeyframes = (Keyframes.FloatKeyframes) newPVH.mKeyframes;
+ 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<propName, int> for setter method
+ synchronized (sJNISetterPropertyMap) {
+ HashMap<String, Long> propertyMap = sJNISetterPropertyMap.get(targetClass);
+ boolean wasInMap = false;
+ if (propertyMap != null) {
+ wasInMap = propertyMap.containsKey(mPropertyName);
+ if (wasInMap) {
+ Long jniSetter = propertyMap.get(mPropertyName);
+ if (jniSetter != null) {
+ mJniSetter = jniSetter;
+ }
+ }
+ }
+ if (!wasInMap) {
+ String methodName = getMethodName("set", mPropertyName);
+ try {
+ mJniSetter = nGetFloatMethod(targetClass, methodName);
+ } catch (NoSuchMethodError e) {
+ // Couldn't find it via JNI - try reflection next. Probably means the method
+ // doesn't exist, or the type is wrong. An error will be logged later if
+ // reflection fails as well.
+ }
+ if (propertyMap == null) {
+ propertyMap = new HashMap<String, Long>();
+ sJNISetterPropertyMap.put(targetClass, propertyMap);
+ }
+ propertyMap.put(mPropertyName, mJniSetter);
+ }
+ }
+ if (mJniSetter == 0) {
+ // Couldn't find method through fast JNI approach - just use reflection
+ super.setupSetter(targetClass);
+ }
+ }
+
+ }
+
+ static class MultiFloatValuesHolder extends PropertyValuesHolder {
+ private long mJniSetter;
+ private static final HashMap<Class, HashMap<String, Long>> sJNISetterPropertyMap =
+ new HashMap<Class, HashMap<String, Long>>();
+
+ public MultiFloatValuesHolder(String propertyName, TypeConverter converter,
+ TypeEvaluator evaluator, Object... values) {
+ super(propertyName);
+ setConverter(converter);
+ setObjectValues(values);
+ setEvaluator(evaluator);
+ }
+
+ public MultiFloatValuesHolder(String propertyName, TypeConverter converter,
+ TypeEvaluator evaluator, Keyframes keyframes) {
+ super(propertyName);
+ setConverter(converter);
+ mKeyframes = keyframes;
+ setEvaluator(evaluator);
+ }
+
+ /**
+ * 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) {
+ float[] values = (float[]) getAnimatedValue();
+ int numParameters = values.length;
+ if (mJniSetter != 0) {
+ switch (numParameters) {
+ case 1:
+ nCallFloatMethod(target, mJniSetter, values[0]);
+ break;
+ case 2:
+ nCallTwoFloatMethod(target, mJniSetter, values[0], values[1]);
+ break;
+ case 4:
+ nCallFourFloatMethod(target, mJniSetter, values[0], values[1],
+ values[2], values[3]);
+ break;
+ default: {
+ nCallMultipleFloatMethod(target, mJniSetter, values);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Internal function (called from ObjectAnimator) to set up the setter and getter
+ * prior to running the animation. No getter can be used for multiple parameters.
+ *
+ * @param target The object on which the setter exists.
+ */
+ @Override
+ void setupSetterAndGetter(Object target) {
+ setupSetter(target.getClass());
+ }
+
+ @Override
+ void setupSetter(Class targetClass) {
+ if (mJniSetter != 0) {
+ return;
+ }
+ synchronized(sJNISetterPropertyMap) {
+ HashMap<String, Long> propertyMap = sJNISetterPropertyMap.get(targetClass);
+ boolean wasInMap = false;
+ if (propertyMap != null) {
+ wasInMap = propertyMap.containsKey(mPropertyName);
+ if (wasInMap) {
+ Long jniSetter = propertyMap.get(mPropertyName);
+ if (jniSetter != null) {
+ mJniSetter = jniSetter;
+ }
+ }
+ }
+ if (!wasInMap) {
+ String methodName = getMethodName("set", mPropertyName);
+ calculateValue(0f);
+ float[] values = (float[]) getAnimatedValue();
+ int numParams = values.length;
+ try {
+ mJniSetter = nGetMultipleFloatMethod(targetClass, methodName, numParams);
+ } catch (NoSuchMethodError e) {
+ // try without the 'set' prefix
+ try {
+ mJniSetter = nGetMultipleFloatMethod(targetClass, mPropertyName,
+ numParams);
+ } catch (NoSuchMethodError e2) {
+ // just try reflection next
+ }
+ }
+ if (propertyMap == null) {
+ propertyMap = new HashMap<String, Long>();
+ sJNISetterPropertyMap.put(targetClass, propertyMap);
+ }
+ propertyMap.put(mPropertyName, mJniSetter);
+ }
+ }
+ }
+ }
+
+ static class MultiIntValuesHolder extends PropertyValuesHolder {
+ private long mJniSetter;
+ private static final HashMap<Class, HashMap<String, Long>> sJNISetterPropertyMap =
+ new HashMap<Class, HashMap<String, Long>>();
+
+ public MultiIntValuesHolder(String propertyName, TypeConverter converter,
+ TypeEvaluator evaluator, Object... values) {
+ super(propertyName);
+ setConverter(converter);
+ setObjectValues(values);
+ setEvaluator(evaluator);
+ }
+
+ public MultiIntValuesHolder(String propertyName, TypeConverter converter,
+ TypeEvaluator evaluator, Keyframes keyframes) {
+ super(propertyName);
+ setConverter(converter);
+ mKeyframes = keyframes;
+ setEvaluator(evaluator);
+ }
+
+ /**
+ * 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) {
+ int[] values = (int[]) getAnimatedValue();
+ int numParameters = values.length;
+ if (mJniSetter != 0) {
+ switch (numParameters) {
+ case 1:
+ nCallIntMethod(target, mJniSetter, values[0]);
+ break;
+ case 2:
+ nCallTwoIntMethod(target, mJniSetter, values[0], values[1]);
+ break;
+ case 4:
+ nCallFourIntMethod(target, mJniSetter, values[0], values[1],
+ values[2], values[3]);
+ break;
+ default: {
+ nCallMultipleIntMethod(target, mJniSetter, values);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Internal function (called from ObjectAnimator) to set up the setter and getter
+ * prior to running the animation. No getter can be used for multiple parameters.
+ *
+ * @param target The object on which the setter exists.
+ */
+ @Override
+ void setupSetterAndGetter(Object target) {
+ setupSetter(target.getClass());
+ }
+
+ @Override
+ void setupSetter(Class targetClass) {
+ if (mJniSetter != 0) {
+ return;
+ }
+ synchronized(sJNISetterPropertyMap) {
+ HashMap<String, Long> propertyMap = sJNISetterPropertyMap.get(targetClass);
+ boolean wasInMap = false;
+ if (propertyMap != null) {
+ wasInMap = propertyMap.containsKey(mPropertyName);
+ if (wasInMap) {
+ Long jniSetter = propertyMap.get(mPropertyName);
+ if (jniSetter != null) {
+ mJniSetter = jniSetter;
+ }
+ }
+ }
+ if (!wasInMap) {
+ String methodName = getMethodName("set", mPropertyName);
+ calculateValue(0f);
+ int[] values = (int[]) getAnimatedValue();
+ int numParams = values.length;
+ try {
+ mJniSetter = nGetMultipleIntMethod(targetClass, methodName, numParams);
+ } catch (NoSuchMethodError e) {
+ // try without the 'set' prefix
+ try {
+ mJniSetter = nGetMultipleIntMethod(targetClass, mPropertyName,
+ numParams);
+ } catch (NoSuchMethodError e2) {
+ // couldn't find it.
+ }
+ }
+ if (propertyMap == null) {
+ propertyMap = new HashMap<String, Long>();
+ sJNISetterPropertyMap.put(targetClass, propertyMap);
+ }
+ propertyMap.put(mPropertyName, mJniSetter);
+ }
+ }
+ }
+ }
+
+ /**
+ * Convert from PointF to float[] for multi-float setters along a Path.
+ */
+ private static class PointFToFloatArray extends TypeConverter<PointF, float[]> {
+ private float[] mCoordinates = new float[2];
+
+ public PointFToFloatArray() {
+ super(PointF.class, float[].class);
+ }
+
+ @Override
+ public float[] convert(PointF value) {
+ mCoordinates[0] = value.x;
+ mCoordinates[1] = value.y;
+ return mCoordinates;
+ }
+ };
+
+ /**
+ * Convert from PointF to int[] for multi-int setters along a Path.
+ */
+ private static class PointFToIntArray extends TypeConverter<PointF, int[]> {
+ private int[] mCoordinates = new int[2];
+
+ public PointFToIntArray() {
+ super(PointF.class, int[].class);
+ }
+
+ @Override
+ public int[] convert(PointF value) {
+ mCoordinates[0] = Math.round(value.x);
+ mCoordinates[1] = Math.round(value.y);
+ return mCoordinates;
+ }
+ };
+
+ /**
+ * @hide
+ */
+ public static class PropertyValues {
+ public String propertyName;
+ public Class type;
+ public Object startValue;
+ public Object endValue;
+ public DataSource dataSource = null;
+ public interface DataSource {
+ Object getValueAtFraction(float fraction);
+ }
+ public String toString() {
+ return ("property name: " + propertyName + ", type: " + type + ", startValue: "
+ + startValue.toString() + ", endValue: " + endValue.toString());
+ }
+ }
+
+ native static private long nGetIntMethod(Class targetClass, String methodName);
+ native static private long nGetFloatMethod(Class targetClass, String methodName);
+ native static private long nGetMultipleIntMethod(Class targetClass, String methodName,
+ int numParams);
+ native static private long nGetMultipleFloatMethod(Class targetClass, String methodName,
+ int numParams);
+ native static private void nCallIntMethod(Object target, long methodID, int arg);
+ native static private void nCallFloatMethod(Object target, long methodID, float arg);
+ native static private void nCallTwoIntMethod(Object target, long methodID, int arg1, int arg2);
+ native static private void nCallFourIntMethod(Object target, long methodID, int arg1, int arg2,
+ int arg3, int arg4);
+ native static private void nCallMultipleIntMethod(Object target, long methodID, int[] args);
+ native static private void nCallTwoFloatMethod(Object target, long methodID, float arg1,
+ float arg2);
+ native static private void nCallFourFloatMethod(Object target, long methodID, float arg1,
+ float arg2, float arg3, float arg4);
+ native static private void nCallMultipleFloatMethod(Object target, long methodID, float[] args);
+}
diff --git a/android/animation/PropertyValuesHolder_Delegate.java b/android/animation/PropertyValuesHolder_Delegate.java
new file mode 100644
index 00000000..1d7026c4
--- /dev/null
+++ b/android/animation/PropertyValuesHolder_Delegate.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Delegate implementing the native methods of android.animation.PropertyValuesHolder
+ *
+ * Through the layoutlib_create tool, the original native methods of PropertyValuesHolder have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager}
+ * around to map int to instance of the delegate.
+ *
+ * The main goal of this class' methods are to provide a native way to access setters and getters
+ * on some object. We override these methods to use reflection since the original reflection
+ * implementation of the PropertyValuesHolder won't be able to access protected methods.
+ *
+ */
+/*package*/
+@SuppressWarnings("unused")
+class PropertyValuesHolder_Delegate {
+ // This code is copied from android.animation.PropertyValuesHolder and must be kept in sync
+ // 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 final Object sMethodIndexLock = new Object();
+ private static final Map<Long, Method> ID_TO_METHOD = new HashMap<Long, Method>();
+ private static final Map<String, Long> METHOD_NAME_TO_ID = new HashMap<String, Long>();
+ private static long sNextId = 1;
+
+ private static long registerMethod(Class<?> targetClass, String methodName, Class[] types,
+ int nArgs) {
+ // Encode the number of arguments in the method name
+ String methodIndexName = String.format("%1$s.%2$s#%3$d", targetClass.getSimpleName(),
+ methodName, nArgs);
+ synchronized (sMethodIndexLock) {
+ Long methodId = METHOD_NAME_TO_ID.get(methodIndexName);
+
+ if (methodId != null) {
+ // The method was already registered
+ return methodId;
+ }
+
+ Class[] args = new Class[nArgs];
+ Method method = null;
+ for (Class typeVariant : types) {
+ for (int i = 0; i < nArgs; i++) {
+ args[i] = typeVariant;
+ }
+ try {
+ method = targetClass.getDeclaredMethod(methodName, args);
+ } catch (NoSuchMethodException ignore) {
+ }
+ }
+
+ if (method != null) {
+ methodId = sNextId++;
+ ID_TO_METHOD.put(methodId, method);
+ METHOD_NAME_TO_ID.put(methodIndexName, methodId);
+
+ return methodId;
+ }
+ }
+
+ // Method not found
+ return 0;
+ }
+
+ private static void callMethod(Object target, long methodID, Object... args) {
+ Method method = ID_TO_METHOD.get(methodID);
+ assert method != null;
+
+ try {
+ method.setAccessible(true);
+ method.invoke(target, args);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ Bridge.getLog().error(null, "Unable to update property during animation", e, null);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nGetIntMethod(Class<?> targetClass, String methodName) {
+ return nGetMultipleIntMethod(targetClass, methodName, 1);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nGetFloatMethod(Class<?> targetClass, String methodName) {
+ return nGetMultipleFloatMethod(targetClass, methodName, 1);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nGetMultipleIntMethod(Class<?> targetClass, String methodName,
+ int numParams) {
+ return registerMethod(targetClass, methodName, INTEGER_VARIANTS, numParams);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nGetMultipleFloatMethod(Class<?> targetClass, String methodName,
+ int numParams) {
+ return registerMethod(targetClass, methodName, FLOAT_VARIANTS, numParams);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCallIntMethod(Object target, long methodID, int arg) {
+ callMethod(target, methodID, arg);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCallFloatMethod(Object target, long methodID, float arg) {
+ callMethod(target, methodID, arg);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCallTwoIntMethod(Object target, long methodID, int arg1,
+ int arg2) {
+ callMethod(target, methodID, arg1, arg2);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCallFourIntMethod(Object target, long methodID, int arg1,
+ int arg2, int arg3, int arg4) {
+ callMethod(target, methodID, arg1, arg2, arg3, arg4);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCallMultipleIntMethod(Object target, long methodID,
+ int[] args) {
+ assert args != null;
+
+ // Box parameters
+ Object[] params = new Object[args.length];
+ for (int i = 0; i < args.length; i++) {
+ params[i] = args;
+ }
+ callMethod(target, methodID, params);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCallTwoFloatMethod(Object target, long methodID, float arg1,
+ float arg2) {
+ callMethod(target, methodID, arg1, arg2);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCallFourFloatMethod(Object target, long methodID, float arg1,
+ float arg2, float arg3, float arg4) {
+ callMethod(target, methodID, arg1, arg2, arg3, arg4);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCallMultipleFloatMethod(Object target, long methodID,
+ float[] args) {
+ assert args != null;
+
+ // Box parameters
+ Object[] params = new Object[args.length];
+ for (int i = 0; i < args.length; i++) {
+ params[i] = args;
+ }
+ callMethod(target, methodID, params);
+ }
+}
diff --git a/android/animation/RectEvaluator.java b/android/animation/RectEvaluator.java
new file mode 100644
index 00000000..23eb766d
--- /dev/null
+++ b/android/animation/RectEvaluator.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.animation;
+
+import android.graphics.Rect;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>Rect</code> values.
+ */
+public class RectEvaluator implements TypeEvaluator<Rect> {
+
+ /**
+ * When null, a new Rect is returned on every evaluate call. When non-null,
+ * mRect will be modified and returned on every evaluate.
+ */
+ private Rect mRect;
+
+ /**
+ * Construct a RectEvaluator that returns a new Rect on every evaluate call.
+ * To avoid creating an object for each evaluate call,
+ * {@link RectEvaluator#RectEvaluator(android.graphics.Rect)} should be used
+ * whenever possible.
+ */
+ public RectEvaluator() {
+ }
+
+ /**
+ * Constructs a RectEvaluator that modifies and returns <code>reuseRect</code>
+ * in {@link #evaluate(float, android.graphics.Rect, android.graphics.Rect)} calls.
+ * The value returned from
+ * {@link #evaluate(float, android.graphics.Rect, android.graphics.Rect)} should
+ * not be cached because it will change over time as the object is reused on each
+ * call.
+ *
+ * @param reuseRect A Rect to be modified and returned by evaluate.
+ */
+ public RectEvaluator(Rect reuseRect) {
+ mRect = reuseRect;
+ }
+
+ /**
+ * This function returns the result of linearly interpolating the start and
+ * end Rect values, with <code>fraction</code> representing the proportion
+ * between the start and end values. The calculation is a simple parametric
+ * calculation on each of the separate components in the Rect objects
+ * (left, top, right, and bottom).
+ *
+ * <p>If {@link #RectEvaluator(android.graphics.Rect)} was used to construct
+ * this RectEvaluator, the object returned will be the <code>reuseRect</code>
+ * passed into the constructor.</p>
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start Rect
+ * @param endValue The end Rect
+ * @return A linear interpolation between the start and end values, given the
+ * <code>fraction</code> parameter.
+ */
+ @Override
+ public Rect evaluate(float fraction, Rect startValue, Rect endValue) {
+ int left = startValue.left + (int) ((endValue.left - startValue.left) * fraction);
+ int top = startValue.top + (int) ((endValue.top - startValue.top) * fraction);
+ int right = startValue.right + (int) ((endValue.right - startValue.right) * fraction);
+ int bottom = startValue.bottom + (int) ((endValue.bottom - startValue.bottom) * fraction);
+ if (mRect == null) {
+ return new Rect(left, top, right, bottom);
+ } else {
+ mRect.set(left, top, right, bottom);
+ return mRect;
+ }
+ }
+}
diff --git a/android/animation/RevealAnimator.java b/android/animation/RevealAnimator.java
new file mode 100644
index 00000000..0f85f490
--- /dev/null
+++ b/android/animation/RevealAnimator.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.view.RenderNodeAnimator;
+import android.view.View;
+
+/**
+ * Reveals a View with an animated clipping circle.
+ * The clipping is implemented efficiently by talking to a private reveal API on View.
+ * This hidden class currently only accessed by the {@link android.view.View}.
+ *
+ * @hide
+ */
+public class RevealAnimator extends RenderNodeAnimator {
+
+ private View mClipView;
+
+ public RevealAnimator(View clipView, int x, int y,
+ float startRadius, float endRadius) {
+ super(x, y, startRadius, endRadius);
+ mClipView = clipView;
+ setTarget(mClipView);
+ }
+
+ @Override
+ protected void onFinished() {
+ mClipView.setRevealClip(false, 0, 0, 0);
+ super.onFinished();
+ }
+
+}
diff --git a/android/animation/StateListAnimator.java b/android/animation/StateListAnimator.java
new file mode 100644
index 00000000..b6d6910c
--- /dev/null
+++ b/android/animation/StateListAnimator.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ConstantState;
+import android.util.StateSet;
+import android.view.View;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Lets you define a number of Animators that will run on the attached View depending on the View's
+ * drawable state.
+ * <p>
+ * It can be defined in an XML file with the <code>&lt;selector></code> element.
+ * Each State Animator is defined in a nested <code>&lt;item></code> element.
+ *
+ * @attr ref android.R.styleable#DrawableStates_state_focused
+ * @attr ref android.R.styleable#DrawableStates_state_window_focused
+ * @attr ref android.R.styleable#DrawableStates_state_enabled
+ * @attr ref android.R.styleable#DrawableStates_state_checkable
+ * @attr ref android.R.styleable#DrawableStates_state_checked
+ * @attr ref android.R.styleable#DrawableStates_state_selected
+ * @attr ref android.R.styleable#DrawableStates_state_activated
+ * @attr ref android.R.styleable#DrawableStates_state_active
+ * @attr ref android.R.styleable#DrawableStates_state_single
+ * @attr ref android.R.styleable#DrawableStates_state_first
+ * @attr ref android.R.styleable#DrawableStates_state_middle
+ * @attr ref android.R.styleable#DrawableStates_state_last
+ * @attr ref android.R.styleable#DrawableStates_state_pressed
+ * @attr ref android.R.styleable#StateListAnimatorItem_animation
+ */
+public class StateListAnimator implements Cloneable {
+
+ private ArrayList<Tuple> mTuples = new ArrayList<Tuple>();
+ private Tuple mLastMatch = null;
+ private Animator mRunningAnimator = null;
+ private WeakReference<View> mViewRef;
+ private StateListAnimatorConstantState mConstantState;
+ private AnimatorListenerAdapter mAnimatorListener;
+ private @Config int mChangingConfigurations;
+
+ public StateListAnimator() {
+ initAnimatorListener();
+ }
+
+ private void initAnimatorListener() {
+ mAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ animation.setTarget(null);
+ if (mRunningAnimator == animation) {
+ mRunningAnimator = null;
+ }
+ }
+ };
+ }
+
+ /**
+ * Associates the given animator with the provided drawable state specs so that it will be run
+ * when the View's drawable state matches the specs.
+ *
+ * @param specs The drawable state specs to match against
+ * @param animator The animator to run when the specs match
+ */
+ public void addState(int[] specs, Animator animator) {
+ Tuple tuple = new Tuple(specs, animator);
+ tuple.mAnimator.addListener(mAnimatorListener);
+ mTuples.add(tuple);
+ mChangingConfigurations |= animator.getChangingConfigurations();
+ }
+
+ /**
+ * Returns the current {@link android.animation.Animator} which is started because of a state
+ * change.
+ *
+ * @return The currently running Animator or null if no Animator is running
+ * @hide
+ */
+ public Animator getRunningAnimator() {
+ return mRunningAnimator;
+ }
+
+ /**
+ * @hide
+ */
+ public View getTarget() {
+ return mViewRef == null ? null : mViewRef.get();
+ }
+
+ /**
+ * Called by View
+ * @hide
+ */
+ public void setTarget(View view) {
+ final View current = getTarget();
+ if (current == view) {
+ return;
+ }
+ if (current != null) {
+ clearTarget();
+ }
+ if (view != null) {
+ mViewRef = new WeakReference<View>(view);
+ }
+
+ }
+
+ private void clearTarget() {
+ final int size = mTuples.size();
+ for (int i = 0; i < size; i++) {
+ mTuples.get(i).mAnimator.setTarget(null);
+ }
+ mViewRef = null;
+ mLastMatch = null;
+ mRunningAnimator = null;
+ }
+
+ @Override
+ public StateListAnimator clone() {
+ try {
+ StateListAnimator clone = (StateListAnimator) super.clone();
+ clone.mTuples = new ArrayList<Tuple>(mTuples.size());
+ clone.mLastMatch = null;
+ clone.mRunningAnimator = null;
+ clone.mViewRef = null;
+ clone.mAnimatorListener = null;
+ clone.initAnimatorListener();
+ final int tupleSize = mTuples.size();
+ for (int i = 0; i < tupleSize; i++) {
+ final Tuple tuple = mTuples.get(i);
+ final Animator animatorClone = tuple.mAnimator.clone();
+ animatorClone.removeListener(mAnimatorListener);
+ clone.addState(tuple.mSpecs, animatorClone);
+ }
+ clone.setChangingConfigurations(getChangingConfigurations());
+ return clone;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError("cannot clone state list animator", e);
+ }
+ }
+
+ /**
+ * Called by View
+ * @hide
+ */
+ public void setState(int[] state) {
+ Tuple match = null;
+ final int count = mTuples.size();
+ for (int i = 0; i < count; i++) {
+ final Tuple tuple = mTuples.get(i);
+ if (StateSet.stateSetMatches(tuple.mSpecs, state)) {
+ match = tuple;
+ break;
+ }
+ }
+ if (match == mLastMatch) {
+ return;
+ }
+ if (mLastMatch != null) {
+ cancel();
+ }
+ mLastMatch = match;
+ if (match != null) {
+ start(match);
+ }
+ }
+
+ private void start(Tuple match) {
+ match.mAnimator.setTarget(getTarget());
+ mRunningAnimator = match.mAnimator;
+ mRunningAnimator.start();
+ }
+
+ private void cancel() {
+ if (mRunningAnimator != null) {
+ mRunningAnimator.cancel();
+ mRunningAnimator = null;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public ArrayList<Tuple> getTuples() {
+ return mTuples;
+ }
+
+ /**
+ * If there is an animation running for a recent state change, ends it.
+ * <p>
+ * This causes the animation to assign the end value(s) to the View.
+ */
+ public void jumpToCurrentState() {
+ if (mRunningAnimator != null) {
+ mRunningAnimator.end();
+ }
+ }
+
+ /**
+ * Return a mask of the configuration parameters for which this animator may change, requiring
+ * that it be re-created. The default implementation returns whatever was provided through
+ * {@link #setChangingConfigurations(int)} or 0 by default.
+ *
+ * @return Returns a mask of the changing configuration parameters, as defined by
+ * {@link android.content.pm.ActivityInfo}.
+ *
+ * @see android.content.pm.ActivityInfo
+ * @hide
+ */
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations;
+ }
+
+ /**
+ * Set a mask of the configuration parameters for which this animator may change, requiring
+ * that it should be recreated from resources instead of being cloned.
+ *
+ * @param configs A mask of the changing configuration parameters, as
+ * defined by {@link android.content.pm.ActivityInfo}.
+ *
+ * @see android.content.pm.ActivityInfo
+ * @hide
+ */
+ public void setChangingConfigurations(@Config int configs) {
+ mChangingConfigurations = configs;
+ }
+
+ /**
+ * Sets the changing configurations value to the union of the current changing configurations
+ * and the provided configs.
+ * This method is called while loading the animator.
+ * @hide
+ */
+ public void appendChangingConfigurations(@Config int configs) {
+ mChangingConfigurations |= configs;
+ }
+
+ /**
+ * Return a {@link android.content.res.ConstantState} instance that holds the shared state of
+ * this Animator.
+ * <p>
+ * This constant state is used to create new instances of this animator when needed. Default
+ * implementation creates a new {@link StateListAnimatorConstantState}. You can override this
+ * method to provide your custom logic or return null if you don't want this animator to be
+ * cached.
+ *
+ * @return The {@link android.content.res.ConstantState} associated to this Animator.
+ * @see android.content.res.ConstantState
+ * @see #clone()
+ * @hide
+ */
+ public ConstantState<StateListAnimator> createConstantState() {
+ return new StateListAnimatorConstantState(this);
+ }
+
+ /**
+ * @hide
+ */
+ public static class Tuple {
+
+ final int[] mSpecs;
+
+ final Animator mAnimator;
+
+ private Tuple(int[] specs, Animator animator) {
+ mSpecs = specs;
+ mAnimator = animator;
+ }
+
+ /**
+ * @hide
+ */
+ public int[] getSpecs() {
+ return mSpecs;
+ }
+
+ /**
+ * @hide
+ */
+ public Animator getAnimator() {
+ return mAnimator;
+ }
+ }
+
+ /**
+ * Creates a constant state which holds changing configurations information associated with the
+ * given Animator.
+ * <p>
+ * When new instance is called, default implementation clones the Animator.
+ */
+ private static class StateListAnimatorConstantState
+ extends ConstantState<StateListAnimator> {
+
+ final StateListAnimator mAnimator;
+
+ @Config int mChangingConf;
+
+ public StateListAnimatorConstantState(StateListAnimator animator) {
+ mAnimator = animator;
+ mAnimator.mConstantState = this;
+ mChangingConf = mAnimator.getChangingConfigurations();
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConf;
+ }
+
+ @Override
+ public StateListAnimator newInstance() {
+ final StateListAnimator clone = mAnimator.clone();
+ clone.mConstantState = this;
+ return clone;
+ }
+ }
+}
diff --git a/android/animation/TimeAnimator.java b/android/animation/TimeAnimator.java
new file mode 100644
index 00000000..113a21f4
--- /dev/null
+++ b/android/animation/TimeAnimator.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.view.animation.AnimationUtils;
+
+/**
+ * This class provides a simple callback mechanism to listeners that is synchronized with all
+ * other animators in the system. There is no duration, interpolation, or object value-setting
+ * with this Animator. Instead, it is simply started, after which it 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 elapsed time since the previous animation frame.
+ */
+public class TimeAnimator extends ValueAnimator {
+
+ private TimeListener mListener;
+ private long mPreviousTime = -1;
+
+ @Override
+ public void start() {
+ mPreviousTime = -1;
+ super.start();
+ }
+
+ @Override
+ boolean animateBasedOnTime(long currentTime) {
+ if (mListener != null) {
+ long totalTime = currentTime - mStartTime;
+ long deltaTime = (mPreviousTime < 0) ? 0 : (currentTime - mPreviousTime);
+ mPreviousTime = currentTime;
+ mListener.onTimeUpdate(this, totalTime, deltaTime);
+ }
+ return false;
+ }
+
+ @Override
+ public void setCurrentPlayTime(long playTime) {
+ long currentTime = AnimationUtils.currentAnimationTimeMillis();
+ mStartTime = Math.max(mStartTime, currentTime - playTime);
+ mStartTimeCommitted = true; // do not allow start time to be compensated for jank
+ animateBasedOnTime(currentTime);
+ }
+
+ /**
+ * 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 <code>TimeAnimator</code> 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,
+ * deltaTime will be zero. The same is true for totalTime, unless the animator was
+ * set to a specific {@link ValueAnimator#setCurrentPlayTime(long) currentPlayTime}
+ * prior to starting.
+ */
+ public static interface TimeListener {
+ /**
+ * <p>Notifies listeners of the occurrence of another frame of the animation,
+ * along with information about the elapsed time.</p>
+ *
+ * @param animation The animator sending out the notification.
+ * @param totalTime The total time elapsed since the animator started, in milliseconds.
+ * @param deltaTime The time elapsed since the previous frame, in milliseconds.
+ */
+ void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime);
+
+ }
+}
diff --git a/android/animation/TimeInterpolator.java b/android/animation/TimeInterpolator.java
new file mode 100644
index 00000000..0f5d8bf8
--- /dev/null
+++ b/android/animation/TimeInterpolator.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * A time interpolator defines the rate of change of an animation. This allows animations
+ * to have non-linear motion, such as acceleration and deceleration.
+ */
+public interface TimeInterpolator {
+
+ /**
+ * Maps a value representing the elapsed fraction of an animation to a value that represents
+ * the interpolated fraction. This interpolated value is then multiplied by the change in
+ * value of an animation to derive the animated value at the current elapsed animation time.
+ *
+ * @param input A value between 0 and 1.0 indicating our current point
+ * in the animation where 0 represents the start and 1.0 represents
+ * the end
+ * @return The interpolation value. This value can be more than 1.0 for
+ * interpolators which overshoot their targets, or less than 0 for
+ * interpolators that undershoot their targets.
+ */
+ float getInterpolation(float input);
+}
diff --git a/android/animation/TypeConverter.java b/android/animation/TypeConverter.java
new file mode 100644
index 00000000..9ead2ad0
--- /dev/null
+++ b/android/animation/TypeConverter.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * Abstract base class used convert type T to another type V. This
+ * is necessary when the value types of in animation are different
+ * from the property type.
+ * @see PropertyValuesHolder#setConverter(TypeConverter)
+ */
+public abstract class TypeConverter<T, V> {
+ private Class<T> mFromClass;
+ private Class<V> mToClass;
+
+ public TypeConverter(Class<T> fromClass, Class<V> toClass) {
+ mFromClass = fromClass;
+ mToClass = toClass;
+ }
+
+ /**
+ * Returns the target converted type. Used by the animation system to determine
+ * the proper setter function to call.
+ * @return The Class to convert the input to.
+ */
+ Class<V> getTargetType() {
+ return mToClass;
+ }
+
+ /**
+ * Returns the source conversion type.
+ */
+ Class<T> getSourceType() {
+ return mFromClass;
+ }
+
+ /**
+ * Converts a value from one type to another.
+ * @param value The Object to convert.
+ * @return A value of type V, converted from <code>value</code>.
+ */
+ public abstract V convert(T value);
+}
diff --git a/android/animation/TypeEvaluator.java b/android/animation/TypeEvaluator.java
new file mode 100644
index 00000000..429c4356
--- /dev/null
+++ b/android/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 android.animation;
+
+/**
+ * Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators
+ * allow developers to create animations on arbitrary property types, by allowing them to supply
+ * custom evaluators for types that are not automatically understood and used by the animation
+ * system.
+ *
+ * @see ValueAnimator#setEvaluator(TypeEvaluator)
+ */
+public interface TypeEvaluator<T> {
+
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * <code>fraction</code> representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: <code>result = x0 + t * (x1 - x0)</code>,
+ * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
+ * and <code>t</code> is <code>fraction</code>.
+ *
+ * @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
+ * <code>fraction</code> parameter.
+ */
+ public T evaluate(float fraction, T startValue, T endValue);
+
+}
diff --git a/android/animation/ValueAnimator.java b/android/animation/ValueAnimator.java
new file mode 100644
index 00000000..ee89ca8d
--- /dev/null
+++ b/android/animation/ValueAnimator.java
@@ -0,0 +1,1651 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.TestApi;
+import android.os.Looper;
+import android.os.Trace;
+import android.util.AndroidRuntimeException;
+import android.util.Log;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.LinearInterpolator;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+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.
+ *
+ * <p>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.</p>
+ *
+ * <p>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(TimeInterpolator)}.</p>
+ *
+ * <p>Animators can be created from either code or resource files. Here is an example
+ * of a ValueAnimator resource file:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/animator.xml ValueAnimatorResources}
+ *
+ * <p>Starting from API 23, it is also possible to use a combination of {@link PropertyValuesHolder}
+ * and {@link Keyframe} resource tags to create a multi-step animation.
+ * Note that you can specify explicit fractional values (from 0 to 1) for
+ * each keyframe to determine when, in the overall duration, the animation should arrive at that
+ * value. Alternatively, you can leave the fractions off and the keyframes will be equally
+ * distributed within the total duration:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/value_animator_pvh_kf.xml
+ * ValueAnimatorKeyframeResources}
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about animating with {@code ValueAnimator}, read the
+ * <a href="{@docRoot}guide/topics/graphics/prop-animation.html#value-animator">Property
+ * Animation</a> developer guide.</p>
+ * </div>
+ */
+@SuppressWarnings("unchecked")
+public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
+ private static final String TAG = "ValueAnimator";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Internal constants
+ */
+ private static float sDurationScale = 1.0f;
+
+ /**
+ * 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().
+ *
+ * Whenever mStartTime is set, you must also update mStartTimeCommitted.
+ */
+ long mStartTime = -1;
+
+ /**
+ * When true, the start time has been firmly committed as a chosen reference point in
+ * time by which the progress of the animation will be evaluated. When false, the
+ * start time may be updated when the first animation frame is committed so as
+ * to compensate for jank that may have occurred between when the start time was
+ * initialized and when the frame was actually drawn.
+ *
+ * This flag is generally set to false during the first frame of the animation
+ * when the animation playing state transitions from STOPPED to RUNNING or
+ * resumes after having been paused. This flag is set to true when the start time
+ * is firmly committed and should not be further compensated for jank.
+ */
+ boolean mStartTimeCommitted;
+
+ /**
+ * Set when setCurrentPlayTime() is called. If negative, animation is not currently seeked
+ * to a value.
+ */
+ float mSeekFraction = -1;
+
+ /**
+ * Set on the next frame after pause() is called, used to calculate a new startTime
+ * or delayStartTime which allows the animator to continue from the point at which
+ * it was paused. If negative, has not yet been set.
+ */
+ private long mPauseTime;
+
+ /**
+ * Set when an animator is resumed. This triggers logic in the next frame which
+ * actually resumes the animator.
+ */
+ private boolean mResumed = false;
+
+ // The time interpolator to be used if none is set on the animation
+ private static final TimeInterpolator sDefaultInterpolator =
+ new AccelerateDecelerateInterpolator();
+
+ /**
+ * Flag to indicate whether this animator is playing in reverse mode, specifically
+ * by being started or interrupted by a call to reverse(). This flag is different than
+ * mPlayingBackwards, which indicates merely whether the current iteration of the
+ * animator is playing in reverse. It is used in corner cases to determine proper end
+ * behavior.
+ */
+ private boolean mReversing;
+
+ /**
+ * Tracks the overall fraction of the animation, ranging from 0 to mRepeatCount + 1
+ */
+ private float mOverallFraction = 0f;
+
+ /**
+ * Tracks current elapsed/eased fraction, for querying in getAnimatedFraction().
+ * This is calculated by interpolating the fraction (range: [0, 1]) in the current iteration.
+ */
+ private float mCurrentFraction = 0f;
+
+ /**
+ * Tracks the time (in milliseconds) when the last frame arrived.
+ */
+ private long mLastFrameTime = -1;
+
+ /**
+ * Tracks the time (in milliseconds) when the first frame arrived. Note the frame may arrive
+ * during the start delay.
+ */
+ private long mFirstFrameTime = -1;
+
+ /**
+ * 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;
+
+ /**
+ * Tracks whether we've notified listeners of the onAnimationStart() event. This can be
+ * complex to keep track of since we notify listeners at different times depending on
+ * startDelay and whether start() was called before end().
+ */
+ private boolean mStartListenersCalled = 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;
+
+ /**
+ * Flag that tracks whether animation has been requested to end.
+ */
+ private boolean mAnimationEndRequested = 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. Note
+ // that this start delay is unscaled. When there is a duration scale set on the animator, the
+ // scaling factor will be applied to this delay.
+ private long mStartDelay = 0;
+
+ // 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;
+
+ /**
+ * Whether or not the animator should register for its own animation callback to receive
+ * animation pulse.
+ */
+ private boolean mSelfPulse = true;
+
+ /**
+ * Whether or not the animator has been requested to start without pulsing. This flag gets set
+ * in startWithoutPulsing(), and reset in start().
+ */
+ private boolean mSuppressSelfPulseRequested = false;
+
+ /**
+ * 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 TimeInterpolator mInterpolator = sDefaultInterpolator;
+
+ /**
+ * The set of listeners to be sent events through the life of an animation.
+ */
+ ArrayList<AnimatorUpdateListener> 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<String, PropertyValuesHolder> mValuesMap;
+
+ /**
+ * Public constants
+ */
+
+ /** @hide */
+ @IntDef({RESTART, REVERSE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RepeatMode {}
+
+ /**
+ * When the animation reaches the end and <code>repeatCount</code> 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 <code>repeatCount</code> 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;
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public static void setDurationScale(float durationScale) {
+ sDurationScale = durationScale;
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public static float getDurationScale() {
+ return sDurationScale;
+ }
+
+ /**
+ * Returns whether animators are currently enabled, system-wide. By default, all
+ * animators are enabled. This can change if either the user sets a Developer Option
+ * to set the animator duration scale to 0 or by Battery Savery mode being enabled
+ * (which disables all animations).
+ *
+ * <p>Developers should not typically need to call this method, but should an app wish
+ * to show a different experience when animators are disabled, this return value
+ * can be used as a decider of which experience to offer.
+ *
+ * @return boolean Whether animators are currently enabled. The default value is
+ * <code>true</code>.
+ */
+ public static boolean areAnimatorsEnabled() {
+ return !(sDurationScale == 0);
+ }
+
+ /**
+ * 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 color 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 ofArgb(int... values) {
+ ValueAnimator anim = new ValueAnimator();
+ anim.setIntValues(values);
+ anim.setEvaluator(ArgbEvaluator.getInstance());
+ 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.
+ *
+ * <p><strong>Note:</strong> The Object values are stored as references to the original
+ * objects, which means that changes to those objects after this method is called will
+ * affect the values on the animator. If the objects will be mutated externally after
+ * this method is called, callers should pass a copy of those objects instead.
+ *
+ * <p>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.
+ *
+ * <p>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.</p>
+ *
+ * @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(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.
+ *
+ * <p>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.</p>
+ *
+ * @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(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.
+ *
+ * <p><strong>Note:</strong> The Object values are stored as references to the original
+ * objects, which means that changes to those objects after this method is called will
+ * affect the values on the animator. If the objects will be mutated externally after
+ * this method is called, callers should pass a copy of those objects instead.
+ *
+ * <p>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.</p>
+ *
+ * <p>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.</p>
+ *
+ * @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(PropertyValuesHolder.ofObject("", 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 a 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<String, PropertyValuesHolder>(numValues);
+ for (int i = 0; i < numValues; ++i) {
+ PropertyValuesHolder valuesHolder = 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 <code>startDelay</code>, the
+ * function is called after that delay ends.
+ * It takes care of the final initialization steps for the
+ * animation.
+ *
+ * <p>Overrides of this method should call the superclass method to ensure
+ * that internal mechanisms for the animation are set up correctly.</p>
+ */
+ @CallSuper
+ 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 <code>ValueAnimator.ofInt(0, 10).setDuration(500).start()</code>.
+ */
+ @Override
+ public ValueAnimator setDuration(long duration) {
+ if (duration < 0) {
+ throw new IllegalArgumentException("Animators cannot have negative duration: " +
+ duration);
+ }
+ mDuration = duration;
+ return this;
+ }
+
+ private long getScaledDuration() {
+ return (long)(mDuration * sDurationScale);
+ }
+
+ /**
+ * Gets the length of the animation. The default duration is 300 milliseconds.
+ *
+ * @return The length of the animation, in milliseconds.
+ */
+ @Override
+ public long getDuration() {
+ return mDuration;
+ }
+
+ @Override
+ public long getTotalDuration() {
+ if (mRepeatCount == INFINITE) {
+ return DURATION_INFINITE;
+ } else {
+ return mStartDelay + (mDuration * (mRepeatCount + 1));
+ }
+ }
+
+ /**
+ * 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) {
+ float fraction = mDuration > 0 ? (float) playTime / mDuration : 1;
+ setCurrentFraction(fraction);
+ }
+
+ /**
+ * Sets the position of the animation to the specified fraction. This fraction should
+ * be between 0 and the total fraction of the animation, including any repetition. That is,
+ * a fraction of 0 will position the animation at the beginning, a value of 1 at the end,
+ * and a value of 2 at the end of a reversing animator that repeats once. If
+ * the animation has not yet been started, then it will not advance forward after it is
+ * set to this fraction; it will simply set the fraction to this value and perform any
+ * appropriate actions based on that fraction. If the animation is already running, then
+ * setCurrentFraction() will set the current fraction to this value and continue
+ * playing from that point. {@link Animator.AnimatorListener} events are not called
+ * due to changing the fraction; those events are only processed while the animation
+ * is running.
+ *
+ * @param fraction The fraction to which the animation is advanced or rewound. Values
+ * outside the range of 0 to the maximum fraction for the animator will be clamped to
+ * the correct range.
+ */
+ public void setCurrentFraction(float fraction) {
+ initAnimation();
+ fraction = clampFraction(fraction);
+ mStartTimeCommitted = true; // do not allow start time to be compensated for jank
+ if (isPulsingInternal()) {
+ long seekTime = (long) (getScaledDuration() * fraction);
+ long currentTime = AnimationUtils.currentAnimationTimeMillis();
+ // Only modify the start time when the animation is running. Seek fraction will ensure
+ // non-running animations skip to the correct start time.
+ mStartTime = currentTime - seekTime;
+ } else {
+ // If the animation loop hasn't started, or during start delay, the startTime will be
+ // adjusted once the delay has passed based on seek fraction.
+ mSeekFraction = fraction;
+ }
+ mOverallFraction = fraction;
+ final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
+ animateValue(currentIterationFraction);
+ }
+
+ /**
+ * Calculates current iteration based on the overall fraction. The overall fraction will be
+ * in the range of [0, mRepeatCount + 1]. Both current iteration and fraction in the current
+ * iteration can be derived from it.
+ */
+ private int getCurrentIteration(float fraction) {
+ fraction = clampFraction(fraction);
+ // If the overall fraction is a positive integer, we consider the current iteration to be
+ // complete. In other words, the fraction for the current iteration would be 1, and the
+ // current iteration would be overall fraction - 1.
+ double iteration = Math.floor(fraction);
+ if (fraction == iteration && fraction > 0) {
+ iteration--;
+ }
+ return (int) iteration;
+ }
+
+ /**
+ * Calculates the fraction of the current iteration, taking into account whether the animation
+ * should be played backwards. E.g. When the animation is played backwards in an iteration,
+ * the fraction for that iteration will go from 1f to 0f.
+ */
+ private float getCurrentIterationFraction(float fraction, boolean inReverse) {
+ fraction = clampFraction(fraction);
+ int iteration = getCurrentIteration(fraction);
+ float currentFraction = fraction - iteration;
+ return shouldPlayBackward(iteration, inReverse) ? 1f - currentFraction : currentFraction;
+ }
+
+ /**
+ * Clamps fraction into the correct range: [0, mRepeatCount + 1]. If repeat count is infinite,
+ * no upper bound will be set for the fraction.
+ *
+ * @param fraction fraction to be clamped
+ * @return fraction clamped into the range of [0, mRepeatCount + 1]
+ */
+ private float clampFraction(float fraction) {
+ if (fraction < 0) {
+ fraction = 0;
+ } else if (mRepeatCount != INFINITE) {
+ fraction = Math.min(fraction, mRepeatCount + 1);
+ }
+ return fraction;
+ }
+
+ /**
+ * Calculates the direction of animation playing (i.e. forward or backward), based on 1)
+ * whether the entire animation is being reversed, 2) repeat mode applied to the current
+ * iteration.
+ */
+ private boolean shouldPlayBackward(int iteration, boolean inReverse) {
+ if (iteration > 0 && mRepeatMode == REVERSE &&
+ (iteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {
+ // if we were seeked to some other iteration in a reversing animator,
+ // figure out the correct direction to start playing based on the iteration
+ if (inReverse) {
+ return (iteration % 2) == 0;
+ } else {
+ return (iteration % 2) != 0;
+ }
+ } else {
+ return inReverse;
+ }
+ }
+
+ /**
+ * 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, unless the animation has has its play time set via
+ * {@link #setCurrentPlayTime(long)} or {@link #setCurrentFraction(float)}, in which case
+ * it will return the time that was set.
+ *
+ * @return The current position in time of the animation.
+ */
+ public long getCurrentPlayTime() {
+ if (!mInitialized || (!mStarted && mSeekFraction < 0)) {
+ return 0;
+ }
+ if (mSeekFraction >= 0) {
+ return (long) (mDuration * mSeekFraction);
+ }
+ float durationScale = sDurationScale == 0 ? 1 : sDurationScale;
+ return (long) ((AnimationUtils.currentAnimationTimeMillis() - mStartTime) / durationScale);
+ }
+
+ /**
+ * 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. Note that the start delay should always be non-negative. Any
+ * negative start delay will be clamped to 0 on N and above.
+ *
+ * @param startDelay The amount of the delay, in milliseconds
+ */
+ @Override
+ public void setStartDelay(long startDelay) {
+ // Clamp start delay to non-negative range.
+ if (startDelay < 0) {
+ Log.w(TAG, "Start delay should always be non-negative");
+ startDelay = 0;
+ }
+ 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.
+ *
+ * The frame delay may be ignored when the animation system uses an external timing
+ * source, such as the display refresh rate (vsync), to govern animations.
+ *
+ * Note that this method should be called from the same thread that {@link #start()} is
+ * called in order to check the frame delay for that animation. A runtime exception will be
+ * thrown if the calling thread does not have a Looper.
+ *
+ * @return the requested time between frames, in milliseconds
+ */
+ public static long getFrameDelay() {
+ return AnimationHandler.getInstance().getFrameDelay();
+ }
+
+ /**
+ * 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.
+ *
+ * The frame delay may be ignored when the animation system uses an external timing
+ * source, such as the display refresh rate (vsync), to govern animations.
+ *
+ * Note that this method should be called from the same thread that {@link #start()} is
+ * called in order to have the new frame delay take effect on that animation. A runtime
+ * exception will be thrown if the calling thread does not have a Looper.
+ *
+ * @param frameDelay the requested time between frames, in milliseconds
+ */
+ public static void setFrameDelay(long frameDelay) {
+ AnimationHandler.getInstance().setFrameDelay(frameDelay);
+ }
+
+ /**
+ * The most recent value calculated by this <code>ValueAnimator</code> 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 <code>ValueAnimator</code>
+ * 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 <code>ValueAnimator</code> 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 <code>ValueAnimator</code> for <code>propertyName</code>.
+ * The main purpose for this read-only property is to retrieve the value from the
+ * <code>ValueAnimator</code> 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 <code>ValueAnimator</code>.
+ */
+ 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(@RepeatMode int value) {
+ mRepeatMode = value;
+ }
+
+ /**
+ * Defines what this animation should do when it reaches the end.
+ *
+ * @return either one of {@link #REVERSE} or {@link #RESTART}
+ */
+ @RepeatMode
+ 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<AnimatorUpdateListener>();
+ }
+ 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 <code>null</code>
+ * will result in linear interpolation.
+ */
+ @Override
+ public void setInterpolator(TimeInterpolator 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.
+ */
+ @Override
+ public TimeInterpolator 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 <code>startValue</code> and <code>endValue</code> 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.
+ *
+ * <p>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.</p>
+ *
+ * @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);
+ }
+ }
+
+ private void notifyStartListeners() {
+ if (mListeners != null && !mStartListenersCalled) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationStart(this, mReversing);
+ }
+ }
+ mStartListenersCalled = true;
+ }
+
+ /**
+ * 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.
+ *
+ * <p>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.</p>
+ *
+ * @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");
+ }
+ mReversing = playBackwards;
+ mSelfPulse = !mSuppressSelfPulseRequested;
+ // Special case: reversing from seek-to-0 should act as if not seeked at all.
+ if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
+ if (mRepeatCount == INFINITE) {
+ // Calculate the fraction of the current iteration.
+ float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
+ mSeekFraction = 1 - fraction;
+ } else {
+ mSeekFraction = 1 + mRepeatCount - mSeekFraction;
+ }
+ }
+ mStarted = true;
+ mPaused = false;
+ mRunning = false;
+ mAnimationEndRequested = false;
+ // Resets mLastFrameTime when start() is called, so that if the animation was running,
+ // calling start() would put the animation in the
+ // started-but-not-yet-reached-the-first-frame phase.
+ mLastFrameTime = -1;
+ mFirstFrameTime = -1;
+ mStartTime = -1;
+ addAnimationCallback(0);
+
+ if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
+ // If there's no start delay, init the animation and notify start listeners right away
+ // to be consistent with the previous behavior. Otherwise, postpone this until the first
+ // frame after the start delay.
+ startAnimation();
+ if (mSeekFraction == -1) {
+ // No seek, start at play time 0. Note that the reason we are not using fraction 0
+ // is because for animations with 0 duration, we want to be consistent with pre-N
+ // behavior: skip to the final value immediately.
+ setCurrentPlayTime(0);
+ } else {
+ setCurrentFraction(mSeekFraction);
+ }
+ }
+ }
+
+ void startWithoutPulsing(boolean inReverse) {
+ mSuppressSelfPulseRequested = true;
+ if (inReverse) {
+ reverse();
+ } else {
+ start();
+ }
+ mSuppressSelfPulseRequested = false;
+ }
+
+ @Override
+ public void start() {
+ start(false);
+ }
+
+ @Override
+ public void cancel() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
+
+ // If end has already been requested, through a previous end() or cancel() call, no-op
+ // until animation starts again.
+ if (mAnimationEndRequested) {
+ return;
+ }
+
+ // Only cancel if the animation is actually running or has been started and is about
+ // to run
+ // Only notify listeners if the animator has actually started
+ if ((mStarted || mRunning) && mListeners != null) {
+ if (!mRunning) {
+ // If it's not yet running, then start listeners weren't called. Call them now.
+ notifyStartListeners();
+ }
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ for (AnimatorListener listener : tmpListeners) {
+ listener.onAnimationCancel(this);
+ }
+ }
+ endAnimation();
+
+ }
+
+ @Override
+ public void end() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
+ if (!mRunning) {
+ // Special case if the animation has not yet started; get it ready for ending
+ startAnimation();
+ mStarted = true;
+ } else if (!mInitialized) {
+ initAnimation();
+ }
+ animateValue(shouldPlayBackward(mRepeatCount, mReversing) ? 0f : 1f);
+ endAnimation();
+ }
+
+ @Override
+ public void resume() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be resumed from the same " +
+ "thread that the animator was started on");
+ }
+ if (mPaused && !mResumed) {
+ mResumed = true;
+ if (mPauseTime > 0) {
+ addAnimationCallback(0);
+ }
+ }
+ super.resume();
+ }
+
+ @Override
+ public void pause() {
+ boolean previouslyPaused = mPaused;
+ super.pause();
+ if (!previouslyPaused && mPaused) {
+ mPauseTime = -1;
+ mResumed = false;
+ }
+ }
+
+ @Override
+ public boolean isRunning() {
+ return 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.
+ */
+ @Override
+ public void reverse() {
+ if (isPulsingInternal()) {
+ long currentTime = AnimationUtils.currentAnimationTimeMillis();
+ long currentPlayTime = currentTime - mStartTime;
+ long timeLeft = getScaledDuration() - currentPlayTime;
+ mStartTime = currentTime - timeLeft;
+ mStartTimeCommitted = true; // do not allow start time to be compensated for jank
+ mReversing = !mReversing;
+ } else if (mStarted) {
+ mReversing = !mReversing;
+ end();
+ } else {
+ start(true);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean canReverse() {
+ return true;
+ }
+
+ /**
+ * Called internally to end an animation by removing it from the animations list. Must be
+ * called on the UI thread.
+ */
+ private void endAnimation() {
+ if (mAnimationEndRequested) {
+ return;
+ }
+ removeAnimationCallback();
+
+ mAnimationEndRequested = true;
+ mPaused = false;
+ boolean notify = (mStarted || mRunning) && mListeners != null;
+ if (notify && !mRunning) {
+ // If it's not yet running, then start listeners weren't called. Call them now.
+ notifyStartListeners();
+ }
+ mRunning = false;
+ mStarted = false;
+ mStartListenersCalled = false;
+ mLastFrameTime = -1;
+ mFirstFrameTime = -1;
+ mStartTime = -1;
+ if (notify && mListeners != null) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationEnd(this, mReversing);
+ }
+ }
+ // mReversing needs to be reset *after* notifying the listeners for the end callbacks.
+ mReversing = false;
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
+ System.identityHashCode(this));
+ }
+ }
+
+ /**
+ * Called internally to start an animation by adding it to the active animations list. Must be
+ * called on the UI thread.
+ */
+ private void startAnimation() {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
+ System.identityHashCode(this));
+ }
+
+ mAnimationEndRequested = false;
+ initAnimation();
+ mRunning = true;
+ if (mSeekFraction >= 0) {
+ mOverallFraction = mSeekFraction;
+ } else {
+ mOverallFraction = 0f;
+ }
+ if (mListeners != null) {
+ notifyStartListeners();
+ }
+ }
+
+ /**
+ * Internal only: This tracks whether the animation has gotten on the animation loop. Note
+ * this is different than {@link #isRunning()} in that the latter tracks the time after start()
+ * is called (or after start delay if any), which may be before the animation loop starts.
+ */
+ private boolean isPulsingInternal() {
+ return mLastFrameTime >= 0;
+ }
+
+ /**
+ * Returns the name of this animator for debugging purposes.
+ */
+ String getNameForTrace() {
+ return "animator";
+ }
+
+ /**
+ * Applies an adjustment to the animation to compensate for jank between when
+ * the animation first ran and when the frame was drawn.
+ * @hide
+ */
+ public void commitAnimationFrame(long frameTime) {
+ if (!mStartTimeCommitted) {
+ mStartTimeCommitted = true;
+ long adjustment = frameTime - mLastFrameTime;
+ if (adjustment > 0) {
+ mStartTime += adjustment;
+ if (DEBUG) {
+ Log.d(TAG, "Adjusted start time by " + adjustment + " ms: " + toString());
+ }
+ }
+ }
+ }
+
+ /**
+ * 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
+ * <code>repeatCount</code> has been exceeded and the animation should be ended.
+ */
+ boolean animateBasedOnTime(long currentTime) {
+ boolean done = false;
+ if (mRunning) {
+ final long scaledDuration = getScaledDuration();
+ final float fraction = scaledDuration > 0 ?
+ (float)(currentTime - mStartTime) / scaledDuration : 1f;
+ final float lastFraction = mOverallFraction;
+ final boolean newIteration = (int) fraction > (int) lastFraction;
+ final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
+ (mRepeatCount != INFINITE);
+ if (scaledDuration == 0) {
+ // 0 duration animator, ignore the repeat count and skip to the end
+ done = true;
+ } else if (newIteration && !lastIterationFinished) {
+ // Time to repeat
+ if (mListeners != null) {
+ int numListeners = mListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ mListeners.get(i).onAnimationRepeat(this);
+ }
+ }
+ } else if (lastIterationFinished) {
+ done = true;
+ }
+ mOverallFraction = clampFraction(fraction);
+ float currentIterationFraction = getCurrentIterationFraction(
+ mOverallFraction, mReversing);
+ animateValue(currentIterationFraction);
+ }
+ return done;
+ }
+
+ /**
+ * Internal use only.
+ *
+ * This method does not modify any fields of the animation. It should be called when seeking
+ * in an AnimatorSet. When the last play time and current play time are of different repeat
+ * iterations,
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationRepeat(Animation)}
+ * will be called.
+ */
+ @Override
+ void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {
+ if (currentPlayTime < 0 || lastPlayTime < 0) {
+ throw new UnsupportedOperationException("Error: Play time should never be negative.");
+ }
+
+ initAnimation();
+ // Check whether repeat callback is needed only when repeat count is non-zero
+ if (mRepeatCount > 0) {
+ int iteration = (int) (currentPlayTime / mDuration);
+ int lastIteration = (int) (lastPlayTime / mDuration);
+
+ // Clamp iteration to [0, mRepeatCount]
+ iteration = Math.min(iteration, mRepeatCount);
+ lastIteration = Math.min(lastIteration, mRepeatCount);
+
+ if (iteration != lastIteration) {
+ if (mListeners != null) {
+ int numListeners = mListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ mListeners.get(i).onAnimationRepeat(this);
+ }
+ }
+ }
+ }
+
+ if (mRepeatCount != INFINITE && currentPlayTime >= (mRepeatCount + 1) * mDuration) {
+ skipToEndValue(inReverse);
+ } else {
+ // Find the current fraction:
+ float fraction = currentPlayTime / (float) mDuration;
+ fraction = getCurrentIterationFraction(fraction, inReverse);
+ animateValue(fraction);
+ }
+ }
+
+ /**
+ * Internal use only.
+ * Skips the animation value to end/start, depending on whether the play direction is forward
+ * or backward.
+ *
+ * @param inReverse whether the end value is based on a reverse direction. If yes, this is
+ * equivalent to skip to start value in a forward playing direction.
+ */
+ void skipToEndValue(boolean inReverse) {
+ initAnimation();
+ float endFraction = inReverse ? 0f : 1f;
+ if (mRepeatCount % 2 == 1 && mRepeatMode == REVERSE) {
+ // This would end on fraction = 0
+ endFraction = 0f;
+ }
+ animateValue(endFraction);
+ }
+
+ @Override
+ boolean isInitialized() {
+ return mInitialized;
+ }
+
+ /**
+ * Processes a frame of the animation, adjusting the start time if needed.
+ *
+ * @param frameTime The frame time.
+ * @return true if the animation has ended.
+ * @hide
+ */
+ public final boolean doAnimationFrame(long frameTime) {
+ if (mStartTime < 0) {
+ // First frame. If there is start delay, start delay count down will happen *after* this
+ // frame.
+ mStartTime = mReversing ? frameTime : frameTime + (long) (mStartDelay * sDurationScale);
+ }
+
+ // Handle pause/resume
+ if (mPaused) {
+ mPauseTime = frameTime;
+ removeAnimationCallback();
+ return false;
+ } else if (mResumed) {
+ mResumed = false;
+ if (mPauseTime > 0) {
+ // Offset by the duration that the animation was paused
+ mStartTime += (frameTime - mPauseTime);
+ }
+ }
+
+ if (!mRunning) {
+ // If not running, that means the animation is in the start delay phase of a forward
+ // running animation. In the case of reversing, we want to run start delay in the end.
+ if (mStartTime > frameTime && mSeekFraction == -1) {
+ // This is when no seek fraction is set during start delay. If developers change the
+ // seek fraction during the delay, animation will start from the seeked position
+ // right away.
+ return false;
+ } else {
+ // If mRunning is not set by now, that means non-zero start delay,
+ // no seeking, not reversing. At this point, start delay has passed.
+ mRunning = true;
+ startAnimation();
+ }
+ }
+
+ if (mLastFrameTime < 0) {
+ if (mSeekFraction >= 0) {
+ long seekTime = (long) (getScaledDuration() * mSeekFraction);
+ mStartTime = frameTime - seekTime;
+ mSeekFraction = -1;
+ }
+ mStartTimeCommitted = false; // allow start time to be compensated for jank
+ }
+ mLastFrameTime = frameTime;
+ // The frame time might be before the start time during the first frame of
+ // an animation. The "current time" must always be on or after the start
+ // time to avoid animating frames at negative time intervals. In practice, this
+ // is very rare and only happens when seeking backwards.
+ final long currentTime = Math.max(frameTime, mStartTime);
+ boolean finished = animateBasedOnTime(currentTime);
+
+ if (finished) {
+ endAnimation();
+ }
+ return finished;
+ }
+
+ @Override
+ boolean pulseAnimationFrame(long frameTime) {
+ if (mSelfPulse) {
+ // Pulse animation frame will *always* be after calling start(). If mSelfPulse isn't
+ // set to false at this point, that means child animators did not call super's start().
+ // This can happen when the Animator is just a non-animating wrapper around a real
+ // functional animation. In this case, we can't really pulse a frame into the animation,
+ // because the animation cannot necessarily be properly initialized (i.e. no start/end
+ // values set).
+ return false;
+ }
+ return doAnimationFrame(frameTime);
+ }
+
+ private void addOneShotCommitCallback() {
+ if (!mSelfPulse) {
+ return;
+ }
+ getAnimationHandler().addOneShotCommitCallback(this);
+ }
+
+ private void removeAnimationCallback() {
+ if (!mSelfPulse) {
+ return;
+ }
+ getAnimationHandler().removeCallback(this);
+ }
+
+ private void addAnimationCallback(long delay) {
+ if (!mSelfPulse) {
+ return;
+ }
+ getAnimationHandler().addAnimationFrameCallback(this, delay);
+ }
+
+ /**
+ * 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 <code>end()</code>
+ * function is called, to set the final value on the property.
+ *
+ * <p>Overrides of this method must call the superclass to perform the calculation
+ * of the animated value.</p>
+ *
+ * @param fraction The elapsed fraction of the animation.
+ */
+ @CallSuper
+ 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) {
+ anim.mUpdateListeners = new ArrayList<AnimatorUpdateListener>(mUpdateListeners);
+ }
+ anim.mSeekFraction = -1;
+ anim.mReversing = false;
+ anim.mInitialized = false;
+ anim.mStarted = false;
+ anim.mRunning = false;
+ anim.mPaused = false;
+ anim.mResumed = false;
+ anim.mStartListenersCalled = false;
+ anim.mStartTime = -1;
+ anim.mStartTimeCommitted = false;
+ anim.mAnimationEndRequested = false;
+ anim.mPauseTime = -1;
+ anim.mLastFrameTime = -1;
+ anim.mFirstFrameTime = -1;
+ anim.mOverallFraction = 0;
+ anim.mCurrentFraction = 0;
+ anim.mSelfPulse = true;
+ anim.mSuppressSelfPulseRequested = false;
+
+ PropertyValuesHolder[] oldValues = mValues;
+ if (oldValues != null) {
+ int numValues = oldValues.length;
+ anim.mValues = new PropertyValuesHolder[numValues];
+ anim.mValuesMap = new HashMap<String, PropertyValuesHolder>(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 <code>ValueAnimator</code> instance to receive callbacks on every animation
+ * frame, after the current frame's values have been calculated for that
+ * <code>ValueAnimator</code>.
+ */
+ public static interface AnimatorUpdateListener {
+ /**
+ * <p>Notifies the occurrence of another frame of the animation.</p>
+ *
+ * @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.
+ * May be called on arbitrary threads!
+ *
+ * @hide
+ */
+ public static int getCurrentAnimationsCount() {
+ return AnimationHandler.getAnimationCount();
+ }
+
+ @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;
+ }
+
+ /**
+ * <p>Whether or not the ValueAnimator is allowed to run asynchronously off of
+ * the UI thread. This is a hint that informs the ValueAnimator that it is
+ * OK to run the animation off-thread, however ValueAnimator may decide
+ * that it must run the animation on the UI thread anyway. For example if there
+ * is an {@link AnimatorUpdateListener} the animation will run on the UI thread,
+ * regardless of the value of this hint.</p>
+ *
+ * <p>Regardless of whether or not the animation runs asynchronously, all
+ * listener callbacks will be called on the UI thread.</p>
+ *
+ * <p>To be able to use this hint the following must be true:</p>
+ * <ol>
+ * <li>{@link #getAnimatedFraction()} is not needed (it will return undefined values).</li>
+ * <li>The animator is immutable while {@link #isStarted()} is true. Requests
+ * to change values, duration, delay, etc... may be ignored.</li>
+ * <li>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.</li>
+ * <li>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.</li>
+ * </ol>
+ * @hide
+ */
+ @Override
+ public void setAllowRunningAsynchronously(boolean mayRunAsync) {
+ // It is up to subclasses to support this, if they can.
+ }
+
+ /**
+ * @return The {@link AnimationHandler} that will be used to schedule updates for this animator.
+ * @hide
+ */
+ public AnimationHandler getAnimationHandler() {
+ return AnimationHandler.getInstance();
+ }
+}