diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/animation | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-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')
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><selector></code> element. + * Each State Animator is defined in a nested <code><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(); + } +} |