diff --git a/src/api-impl/android/animation/AnimationHandler.java b/src/api-impl/android/animation/AnimationHandler.java new file mode 100644 index 00000000..f1560340 --- /dev/null +++ b/src/api-impl/android/animation/AnimationHandler.java @@ -0,0 +1,518 @@ +/* + * 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 java.lang.ref.WeakReference; +import java.util.ArrayList; + +import android.annotation.Nullable; +import android.os.SystemClock; +import android.util.ArrayMap; +import android.util.Log; +import android.view.Choreographer; + +/** + * 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 { + + private static final String TAG = "AnimationHandler"; + private static final boolean LOCAL_LOGV = false; + + /** + * Internal per-thread collections used to avoid set collisions as animations start and end + * while being processed. + */ + private final ArrayMap mDelayedCallbackStartTime = + new ArrayMap<>(); + private final ArrayList mAnimationCallbacks = + new ArrayList<>(); + private final ArrayList mCommitCallbacks = + new ArrayList<>(); + private AnimationFrameCallbackProvider mProvider; + + // Static flag which allows the pausing behavior to be globally disabled/enabled. + private static boolean sAnimatorPausingEnabled = isPauseBgAnimationsEnabledInSystemProperties(); + + // Static flag which prevents the system property from overriding sAnimatorPausingEnabled field. + private static boolean sOverrideAnimatorPausingSystemProperty = false; + + /** + * This paused list is used to store animators forcibly paused when the activity + * went into the background (to avoid unnecessary background processing work). + * These animators should be resume()'d when the activity returns to the foreground. + */ + private final ArrayList mPausedAnimators = new ArrayList<>(); + + /** + * This structure is used to store the currently active objects (ViewRootImpls or + * WallpaperService.Engines) in the process. Each of these objects sends a request to + * AnimationHandler when it goes into the background (request to pause) or foreground + * (request to resume). Because all animators are managed by AnimationHandler on the same + * thread, it should only ever pause animators when *all* requestors are in the background. + * This list tracks the background/foreground state of all requestors and only ever + * pauses animators when all items are in the background (false). To simplify, we only ever + * store visible (foreground) requestors; if the set size reaches zero, there are no + * objects in the foreground and it is time to pause animators. + */ + private final ArrayList> mAnimatorRequestors = new ArrayList<>(); + + 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 sAnimatorHandler = new ThreadLocal<>(); + private static AnimationHandler sTestHandler = null; + private boolean mListDirty = false; + + public static AnimationHandler getInstance() { + if (sTestHandler != null) { + return sTestHandler; + } + if (sAnimatorHandler.get() == null) { + sAnimatorHandler.set(new AnimationHandler()); + } + return sAnimatorHandler.get(); + } + + /** + * Sets an instance that will be returned by {@link #getInstance()} on every thread. + * @return the previously active test handler, if any. + * @hide + */ + public static @Nullable AnimationHandler setTestHandler(@Nullable AnimationHandler handler) { + AnimationHandler oldHandler = sTestHandler; + sTestHandler = handler; + return oldHandler; + } + + /** + * System property that controls the behavior of pausing infinite animators when an app + * is moved to the background. + * + * @return the value of 'framework.pause_bg_animations.enabled' system property + */ + private static boolean isPauseBgAnimationsEnabledInSystemProperties() { + if (sOverrideAnimatorPausingSystemProperty) return sAnimatorPausingEnabled; + return true; /*SystemProperties + .getBoolean("framework.pause_bg_animations.enabled", true);*/ + } + + /** + * Disable the default behavior of pausing infinite animators when + * apps go into the background. + * + * @param enable Enable (default behavior) or disable background pausing behavior. + */ + public static void setAnimatorPausingEnabled(boolean enable) { + sAnimatorPausingEnabled = enable; + } + + /** + * Prevents the setAnimatorPausingEnabled behavior from being overridden + * by the 'framework.pause_bg_animations.enabled' system property value. + * + * This is for testing purposes only. + * + * @param enable Enable or disable (default behavior) overriding the system + * property. + */ + public static void setOverrideAnimatorPausingSystemProperty(boolean enable) { + sOverrideAnimatorPausingSystemProperty = enable; + } + + /** + * This is called when a window goes away. We should remove + * it from the requestors list to ensure that we are counting requests correctly and not + * tracking obsolete+enabled requestors. + */ + public static void removeRequestor(Object requestor) { + getInstance().requestAnimatorsEnabledImpl(false, requestor); + if (LOCAL_LOGV) { + Log.v(TAG, "removeRequestor for " + requestor); + } + } + + /** + * This method is called from ViewRootImpl or WallpaperService when either a window is no + * longer visible (enable == false) or when a window becomes visible (enable == true). + * If animators are not properly disabled when activities are backgrounded, it can lead to + * unnecessary processing, particularly for infinite animators, as the system will continue + * to pulse timing events even though the results are not visible. As a workaround, we + * pause all un-paused infinite animators, and resume them when any window in the process + * becomes visible. + */ + public static void requestAnimatorsEnabled(boolean enable, Object requestor) { + getInstance().requestAnimatorsEnabledImpl(enable, requestor); + } + + private void requestAnimatorsEnabledImpl(boolean enable, Object requestor) { + boolean wasEmpty = mAnimatorRequestors.isEmpty(); + setAnimatorPausingEnabled(isPauseBgAnimationsEnabledInSystemProperties()); + synchronized (mAnimatorRequestors) { + // Only store WeakRef objects to avoid leaks + if (enable) { + // First, check whether such a reference is already on the list + WeakReference weakRef = null; + for (int i = mAnimatorRequestors.size() - 1; i >= 0; --i) { + WeakReference ref = mAnimatorRequestors.get(i); + Object referent = ref.get(); + if (referent == requestor) { + weakRef = ref; + } else if (referent == null) { + // Remove any reference that has been cleared + mAnimatorRequestors.remove(i); + } + } + if (weakRef == null) { + weakRef = new WeakReference<>(requestor); + mAnimatorRequestors.add(weakRef); + } + } else { + for (int i = mAnimatorRequestors.size() - 1; i >= 0; --i) { + WeakReference ref = mAnimatorRequestors.get(i); + Object referent = ref.get(); + if (referent == requestor || referent == null) { + // remove requested item or item that has been cleared + mAnimatorRequestors.remove(i); + } + } + // If a reference to the requestor wasn't in the list, nothing to remove + } + } + if (!sAnimatorPausingEnabled) { + // Resume any animators that have been paused in the meantime, otherwise noop + // Leave logic above so that if pausing gets re-enabled, the state of the requestors + // list is valid + resumeAnimators(); + return; + } + boolean isEmpty = mAnimatorRequestors.isEmpty(); + if (wasEmpty != isEmpty) { + // only paused/resume animators if there was a visibility change + if (!isEmpty) { + // If any requestors are enabled, resume currently paused animators + resumeAnimators(); + } else { + // Wait before pausing to avoid thrashing animator state for temporary backgrounding + Choreographer.getInstance().postFrameCallbackDelayed(mPauser, + /*Animator.getBackgroundPauseDelay()*/100); + } + } + if (LOCAL_LOGV) { + Log.v(TAG, (enable ? "enable" : "disable") + " animators for " + requestor + + " with pauseDelay of " + /*Animator.getBackgroundPauseDelay()*/100); + for (int i = 0; i < mAnimatorRequestors.size(); ++i) { + Log.v(TAG, "animatorRequestors " + i + " = " + + mAnimatorRequestors.get(i) + " with referent " + + mAnimatorRequestors.get(i).get()); + } + } + } + + private void resumeAnimators() { + Choreographer.getInstance().removeFrameCallback(mPauser); + for (int i = mPausedAnimators.size() - 1; i >= 0; --i) { + mPausedAnimators.get(i).resume(); + } + mPausedAnimators.clear(); + } + + private Choreographer.FrameCallback mPauser = new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { + if (mAnimatorRequestors.size() > 0) { + // something enabled animators since this callback was scheduled - bail + return; + } + for (int i = 0; i < mAnimationCallbacks.size(); ++i) { + AnimationFrameCallback callback = mAnimationCallbacks.get(i); + if (callback instanceof Animator) { + Animator animator = ((Animator) callback); + if (animator.getTotalDuration() == Animator.DURATION_INFINITE + && !animator.isPaused()) { + mPausedAnimators.add(animator); + animator.pause(); + } + } + } + }}; + + /** + * 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 = sTestHandler; + if (handler == null) { + 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*/4, 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. + * @hide + */ + public 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); + } +} \ No newline at end of file diff --git a/src/api-impl/android/animation/Animator.java b/src/api-impl/android/animation/Animator.java index 4add8628..4a28bd53 100644 --- a/src/api-impl/android/animation/Animator.java +++ b/src/api-impl/android/animation/Animator.java @@ -1,55 +1,824 @@ +/* + * 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.os.Handler; -import android.os.Looper; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; -public class Animator { +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.ConstantState; +import android.util.LongArray; - public interface AnimatorListener { - public abstract void onAnimationEnd (Animator animation); - } +/** + * This is the superclass for classes which provide basic support for animations which can be + * started, ended, and have AnimatorListeners added to them. + */ +public abstract class Animator implements Cloneable { - private long duration = 50; - private AnimatorListener listener; + /** + * 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 mListeners = null; - public void setTarget(Object target) {} + /** + * The set of listeners to be sent pause/resume events through the life + * of an animation. + */ + ArrayList 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. + */ + 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; + + /** + * A cache of the values in a list. Used so that when calling the list, we have a copy + * of it in case the list is modified while iterating. The array can be reused to avoid + * allocation on every notification. + */ + private AtomicReference mCachedList = new AtomicReference<>(); + + /** + * 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(). + */ + boolean mStartListenersCalled = false; + + /** + * 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. + * + *

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.

+ * + */ public void start() { - if (listener == null) - return; - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - public void run() { - if (listener != null) - listener.onAnimationEnd(Animator.this); - } - }, duration); } + /** + * Cancels the animation. Unlike {@link #end()}, cancel() 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. + * + *

This method must be called on the thread that is running the animation.

+ */ + 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. + * + *

This method must be called on the thread that is running the animation.

+ */ + 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() { + // We only want to pause started Animators or animators that setCurrentPlayTime() + // have been called on. mStartListenerCalled will be true if seek has happened. + if ((isStarted() || mStartListenersCalled) && !mPaused) { + mPaused = true; + notifyPauseListeners(AnimatorCaller.ON_PAUSE); + } + } + + /** + * 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; + notifyPauseListeners(AnimatorCaller.ON_RESUME); + } + } + + /** + * 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) { - this.listener = listener; + if (mListeners == null) { + mListeners = new ArrayList(); + } + mListeners.add(listener); } - public void cancel() {} - - public long getStartDelay() { return 0; } - - public long getDuration() { return duration; } - - public Animator setDuration(long duration) { - this.duration = duration; - return this; + /** + * 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; + } } - public void setInterpolator(TimeInterpolator i) {} + /** + * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently + * listening for events on this Animator object. + * + * @return ArrayList The set of listeners. + */ + public ArrayList getListeners() { + return mListeners; + } - public void setStartDelay(long startDelay) {} + /** + * 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(); + } + mPauseListeners.add(listener); + } - public boolean isStarted() { return false; } + /** + * 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; + } + } - public void end() {} + /** + * 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; + } + } - public TimeInterpolator getInterpolator() { return 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 int getChangingConfigurations() { + return mChangingConfigurations; + } - public boolean isRunning() { return false; } + /** + * 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(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(int configs) { + mChangingConfigurations |= configs; + } + + /** + * Return a {@link android.content.res.ConstantState} instance that holds the shared state of + * this Animator. + *

+ * 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 createConstantState() { + return new AnimatorConstantState(this); + } + + @Override + public Animator clone() { + try { + final Animator anim = (Animator) super.clone(); + if (mListeners != null) { + anim.mListeners = new ArrayList(mListeners); + } + if (mPauseListeners != null) { + anim.mPauseListeners = new ArrayList(mPauseListeners); + } + anim.mCachedList.set(null); + anim.mStartListenersCalled = false; + 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. + *

+ * Note: 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. Changes the value of the animator as if currentPlayTime has passed since + * the start of the animation. Therefore, currentPlayTime includes the start delay, and any + * repetition. lastPlayTime is similar and is used to calculate how many repeats have been + * done between the two times. + */ + void animateValuesInRange(long currentPlayTime, long lastPlayTime) {} + + /** + * Internal use only. This animates any animation that has ended since lastPlayTime. + * If an animation hasn't been finished, no change will be made. + */ + void animateSkipToEnds(long currentPlayTime, long lastPlayTime) {} + + /** + * Internal use only. Adds all start times (after delay) to and end times to times. + * The value must include offset. + */ + void getStartAndEndTimes(LongArray times, long offset) { + long startTime = offset + getStartDelay(); + if (times.indexOf(startTime) < 0) { + times.add(startTime); + } + long duration = getTotalDuration(); + if (duration != DURATION_INFINITE) { + long endTime = duration + offset; + if (times.indexOf(endTime) < 0) { + times.add(endTime); + } + } + } + + /** + * Calls notification for each AnimatorListener. + * + * @param notification The notification method to call on each listener. + * @param isReverse When this is used with start/end, this is the isReverse parameter. For + * other calls, this is ignored. + */ + void notifyListeners( + AnimatorCaller notification, + boolean isReverse + ) { + callOnList(mListeners, notification, this, isReverse); + } + + /** + * Call pause/resume on each AnimatorPauseListener. + * + * @param notification Either ON_PAUSE or ON_RESUME to call onPause or onResume on each + * listener. + */ + void notifyPauseListeners(AnimatorCaller notification) { + callOnList(mPauseListeners, notification, this, false); + } + + void notifyStartListeners(boolean isReversing) { + boolean startListenersCalled = mStartListenersCalled; + mStartListenersCalled = true; + if (mListeners != null && !startListenersCalled) { + notifyListeners(AnimatorCaller.ON_START, isReversing); + } + } + + void notifyEndListeners(boolean isReversing) { + boolean startListenersCalled = mStartListenersCalled; + mStartListenersCalled = false; + if (mListeners != null && startListenersCalled) { + notifyListeners(AnimatorCaller.ON_END, isReversing); + } + } + + /** + * Calls call for every item in list with animator and + * isReverse as parameters. + * + * @param list The list of items to make calls on. + * @param call The method to call for each item in list. + * @param animator The animator parameter of call. + * @param isReverse The isReverse parameter of call. + * @param The item type of list + * @param The Animator type of animator. + */ + void callOnList( + ArrayList list, + AnimatorCaller call, + A animator, + boolean isReverse + ) { + int size = list == null ? 0 : list.size(); + if (size > 0) { + // Try to reuse mCacheList to store the items of list. + Object[] array = mCachedList.getAndSet(null); + if (array == null || array.length < size) { + array = new Object[size]; + } + list.toArray(array); + for (int i = 0; i < size; i++) { + //noinspection unchecked + T item = (T) array[i]; + call.call(item, animator, isReverse); + array[i] = null; + } + // Store it for the next call so we can reuse this array, if needed. + mCachedList.compareAndSet(null, array); + } + } + + /** + *

An animation listener receives notifications from an animation. + * Notifications indicate animation related events, such as the end or the + * repetition of the animation.

+ */ + public static interface AnimatorListener { + + /** + *

Notifies the start of the animation 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(@NonNull Animator animation, boolean isReverse) { + onAnimationStart(animation); + } + + /** + *

Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + *

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(@NonNull Animator animation, boolean isReverse) { + onAnimationEnd(animation); + } + + /** + *

Notifies the start of the animation.

+ * + * @param animation The started animation. + */ + void onAnimationStart(@NonNull Animator animation); + + /** + *

Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @param animation The animation which reached its end. + */ + void onAnimationEnd(@NonNull Animator animation); + + /** + *

Notifies the cancellation of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @param animation The animation which was canceled. + */ + void onAnimationCancel(@NonNull Animator animation); + + /** + *

Notifies the repetition of the animation.

+ * + * @param animation The animation which was repeated. + */ + void onAnimationRepeat(@NonNull Animator animation); + } + + /** + * A pause listener receives notifications from an animation when the + * animation is {@link #pause() paused} or {@link #resume() resumed}. + * + * @see #addPauseListener(AnimatorPauseListener) + */ + public static interface AnimatorPauseListener { + /** + *

Notifies that the animation was paused.

+ * + * @param animation The animaton being paused. + * @see #pause() + */ + void onAnimationPause(@NonNull Animator animation); + + /** + *

Notifies that the animation was resumed, after being + * previously paused.

+ * + * @param animation The animation being resumed. + * @see #resume() + */ + void onAnimationResume(@NonNull Animator animation); + } + + /** + *

Whether or not the Animator is allowed to run asynchronously off of + * the UI thread. This is a hint that informs the Animator that it is + * OK to run the animation off-thread, however the Animator may decide + * that it must run the animation on the UI thread anyway. + * + *

Regardless of whether or not the animation runs asynchronously, all + * listener callbacks will be called on the UI thread.

+ * + *

To be able to use this hint the following must be true:

+ *
    + *
  1. The animator is immutable while {@link #isStarted()} is true. Requests + * to change duration, delay, etc... may be ignored.
  2. + *
  3. 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.
  4. + *
  5. 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.
  6. + *
+ * @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. + *

+ * When {@link #newInstance()} is called, default implementation clones the Animator. + */ + private static class AnimatorConstantState extends ConstantState { + + final Animator mAnimator; + 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 int getChangingConfigurations() { + return mChangingConf; + } + + @Override + public Animator newInstance() { + final Animator clone = mAnimator.clone(); + clone.mConstantState = this; + return clone; + } + } + + /** + * Internally used by {@link #callOnList(ArrayList, AnimatorCaller, Object, boolean)} to + * make a call on all children of a list. This can be for start, stop, pause, cancel, update, + * etc notifications. + * + * @param The type of listener to make the call on + * @param The type of animator that is passed as a parameter + */ + interface AnimatorCaller { + void call(T listener, A animator, boolean isReverse); + + AnimatorCaller ON_START = new AnimatorCaller() { + @Override + public void call(AnimatorListener listener, Animator animator, boolean isReverse) { + listener.onAnimationStart(animator); + } + }; + AnimatorCaller ON_END = new AnimatorCaller() { + @Override + public void call(AnimatorListener listener, Animator animator, boolean isReverse) { + listener.onAnimationEnd(animator); + } + }; + AnimatorCaller ON_CANCEL = + new AnimatorCaller() { @Override public void call(AnimatorListener listener, Animator animator, boolean isReverse) { listener.onAnimationCancel(animator); }}; + AnimatorCaller ON_REPEAT = + new AnimatorCaller() { @Override public void call(AnimatorListener listener, Animator animator, boolean isReverse) { listener.onAnimationRepeat(animator); }}; + AnimatorCaller ON_PAUSE = + new AnimatorCaller() { @Override public void call(AnimatorPauseListener listener, Animator animator, boolean isReverse) { listener.onAnimationPause(animator); }}; + AnimatorCaller ON_RESUME = + new AnimatorCaller() { @Override public void call(AnimatorPauseListener listener, Animator animator, boolean isReverse) { listener.onAnimationResume(animator); }}; + AnimatorCaller ON_UPDATE = + new AnimatorCaller() { + @Override + public void call(ValueAnimator.AnimatorUpdateListener listener, ValueAnimator animator, boolean isReverse) { + listener.onAnimationUpdate(animator); + } + }; + } } diff --git a/src/api-impl/android/animation/AnimatorSet.java b/src/api-impl/android/animation/AnimatorSet.java index b7231ffb..fd602933 100644 --- a/src/api-impl/android/animation/AnimatorSet.java +++ b/src/api-impl/android/animation/AnimatorSet.java @@ -1,37 +1,2268 @@ +/* + * 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 java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.function.Consumer; -public class AnimatorSet extends Animator { +import android.app.ActivityThread; +import android.app.Application; +import android.os.Build; +import android.os.Looper; +import android.os.SystemClock; +import android.util.AndroidRuntimeException; +import android.util.ArrayMap; +import android.util.Log; +import android.util.LongArray; +import android.view.animation.Animation; - public class Builder { +/** + * 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. + * + *

There are two different approaches to adding animations to a AnimatorSet: + * 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.

+ * + *

It is possible to set up a AnimatorSet with circular dependencies between + * its animations. For example, an animation a1 could be set up to start before animation a2, a2 + * before a3, and a3 before a1. The results of this configuration are undefined, but will typically + * result in none of the affected animations being played. Because of this (and because + * circular dependencies do not make logical sense anyway), circular dependencies + * should be avoided, and the dependency flow of animations should only be in one direction. + * + *

+ */ +public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback { - public Builder with(Animator animator) { - return this; + 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 mPlayingSet = new ArrayList(); + + /** + * Contains all nodes, mapped to their respective Animators. When new + * dependency information is added for an Animator, we want to add it + * to a single node representing that Animator, not create a new Node + * if one already exists. + */ + private ArrayMap mNodeMap = new ArrayMap(); + + /** + * Contains the start and end events of all the nodes. All these events are sorted in this list. + */ + private ArrayList 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 mNodes = new ArrayList(); + + /** + * 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; + + /** + * The start and stop times of all descendant animators. + */ + private long[] mChildStartAndStopTimes; + + // This is to work around a bug in b/34736819. This needs to be removed once app team + // fixes their side. + private AnimatorListenerAdapter mAnimationEndListener = 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*/24) { + mShouldIgnoreEndWithoutStart = true; + } else { + mShouldIgnoreEndWithoutStart = false; + } + + isPreO = app.getApplicationInfo().targetSdkVersion < /*Build.VERSION_CODES.O*/26; + } + 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]); + } } } - public Builder play(Animator animator) { - return new Builder(); + /** + * Sets up this AnimatorSet to play all of the supplied animations at the same time. + * + * @param items The animations that will be started simultaneously. + */ + public void playTogether(Collection items) { + if (items != null && items.size() > 0) { + Builder builder = null; + for (Animator anim : items) { + if (builder == null) { + builder = play(anim); + } else { + builder.with(anim); + } + } + } } - public void setInterpolator(TimeInterpolator value) {} + /** + * 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]); + } + } + } + } - public void playSequentially(Animator[] animators) {} + /** + * Sets up this AnimatorSet to play each of the supplied animations when the + * previous animation ends. + * + * @param items The animations that will be started one after another. + */ + public void playSequentially(List items) { + if (items != null && items.size() > 0) { + if (items.size() == 1) { + play(items.get(0)); + } else { + for (int i = 0; i < items.size() - 1; ++i) { + play(items.get(i)).before(items.get(i + 1)); + } + } + } + } + /** + * Returns the current list of child Animator objects controlled by this + * AnimatorSet. This is a copy of the internal list; modifications to the returned list + * will not affect the AnimatorSet, although changes to the underlying Animator objects + * will affect those objects being managed by the AnimatorSet. + * + * @return ArrayList The list of child animations of this AnimatorSet. + */ + public ArrayList getChildAnimations() { + ArrayList childList = new ArrayList(); + 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 Builder object, which is used to + * set up playing constraints. This initial play() method + * tells the Builder the animation that is the dependency for + * the succeeding commands to the Builder. For example, + * calling play(a1).with(a2) sets up the AnimatorSet to play + * a1 and a2 at the same time, + * play(a1).before(a2) sets up the AnimatorSet to play + * a1 first, followed by a2, and + * play(a1).after(a2) sets up the AnimatorSet to play + * a2 first, followed by a1. + * + *

Note that play() is the only way to tell the + * Builder the animation upon which the dependency is created, + * so successive calls to the various functions in Builder + * will all refer to the initial parameter supplied in play() + * as the dependency of the other animations. For example, calling + * play(a1).before(a2).before(a3) will play both a2 + * and a3 when a1 ends; it does not set up a dependency between + * a2 and a3.

+ * + * @param anim The animation that is the dependency used in later calls to the + * methods in the returned Builder object. A null parameter will result + * in a null Builder return value. + * @return Builder The object that constructs the AnimatorSet based on the dependencies + * outlined in the calls to play and the other methods in the + * BuilderNote that canceling a AnimatorSet also cancels all of the animations that it + * is responsible for.

+ */ + @SuppressWarnings("unchecked") + @Override + public void cancel() { + if (Looper.myLooper() == null) { + throw new AndroidRuntimeException("Animators may only be run on Looper threads"); + } + if (isStarted() || mStartListenersCalled) { + notifyListeners(AnimatorCaller.ON_CANCEL, false); + callOnPlayingSet(new Consumer() { + @Override + public void accept(Animator animator) { + animator.cancel(); + } + }); + mPlayingSet.clear(); + endAnimation(); + } + } + + /** + * Calls consumer on every Animator of mPlayingSet. + * + * @param consumer The method to call on every Animator of mPlayingSet. + */ + private void callOnPlayingSet(Consumer consumer) { + final ArrayList list = mPlayingSet; + final int size = list.size(); + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < size; i++) { + final Animator animator = list.get(i).mAnimation; + consumer.accept(animator); + } + } + + // 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} + * + *

Note that ending a AnimatorSet also ends all of the animations that it is + * responsible for.

+ */ + @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()) { + mStarted = false; // don't allow reentrancy + // 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; + for (int eventId = mLastEventId - 1; eventId >= 0; eventId--) { + AnimationEvent event = mEvents.get(eventId); + 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 { + for (int eventId = mLastEventId + 1; eventId < mEvents.size(); eventId++) { + // Avoid potential reentrant loop caused by child animators manipulating + // AnimatorSet's lifecycle (i.e. not a recommended approach). + AnimationEvent event = mEvents.get(eventId); + 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(); + } + } + } + } + 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; + callOnPlayingSet(new Consumer() { + @Override + public void accept(Animator animator) { + animator.pause(); + } + }); + } + } + + @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); + } + callOnPlayingSet(new Consumer() { + @Override + public void accept(Animator animator) { + animator.resume(); + } + }); + } + } + + /** + * {@inheritDoc} + * + *

Starting this AnimatorSet will, in turn, start the animations for which + * it is responsible. The details of when exactly those animations are started depends on + * the dependency relationships that have been set up between the animations. + * + * Note: 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"); + } + if (inReverse == mReversing && selfPulse == mSelfPulse && mStarted) { + // It is already started + return; + } + 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(); + } + + notifyStartListeners(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) { + // This makes sure the animation events are sorted an up to date. + initAnimation(); + initChildren(); + + // 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--) { + AnimationEvent event = mEvents.get(i); + if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) { + event.mNode.mAnimation.skipToEndValue(true); + } + } + } else { + for (int i = 0; i < mEvents.size(); i++) { + AnimationEvent event = mEvents.get(i); + if (event.mEvent == AnimationEvent.ANIMATION_END) { + event.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. + */ + private void animateBasedOnPlayTime( + long currentPlayTime, + long lastPlayTime, + boolean inReverse + ) { + if (currentPlayTime < 0 || lastPlayTime < -1) { + throw new UnsupportedOperationException("Error: Play time should never be negative."); + } + // TODO: take into account repeat counts and repeat callback when repeat is implemented. + + if (inReverse) { + long duration = getTotalDuration(); + if (duration == DURATION_INFINITE) { + throw new UnsupportedOperationException( + "Cannot reverse AnimatorSet with infinite duration" + ); + } + // Convert the play times to the forward direction. + currentPlayTime = Math.min(currentPlayTime, duration); + currentPlayTime = duration - currentPlayTime; + lastPlayTime = duration - lastPlayTime; + } + + long[] startEndTimes = ensureChildStartAndEndTimes(); + int index = findNextIndex(lastPlayTime, startEndTimes); + int endIndex = findNextIndex(currentPlayTime, startEndTimes); + + // Change values at the start/end times so that values are set in the right order. + // We don't want an animator that would finish before another to override the value + // set by another animator that finishes earlier. + if (currentPlayTime >= lastPlayTime) { + while (index < endIndex) { + long playTime = startEndTimes[index]; + if (lastPlayTime != playTime) { + animateSkipToEnds(playTime, lastPlayTime); + animateValuesInRange(playTime, lastPlayTime); + lastPlayTime = playTime; + } + index++; + } + } else { + while (index > endIndex) { + index--; + long playTime = startEndTimes[index]; + if (lastPlayTime != playTime) { + animateSkipToEnds(playTime, lastPlayTime); + animateValuesInRange(playTime, lastPlayTime); + lastPlayTime = playTime; + } + } + } + if (currentPlayTime != lastPlayTime) { + animateSkipToEnds(currentPlayTime, lastPlayTime); + animateValuesInRange(currentPlayTime, lastPlayTime); + } + } + + /** + * Looks through startEndTimes for playTime. If it is in startEndTimes, the index after + * is returned. Otherwise, it returns the index at which it would be placed if it were + * to be inserted. + */ + private int findNextIndex(long playTime, long[] startEndTimes) { + int index = Arrays.binarySearch(startEndTimes, playTime); + if (index < 0) { + index = -index - 1; + } else { + index++; + } + return index; + } + + @Override + void animateSkipToEnds(long currentPlayTime, long lastPlayTime) { + initAnimation(); + + if (lastPlayTime > currentPlayTime) { + notifyStartListeners(true); + for (int i = mEvents.size() - 1; i >= 0; i--) { + AnimationEvent event = mEvents.get(i); + Node node = event.mNode; + if (event.mEvent == AnimationEvent.ANIMATION_END + && node.mStartTime != DURATION_INFINITE + ) { + Animator animator = node.mAnimation; + long start = node.mStartTime; + long end = node.mTotalDuration == DURATION_INFINITE + ? Long.MAX_VALUE : node.mEndTime; + if (currentPlayTime <= start && start < lastPlayTime) { + animator.animateSkipToEnds( + 0, + lastPlayTime - node.mStartTime + ); + mPlayingSet.remove(node); + } else if (start <= currentPlayTime && currentPlayTime <= end) { + animator.animateSkipToEnds( + currentPlayTime - node.mStartTime, + lastPlayTime - node.mStartTime + ); + if (!mPlayingSet.contains(node)) { + mPlayingSet.add(node); + } + } + } + } + if (currentPlayTime <= 0) { + notifyEndListeners(true); + } + } else { + notifyStartListeners(false); + int eventsSize = mEvents.size(); + for (int i = 0; i < eventsSize; i++) { + AnimationEvent event = mEvents.get(i); + Node node = event.mNode; + if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED + && node.mStartTime != DURATION_INFINITE + ) { + Animator animator = node.mAnimation; + long start = node.mStartTime; + long end = node.mTotalDuration == DURATION_INFINITE + ? Long.MAX_VALUE : node.mEndTime; + if (lastPlayTime < end && end <= currentPlayTime) { + animator.animateSkipToEnds( + end - node.mStartTime, + lastPlayTime - node.mStartTime + ); + mPlayingSet.remove(node); + } else if (start <= currentPlayTime && currentPlayTime <= end) { + animator.animateSkipToEnds( + currentPlayTime - node.mStartTime, + lastPlayTime - node.mStartTime + ); + if (!mPlayingSet.contains(node)) { + mPlayingSet.add(node); + } + } + } + } + if (currentPlayTime >= getTotalDuration()) { + notifyEndListeners(false); + } + } + } + + @Override + void animateValuesInRange(long currentPlayTime, long lastPlayTime) { + initAnimation(); + + if (lastPlayTime < 0 || (lastPlayTime == 0 && currentPlayTime > 0)) { + notifyStartListeners(false); + } else { + long duration = getTotalDuration(); + if (duration >= 0 + && (lastPlayTime > duration || (lastPlayTime == duration + && currentPlayTime < duration)) + ) { + notifyStartListeners(true); + } + } + + int eventsSize = mEvents.size(); + for (int i = 0; i < eventsSize; i++) { + AnimationEvent event = mEvents.get(i); + Node node = event.mNode; + if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED + && node.mStartTime != DURATION_INFINITE + ) { + Animator animator = node.mAnimation; + long start = node.mStartTime; + long end = node.mTotalDuration == DURATION_INFINITE + ? Long.MAX_VALUE : node.mEndTime; + if ((start < currentPlayTime && currentPlayTime < end) + || (start == currentPlayTime && lastPlayTime < start) + || (end == currentPlayTime && lastPlayTime > end) + ) { + animator.animateValuesInRange( + currentPlayTime - node.mStartTime, + Math.max(-1, lastPlayTime - node.mStartTime) + ); + } + } + } + } + + private long[] ensureChildStartAndEndTimes() { + if (mChildStartAndStopTimes == null) { + LongArray startAndEndTimes = new LongArray(); + getStartAndEndTimes(startAndEndTimes, 0); + long[] times = startAndEndTimes.toArray(); + Arrays.sort(times); + mChildStartAndStopTimes = times; + } + return mChildStartAndStopTimes; + } + + void getStartAndEndTimes(LongArray times, long offset) { + int eventsSize = mEvents.size(); + for (int i = 0; i < eventsSize; i++) { + AnimationEvent event = mEvents.get(i); + if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED + && event.mNode.mStartTime != DURATION_INFINITE + ) { + event.mNode.mAnimation.getStartAndEndTimes(times, offset + event.mNode.mStartTime); + } + } + } + + @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; + } + + /** + * 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. + * On {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, an AnimatorSet + * that hasn't been {@link #start()}ed, will issue + * {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator, boolean)} + * and {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator, boolean)} + * events. + * + * @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(); + + long lastPlayTime = mSeekState.getPlayTime(); + if (!isStarted() || isPaused()) { + if (mReversing && !isStarted()) { + throw new UnsupportedOperationException("Error: Something went wrong. mReversing" + + " should not be set when AnimatorSet is not started."); + } + if (!mSeekState.isActive()) { + findLatestEventIdForTime(0); + initChildren(); + // Set all the values to start values. + skipToEndValue(!mReversing); + mSeekState.setPlayTime(0, mReversing); + } + } + mSeekState.setPlayTime(playTime, mReversing); + animateBasedOnPlayTime(playTime, lastPlayTime, mReversing); + } + + /** + * Returns the milliseconds elapsed since the start of the animation. + * + *

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; + 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, getPlayTimeForNodeIncludingDelay(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; } - public void playTogether(Collection animators) {} - - public AnimatorSet setDuration(long duration) { return this; } - - public void playTogether(Animator[] animators) {} - - public ArrayList getChildAnimations() { - return new ArrayList(0); + /** + * @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, getPlayTimeForNodeIncludingDelay(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, getPlayTimeForNodeIncludingDelay(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; + if (node.mAnimation.pulseAnimationFrame((long) (animPlayTime * durationScale))) { + node.mEnded = true; + } + } + } + + private long getPlayTimeForNodeIncludingDelay(long overallPlayTime, Node node) { + return getPlayTimeForNodeIncludingDelay(overallPlayTime, node, mReversing); + } + + private long getPlayTimeForNodeIncludingDelay( + long overallPlayTime, + Node node, + boolean inReverse + ) { + if (inReverse) { + overallPlayTime = getTotalDuration() - overallPlayTime; + return node.mEndTime - overallPlayTime; + } else { + return overallPlayTime - node.mStartTime; + } + } + + private void startAnimation() { + addAnimationEndListener(); + + // 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); + + if (mSeekState.isActive()) { + // 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, getPlayTimeForNodeIncludingDelay(playTime, node)); + } + } + } + + // Remove all the finished anims + 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 addAnimationEndListener() { + for (int i = 1; i < mNodes.size(); i++) { + mNodes.get(i).mAnimation.addListener(mAnimationEndListener); + } + } + + private void removeAnimationEndListener() { + for (int i = 1; i < mNodes.size(); i++) { + mNodes.get(i).mAnimation.removeListener(mAnimationEndListener); + } + } + + 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); + // TODO: need a function that accounts for infinite duration to compare time + if (event.getTime() != DURATION_INFINITE && 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(); + notifyEndListeners(mReversing); + removeAnimationEndListener(); + 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.mStartListenersCalled = false; + anim.mPlayingSet = new ArrayList(); + anim.mNodeMap = new ArrayMap(); + anim.mNodes = new ArrayList(nodeCount); + anim.mEvents = new ArrayList(); + anim.mAnimationEndListener = 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 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(mAnimationEndListener); + 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. + *

+ * 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 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 visited = new ArrayList(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() { + @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 t1 - t2 > 0 ? 1 : -1; + } + }); + + 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 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 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 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 mSiblings; + + /** + * Parent nodes are the nodes with animations preceding current node's animation. Parent + * nodes here are derived from user defined animation sequence. + */ + ArrayList 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(); + } + if (!mSiblings.contains(node)) { + mSiblings.add(node); + node.addSibling(this); + } + } + + public void addParent(Node node) { + if (mParents == null) { + mParents = new ArrayList(); + } + if (!mParents.contains(node)) { + mParents.add(node); + node.addChild(this); + } + } + + public void addParents(ArrayList 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) { + // Clamp the play time + if (getTotalDuration() != DURATION_INFINITE) { + mPlayTime = Math.min(playTime, getTotalDuration() - mStartDelay); + } else { + mPlayTime = playTime; + } + 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 Builder object is a utility class to facilitate adding animations to a + * AnimatorSet along with the relationships between the various animations. The + * intention of the Builder methods, along with the {@link + * AnimatorSet#play(Animator) play()} method of AnimatorSet is to make it possible + * to express the dependency relationships of animations in a natural way. Developers can also + * use the {@link 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. + *

+ *

The Builder object cannot be constructed directly, but is rather constructed + * internally via a call to {@link AnimatorSet#play(Animator)}.

+ *

+ *

For example, this sets up a AnimatorSet to play anim1 and anim2 at the same time, anim3 to + * play when anim2 finishes, and anim4 to play when anim3 finishes:

+ *
+	 *     AnimatorSet s = new AnimatorSet();
+	 *     s.play(anim1).with(anim2);
+	 *     s.play(anim2).before(anim3);
+	 *     s.play(anim4).after(anim3);
+	 * 
+ *

+ *

Note in the example that both {@link 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.

+ *

+ *

It is possible to make several calls into the same Builder object to express + * multiple relationships. However, note that it is only the animation passed into the initial + * {@link AnimatorSet#play(Animator)} method that is the dependency in any of the successive + * calls to the Builder object. For example, the following code starts both anim2 + * and anim3 when anim1 ends; there is no direct dependency relationship between anim2 and + * anim3: + *

+	 *   AnimatorSet s = new AnimatorSet();
+	 *   s.play(anim1).before(anim2).before(anim3);
+	 * 
+ * If the desired result is to play anim1 then anim2 then anim3, this code expresses the + * relationship correctly:

+ *
+	 *   AnimatorSet s = new AnimatorSet();
+	 *   s.play(anim1).before(anim2);
+	 *   s.play(anim2).before(anim3);
+	 * 
+ *

+ *

Note that it is possible to express relationships that cannot be resolved and will not + * result in sensible results. For example, play(anim1).after(anim1) makes no + * sense. In general, circular dependencies like this one (or more indirect ones where a depends + * on b, which depends on c, which depends on a) should be avoided. Only create AnimatorSets + * that can boil down to a simple, one-way relationship of animations starting with, before, and + * after other, different, animations.

+ */ + public class Builder { + + /** + * This tracks the current node being processed. It is supplied to the play() method + * of AnimatorSet and passed into the constructor of Builder. + */ + private Node mCurrentNode; + + /** + * package-private constructor. Builders are only constructed by AnimatorSet, when the + * play() method is called. + * + * @param anim The animation that is the dependency for the other animations passed into + * the other methods of this Builder object. + */ + Builder(Animator anim) { + 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 Builder 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 Builder 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 Builder object + * to start when the animation supplied in this method call ends. + * + * @param anim The animation whose end will cause the animation supplied to the + * {@link 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 Builder object + * to play when the given amount of time elapses. + * + * @param delay The number of milliseconds that should elapse before the + * animation starts. + */ + public Builder after(long delay) { + // setup a ValueAnimator just to run the clock + ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); + anim.setDuration(delay); + after(anim); + return this; + } + + } + +} \ No newline at end of file diff --git a/src/api-impl/android/animation/ArgbEvaluator.java b/src/api-impl/android/animation/ArgbEvaluator.java index ee988386..f79eda26 100644 --- a/src/api-impl/android/animation/ArgbEvaluator.java +++ b/src/api-impl/android/animation/ArgbEvaluator.java @@ -1,5 +1,9 @@ package android.animation; -public class ArgbEvaluator { +public class ArgbEvaluator implements TypeEvaluator { + + public static ArgbEvaluator getInstance() { + return null; + } } diff --git a/src/api-impl/android/animation/ObjectAnimator.java b/src/api-impl/android/animation/ObjectAnimator.java index bb13032f..878a1188 100644 --- a/src/api-impl/android/animation/ObjectAnimator.java +++ b/src/api-impl/android/animation/ObjectAnimator.java @@ -1,46 +1,648 @@ +/* + * 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 java.lang.reflect.Method; - +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; -public class ObjectAnimator extends ValueAnimator { +/** + * 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. + * + *

Animators can be created from either code or resource files, as shown here:

+ * + * {@sample development/samples/ApiDemos/res/anim/object_animator.xml ObjectAnimatorResources} + * + *

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:

+ * + * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh.xml + * PropertyValuesHolderResources} + * + *

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.

+ * + * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh_kf_interpolated.xml KeyframeResources} + * + *
+ *

Developer Guides

+ *

For more information about animating with {@code ObjectAnimator}, read the + * Property + * Animation developer guide.

+ *
+ * + * @see #setPropertyName(String) + * + */ +public final class ObjectAnimator extends ValueAnimator { + private static final String LOG_TAG = "ObjectAnimator"; + private static final boolean DBG = false; + + private Object mTarget; + + private String mPropertyName; + + private Property mProperty; + + private boolean mAutoCancel = false; + + /** + * Sets the name of the property that will be animated. This name is used to derive + * a setter function that will be called to set animated values. + * For example, a property name of foo will result + * in a call to the function setFoo() on the target object. If either + * valueFrom or valueTo is null, then a getter function will + * also be derived and called. + * + *

For best performance of the mechanism that calls the setter function determined by the + * name of the property being animated, use float or int typed values, + * and make the setter function for those properties have a void return value. This + * will cause the code to take an optimized path for these constrained circumstances. Other + * property types and return types will work, but will have more overhead in processing + * the requests due to normal reflection mechanisms.

+ * + *

Note that the setter function derived from this property name + * must take the same parameter type as the + * valueFrom and valueTo properties, otherwise the call to + * the setter function will fail.

+ * + *

If this ObjectAnimator has been set up to animate several properties together, + * using more than one PropertyValuesHolder objects, then setting the propertyName simply + * sets the propertyName in the first of those PropertyValuesHolder objects.

+ * + * @param propertyName The name of the property being animated. Should not be null. + */ + public void setPropertyName(@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.getProperty_name(); + valuesHolder.setProperty_name(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.getProperty_name(); + valuesHolder.setProperty(property); + mValuesMap.remove(oldName); + mValuesMap.put(mPropertyName, valuesHolder); + } + if (mProperty != null) { + mPropertyName = property.getName(); + } + mProperty = property; + // New property/values/target should cause re-initialization prior to starting + mInitialized = false; + } + + /** + * Gets the name of the property that will be animated. This name will be used to derive + * a setter function that will be called to set animated values. + * For example, a property name of foo will result + * in a call to the function setFoo() on the target object. If either + * valueFrom or valueTo is null, then a getter function will + * also be derived and called. + * + *

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#getProperty_name() 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).

+ */ + @Nullable public String getPropertyName() { - return null; + 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].getProperty_name(); + } + } + return propertyName; } - public static ObjectAnimator ofFloat(Object target, String xPropertyName, String yPropertyName, Path path) { - return new ObjectAnimator(); + @Override + String getNameForTrace() { + return "animator:" + getPropertyName(); } - public static ObjectAnimator ofFloat(T target, Property xProperty, Property yProperty, Path path) { - return new ObjectAnimator(); + /** + * 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() { } - public static ObjectAnimator ofFloat(T target, Property property, float... values) { - return new ObjectAnimator(); + /** + * Private utility constructor that initializes the target object and name of the + * property being animated. + * + * @param target The object whose property is to be animated. This object should + * have a public method on it called setName(), where name is + * the value of the propertyName parameter. + * @param propertyName The name of the property being animated. + */ + private ObjectAnimator(Object target, String propertyName) { + 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 ObjectAnimator(T target, Property 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 setName(), where name is + * the value of the propertyName parameter. + * @param propertyName The name of the property being animated. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static ObjectAnimator ofInt(Object target, String propertyName, int... values) { + ObjectAnimator anim = new ObjectAnimator(target, propertyName); + anim.setIntValues(values); + return anim; + } + + /** + * Constructs and returns an ObjectAnimator that animates between int values. A single + * value implies that that value is the one being animated to, 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 ObjectAnimator ofInt(T target, Property property, int... values) { + ObjectAnimator anim = new ObjectAnimator(target, property); + anim.setIntValues(values); + return anim; + } + + /** + * Constructs and returns an ObjectAnimator that animates between 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 setName(), where name is + * the value of the propertyName parameter. + * @param propertyName The name of the property being animated. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static ObjectAnimator 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 ObjectAnimator ofArgb(T target, Property 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 setName(), where name is + * the value of the propertyName parameter. + * @param propertyName The name of the property being animated. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) { - return new ObjectAnimator(); + ObjectAnimator anim = new ObjectAnimator(target, propertyName); + anim.setFloatValues(values); + return anim; } - public static ObjectAnimator ofInt(T target, String propertyName, int... values) throws ReflectiveOperationException { - Method setter = target.getClass().getMethod("set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1), int.class); - setter.invoke(target, values[values.length - 1]); - return new ObjectAnimator(); + /** + * 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 ObjectAnimator ofFloat(T target, Property property, + float... values) { + ObjectAnimator anim = new ObjectAnimator(target, property); + anim.setFloatValues(values); + return anim; } - public ObjectAnimator setDuration(long duration) {return this;} - - public void setAutoCancel(boolean autoCancel) {} - - public void setPropertyName(String propertyName) {} - - public static ObjectAnimator ofPropertyValuesHolder(Object target, PropertyValuesHolder... values) { - return new ObjectAnimator(); + /** + * 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). + * + *

Note: 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 setName(), where name is + * the value of the propertyName parameter. + * @param propertyName The name of the property being animated. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static ObjectAnimator ofObject(Object target, String propertyName, + TypeEvaluator evaluator, Object... values) { + ObjectAnimator anim = new ObjectAnimator(target, propertyName); + anim.setObjectValues(values); + anim.setEvaluator(evaluator); + return anim; } + /** + * Constructs and returns an ObjectAnimator that animates between the sets of values specified + * in PropertyValueHolder objects. This variant should be used when animating + * several properties at once with the same ObjectAnimator, since PropertyValuesHolder allows + * you to associate a set of animation values with a property name. + * + * @param target The object whose property is to be animated. Depending on how the + * PropertyValuesObjects were constructed, the target object should either have the {@link + * android.util.Property} objects used to construct the PropertyValuesHolder objects or (if the + * PropertyValuesHOlder objects were created with property names) the target object should have + * public methods on it called setName(), where name is the name of + * the property passed in as the propertyName parameter for each of the + * PropertyValuesHolder objects. + * @param values A set of PropertyValuesHolder objects whose values will be animated between + * over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + @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.getProperty_name() == null || + !pvhMine.getProperty_name().equals(pvhTheirs.getProperty_name())) { + 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 startDelay, the + * function is called after that delay ends. + * It takes care of the final initialization steps for the + * animation. This includes setting mEvaluator, if the user has not yet + * set it up, and the setter/getter methods, if the user did not supply + * them. + * + *

Overriders of this method should call the superclass method to cause + * internal mechanisms to be set up correctly.

+ */ + @Override + void initAnimation() { + if (!mInitialized) { + // mValueType may change due to setter/getter setup; do this before calling super.init(), + // which uses mValueType to set up the default type evaluator. + 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 + * ObjectAnimator.ofInt(target, propertyName, 0, 10).setDuration(500).start(). + */ + @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; + } + + @Override + public void setTarget(@Nullable Object target) { + final Object oldTarget = getTarget(); + if (oldTarget != target) { + if (isStarted()) { + cancel(); + } + mTarget = 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 end() + * function is called, to set the final value on the property. + * + *

Overrides of this method must call the superclass to perform the calculation + * of the animated value.

+ * + * @param fraction The elapsed fraction of the animation. + */ + @Override + void animateValue(float fraction) { + final Object target = getTarget(); + 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/src/api-impl/android/animation/PropertyValuesHolder.java b/src/api-impl/android/animation/PropertyValuesHolder.java index bd8a7e00..b32efe22 100644 --- a/src/api-impl/android/animation/PropertyValuesHolder.java +++ b/src/api-impl/android/animation/PropertyValuesHolder.java @@ -1,11 +1,141 @@ package android.animation; -public class PropertyValuesHolder{ +import java.lang.reflect.Method; +import android.util.Log; +import android.util.Property; + +public class PropertyValuesHolder { + + private float values_float[]; + private int values_int[]; + private Object values_object[]; + private Object value; + private String property_name; + private Method setter; + public static PropertyValuesHolder ofFloat(String propertyName, float... values) { - return null; + PropertyValuesHolder propertyValuesHolder = new PropertyValuesHolder(); + propertyValuesHolder.values_float = values; + propertyValuesHolder.property_name = propertyName; + return propertyValuesHolder; } public static PropertyValuesHolder ofObject(String propertyName, TypeEvaluator evaluator, Object... values) { + PropertyValuesHolder propertyValuesHolder = new PropertyValuesHolder(); + propertyValuesHolder.values_object = values; + propertyValuesHolder.property_name = propertyName; + return propertyValuesHolder; + } + + public static PropertyValuesHolder ofInt(String propertyName, int... values) { + PropertyValuesHolder propertyValuesHolder = new PropertyValuesHolder(); + propertyValuesHolder.values_int = values; + propertyValuesHolder.property_name = propertyName; + return propertyValuesHolder; + } + + public static PropertyValuesHolder ofFloat(Property property, float... values) { + PropertyValuesHolder propertyValuesHolder = new PropertyValuesHolder(); + propertyValuesHolder.values_float = values; + propertyValuesHolder.property_name = property.getName(); + return propertyValuesHolder; + } + + public static PropertyValuesHolder ofObject(Property property, TypeEvaluator evaluator, Object... values) { + PropertyValuesHolder propertyValuesHolder = new PropertyValuesHolder(); + propertyValuesHolder.values_object = values; + propertyValuesHolder.property_name = property.getName(); + return propertyValuesHolder; + } + + public static PropertyValuesHolder ofInt(Property property, int... values) { + PropertyValuesHolder propertyValuesHolder = new PropertyValuesHolder(); + propertyValuesHolder.values_int = values; + propertyValuesHolder.property_name = property.getName(); + return propertyValuesHolder; + } + + public void setIntValues(int... values) { + values_int = values; + } + + public void setFloatValues(float... values) { + values_float = values; + } + + public void setObjectValues(Object... values) { + values_object = values; + } + + public String getProperty_name() { + return property_name; + } + + public void setProperty_name(String propertyName) { + this.property_name = propertyName; + } + + public void setProperty(Property property) { + property_name = property.getName(); + } + + public void init() {} + + public Object getAnimatedValue() { + return value; + } + + public void setEvaluator(TypeEvaluator value) {} + + public void calculateValue(float fraction) { + if (values_object != null) { + value = values_object[(int) (fraction * (values_object.length - 1) + 0.5f)]; + } else if (values_float != null) { + int i = (int) (fraction * (values_float.length - 1)); + float f = fraction * (values_float.length - 1) - i; + value = values_float[i] * (1 - f) + ((f!=0.f) ? values_float[i + 1] * f : 0.f); + } else if (values_int != null) { + int i = (int) (fraction * (values_int.length - 1)); + float f = fraction * (values_int.length - 1) - i; + value = (int)(values_int[i] * (1 - f) + ((f!=0.f) ? values_int[i + 1] * f : 0.f) + 0.5f); + } else { + Log.e("PropertyValuesHolder", "No values set"); + } + } + + public PropertyValuesHolder clone() { return null; } + + public void setupSetterAndGetter(Object target) { + try { + Class clazz; + if (values_float != null) { + clazz = float.class; + } else { + clazz = values_object[0].getClass(); + } + setter = target.getClass().getMethod("set" + property_name.substring(0, 1).toUpperCase() + property_name.substring(1), clazz); + } catch (NoSuchMethodException e) { + Log.e("PropertyValuesHolder", "failed to find setter", e); + } + } + + public void setupStartValue(Object target) { + } + + public void setupEndValue(Object target) { + } + + public void setAnimatedValue(Object target) { + if (setter != null && value != null) { + try { + setter.invoke(target, value); + } catch (ReflectiveOperationException e) { + Log.e("PropertyValuesHolder", "failed to invoke setter", e); + } + } else { + Log.e("PropertyValuesHolder", "no setter or value set"); + } + } } diff --git a/src/api-impl/android/animation/ValueAnimator.java b/src/api-impl/android/animation/ValueAnimator.java index 0832a08c..6fdf2c63 100644 --- a/src/api-impl/android/animation/ValueAnimator.java +++ b/src/api-impl/android/animation/ValueAnimator.java @@ -1,79 +1,1629 @@ +/* + * 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; -public class ValueAnimator extends Animator { +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; - private AnimatorUpdateListener update_listener; - private Object value_end; +import android.annotation.NonNull; +import android.annotation.Nullable; +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; - public static ValueAnimator ofFloat(float... values) { - ValueAnimator animator = new ValueAnimator(); - animator.value_end = values[values.length - 1]; - return animator; +/** + * This class provides a simple timing engine for running animations + * which calculate animated values and set them on target objects. + * + *

There is a single timing pulse that all animations use. It runs in a + * custom handler to ensure that property changes happen on the UI thread.

+ * + *

By default, ValueAnimator uses non-linear time interpolation, via the + * {@link AccelerateDecelerateInterpolator} class, which accelerates into and decelerates + * out of an animation. This behavior can be changed by calling + * {@link ValueAnimator#setInterpolator(TimeInterpolator)}.

+ * + *

Animators can be created from either code or resource files. Here is an example + * of a ValueAnimator resource file:

+ * + * {@sample development/samples/ApiDemos/res/anim/animator.xml ValueAnimatorResources} + * + *

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:

+ * + * {@sample development/samples/ApiDemos/res/anim/value_animator_pvh_kf.xml + * ValueAnimatorKeyframeResources} + * + *
+ *

Developer Guides

+ *

For more information about animating with {@code ValueAnimator}, read the + * Property + * Animation developer guide.

+ *
+ */ +@SuppressWarnings("unchecked") +public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback { + private static final String TAG = "ValueAnimator"; + private static final boolean DEBUG = false; + private static final boolean TRACE_ANIMATION_FRACTION = false; + + /** + * Internal constants + */ + + /** + * System-wide animation scale. + * + *

To check whether animations are enabled system-wise use {@link #areAnimatorsEnabled()}. + */ + private static float sDurationScale = 1.0f; + + private static final ArrayList> + sDurationScaleChangeListeners = new ArrayList<>(); + + /** + * 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; + + /** + * 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 mUpdateListeners = null; + + /** + * The property/value sets being animated. + */ + PropertyValuesHolder[] mValues; + + /** + * A hashmap of the PropertyValuesHolder objects. This map is used to lookup animated values + * by property name during calls to getAnimatedValue(String). + */ + HashMap mValuesMap; + + /** + * If set to non-negative value, this will override {@link #sDurationScale}. + */ + private float mDurationScale = -1f; + + /** + * Animation handler used to schedule updates for this animation. + */ + private AnimationHandler mAnimationHandler; + + /** + * Public constants + */ + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + public @interface RepeatMode {} + + /** + * When the animation reaches the end and repeatCount is INFINITE + * or a positive value, the animation restarts from the beginning. + */ + public static final int RESTART = 1; + /** + * When the animation reaches the end and repeatCount is INFINITE + * or a positive value, the animation reverses direction on every iteration. + */ + public static final int REVERSE = 2; + /** + * This value used used with the {@link #setRepeatCount(int)} property to repeat + * the animation indefinitely. + */ + public static final int INFINITE = -1; + + /** + * @hide + */ + public static void setDurationScale(float durationScale) { + sDurationScale = durationScale; + List> listenerCopy; + + synchronized (sDurationScaleChangeListeners) { + listenerCopy = new ArrayList<>(sDurationScaleChangeListeners); + } + + int listenersSize = listenerCopy.size(); + for (int i = 0; i < listenersSize; i++) { + final DurationScaleChangeListener listener = listenerCopy.get(i).get(); + if (listener != null) { + listener.onChanged(durationScale); + } + } } - public static ValueAnimator ofObject(TypeEvaluator evaluator, Object[] values) { - ValueAnimator animator = new ValueAnimator(); - animator.value_end = values[values.length - 1]; - return animator; + /** + * Returns the system-wide scaling factor for Animator-based animations. + * + * This affects both the start delay and duration of all such animations. Setting to 0 will + * cause animations to end immediately. The default value is 1.0f. + * + * @return the duration scale. + */ + public static float getDurationScale() { + return sDurationScale; } + /** + * Registers a {@link DurationScaleChangeListener} + * + * This listens for changes to the system-wide scaling factor for Animator-based animations. + * Listeners will be called on the main thread. + * + * @param listener the listener to register. + * @return true if the listener was registered. + */ + public static boolean registerDurationScaleChangeListener( + @NonNull DurationScaleChangeListener listener) { + int posToReplace = -1; + synchronized (sDurationScaleChangeListeners) { + for (int i = 0; i < sDurationScaleChangeListeners.size(); i++) { + final WeakReference ref = + sDurationScaleChangeListeners.get(i); + if (ref.get() == null) { + if (posToReplace == -1) { + posToReplace = i; + } + } else if (ref.get() == listener) { + return false; + } + } + if (posToReplace != -1) { + sDurationScaleChangeListeners.set(posToReplace, new WeakReference<>(listener)); + return true; + } else { + return sDurationScaleChangeListeners.add(new WeakReference<>(listener)); + } + } + } + + /** + * Unregisters a DurationScaleChangeListener. + * + * @see #registerDurationScaleChangeListener(DurationScaleChangeListener) + * @param listener the listener to unregister. + * @return true if the listener was unregistered. + */ + public static boolean unregisterDurationScaleChangeListener( + @NonNull DurationScaleChangeListener listener) { + synchronized (sDurationScaleChangeListeners) { + WeakReference listenerRefToRemove = null; + for (WeakReference listenerRef : + sDurationScaleChangeListeners) { + if (listenerRef.get() == listener) { + listenerRefToRemove = listenerRef; + break; + } + } + return sDurationScaleChangeListeners.remove(listenerRefToRemove); + } + } + + /** + * 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). + * + *

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 + * true. + */ + 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 animator = new ValueAnimator(); - animator.value_end = values[values.length - 1]; - return animator; + 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. + * + *

Note: 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. + * + *

Since ValueAnimator does not know how to animate between arbitrary Objects, this + * factory method also takes a TypeEvaluator object that the ValueAnimator will use + * to perform that interpolation. + * + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the ncessry interpolation between the Object values to derive the animated + * value. + * @param values A set of values that the animation will animate between over time. + * @return A ValueAnimator object that is set up to animate between the given values. + */ + public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values) { + ValueAnimator anim = new ValueAnimator(); + anim.setObjectValues(values); + anim.setEvaluator(evaluator); + return anim; + } + + /** + * Sets int values that will be animated between. A single + * value implies that that value is the one being animated to. However, this is not typically + * useful in a ValueAnimator object because there is no way for the object to determine the + * starting value for the animation (unlike ObjectAnimator, which can derive that value + * from the target object and property being animated). Therefore, there should typically + * be two or more values. + * + *

If there are already multiple sets of values defined for this ValueAnimator via more + * than one PropertyValuesHolder object, this method will set the values for the first + * of those objects.

+ * + * @param values A set of values that the animation will animate between over time. + */ + public void setIntValues(int... values) { + if (values == null || values.length == 0) { + return; + } + if (mValues == null || mValues.length == 0) { + setValues(PropertyValuesHolder.ofInt("", values)); + } else { + PropertyValuesHolder valuesHolder = mValues[0]; + valuesHolder.setIntValues(values); + } + // New property/values/target should cause re-initialization prior to starting + mInitialized = false; + } + + /** + * Sets float values that will be animated between. A single + * value implies that that value is the one being animated to. However, this is not typically + * useful in a ValueAnimator object because there is no way for the object to determine the + * starting value for the animation (unlike ObjectAnimator, which can derive that value + * from the target object and property being animated). Therefore, there should typically + * be two or more values. + * + *

If there are already multiple sets of values defined for this ValueAnimator via more + * than one PropertyValuesHolder object, this method will set the values for the first + * of those objects.

+ * + * @param values A set of values that the animation will animate between over time. + */ + public void setFloatValues(float... values) { + if (values == null || values.length == 0) { + return; + } + if (mValues == null || mValues.length == 0) { + setValues(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. + * + *

Note: 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. + * + *

If there are already multiple sets of values defined for this ValueAnimator via more + * than one PropertyValuesHolder object, this method will set the values for the first + * of those objects.

+ * + *

There should be a TypeEvaluator set on the ValueAnimator that knows how to interpolate + * between these value objects. ValueAnimator only knows how to interpolate between the + * primitive types specified in the other setValues() methods.

+ * + * @param values The set of values to animate between. + */ + public void setObjectValues(Object... values) { + if (values == null || values.length == 0) { + return; + } + if (mValues == null || mValues.length == 0) { + setValues(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<>(numValues); + for (int i = 0; i < numValues; ++i) { + PropertyValuesHolder valuesHolder = values[i]; + mValuesMap.put(valuesHolder.getProperty_name(), valuesHolder); + } + // New property/values/target should cause re-initialization prior to starting + mInitialized = false; + } + + /** + * Returns the values that this ValueAnimator animates between. These values are stored in + * PropertyValuesHolder objects, even if the ValueAnimator was created with a simple list + * of value objects instead. + * + * @return PropertyValuesHolder[] An array of PropertyValuesHolder objects which hold the + * values, per property, that define the animation. + */ + public PropertyValuesHolder[] getValues() { + return mValues; + } + + /** + * This function is called immediately before processing the first animation + * frame of an animation. If there is a nonzero startDelay, the + * function is called after that delay ends. + * It takes care of the final initialization steps for the + * animation. + * + *

Overrides of this method should call the superclass method to ensure + * that internal mechanisms for the animation are set up correctly.

+ */ + void initAnimation() { + if (!mInitialized) { + if (mValues != null) { + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].init(); + } + } + mInitialized = true; + } + } + + /** + * Sets the length of the animation. The default duration is 300 milliseconds. + * + * @param duration The length of the animation, in milliseconds. This value cannot + * be negative. + * @return ValueAnimator The object called with setDuration(). This return + * value makes it easier to compose statements together that construct and then set the + * duration, as in ValueAnimator.ofInt(0, 10).setDuration(500).start(). + */ + @Override public ValueAnimator setDuration(long duration) { + if (duration < 0) { + throw new IllegalArgumentException("Animators cannot have negative duration: " + + duration); + } + mDuration = duration; return this; } - public void addUpdateListener(AnimatorUpdateListener listener) { - this.update_listener = listener; + /** + * Overrides the global duration scale by a custom value. + * + * @param durationScale The duration scale to set; or {@code -1f} to use the global duration + * scale. + * @hide + */ + public void overrideDurationScale(float durationScale) { + mDurationScale = durationScale; } - public static long getFrameDelay() { - return 20; // 20ms frame interval + private float resolveDurationScale() { + return mDurationScale >= 0f ? mDurationScale : sDurationScale; } - public PropertyValuesHolder[] getValues() { + private long getScaledDuration() { + return (long)(mDuration * resolveDurationScale()); + } + + /** + * 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 = resolveDurationScale(); + if (durationScale == 0f) { + durationScale = 1f; + } + 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 most recent value calculated by this ValueAnimator when there is just one + * property being animated. This value is only sensible while the animation is running. The main + * purpose for this read-only property is to retrieve the value from the ValueAnimator + * during a call to {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)}, which + * is called during each animation frame, immediately after the value is calculated. + * + * @return animatedValue The value most recently calculated by this ValueAnimator for + * the single property being animated. If there are several properties being animated + * (specified by several PropertyValuesHolder objects in the constructor), this function + * returns the animated value for the first of those objects. + */ + public Object getAnimatedValue() { + if (mValues != null && mValues.length > 0) { + return mValues[0].getAnimatedValue(); + } + // Shouldn't get here; should always have values unless ValueAnimator was set up wrong return null; } - public long getStartDelay() {return 0;} - public long getDuration() {return 0;} - public TimeInterpolator getInterpolator() {return null;} - public int getRepeatCount() {return 0;} - public int getRepeatMode() {return 0;} - public void setInterpolator(TimeInterpolator interpolator) {} - public void setFloatValues(float[] values) { - value_end = values[values.length - 1]; + /** + * The most recent value calculated by this ValueAnimator for propertyName. + * The main purpose for this read-only property is to retrieve the value from the + * ValueAnimator during a call to + * {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)}, which + * is called during each animation frame, immediately after the value is calculated. + * + * @return animatedValue The value most recently calculated for the named property + * by this ValueAnimator. + */ + public Object getAnimatedValue(String propertyName) { + PropertyValuesHolder valuesHolder = mValuesMap.get(propertyName); + if (valuesHolder != null) { + return valuesHolder.getAnimatedValue(); + } else { + // At least avoid crashing if called with bogus propertyName + return null; + } } - public boolean isRunning() {return false;} - public void setIntValues(int[] values) { - value_end = values[values.length - 1]; + + /** + * 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(); + } + mUpdateListeners.add(listener); + } + + /** + * Removes all listeners from the set listening to frame updates for this animation. + */ + public void removeAllUpdateListeners() { + if (mUpdateListeners == null) { + return; + } + mUpdateListeners.clear(); + mUpdateListeners = null; + } + + /** + * Removes a listener from the set listening to frame updates for this animation. + * + * @param listener the listener to be removed from the current set of update listeners + * for this animation. + */ + public void removeUpdateListener(AnimatorUpdateListener listener) { + if (mUpdateListeners == null) { + return; + } + mUpdateListeners.remove(listener); + if (mUpdateListeners.size() == 0) { + mUpdateListeners = null; + } + } + + + /** + * The time interpolator used in calculating the elapsed fraction of this animation. The + * interpolator determines whether the animation runs with linear or non-linear motion, + * such as acceleration and deceleration. The default value is + * {@link android.view.animation.AccelerateDecelerateInterpolator} + * + * @param value the interpolator to be used by this animation. A value of null + * will result in linear interpolation. + */ + @Override + public void setInterpolator(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 startValue and endValue in the constructor. But if these values + * are not one of these primitive types, or if different evaluation is desired (such as is + * necessary with int values that represent colors), a custom evaluator needs to be assigned. + * For example, when running an animation on color values, the {@link ArgbEvaluator} + * should be used to get correct RGB color interpolation. + * + *

If this ValueAnimator has only one set of values being animated between, this evaluator + * will be used for that set. If there are several sets of values being animated, which is + * the case if PropertyValuesHolder objects were set on the ValueAnimator, then the evaluator + * is assigned just to the first PropertyValuesHolder object.

+ * + * @param value the evaluator to be used this animation + */ + public void setEvaluator(TypeEvaluator value) { + if (value != null && mValues != null && mValues.length > 0) { + mValues[0].setEvaluator(value); + } + } + + /** + * Start the animation playing. This version of start() takes a boolean flag that indicates + * whether the animation should play in reverse. The flag is usually false, but may be set + * to true if called from the reverse() method. + * + *

The animation started by calling this method will be run on the thread that called + * this method. This thread should have a Looper on it (a runtime exception will be thrown if + * this is not the case). Also, if the animation will animate + * properties of objects in the view hierarchy, then the calling thread should be the UI + * thread for that view hierarchy.

+ * + * @param playBackwards Whether the ValueAnimator should start playing in reverse. + */ + private void start(boolean playBackwards) { + if (Looper.myLooper() == null) { + throw new AndroidRuntimeException("Animators may only be run on Looper threads"); + } + 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; } - public void setRepeatCount(int value) {} - public void setRepeatMode(int value) {} - public void cancel() {} - public void setEvaluator(TypeEvaluator evaluator) {} - public void setStartDelay(long startDelay) {} @Override public void start() { - if (update_listener != null) - update_listener.onAnimationUpdate(this); - super.start(); + start(false); } - public Object getAnimatedValue() { - return value_end; + @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 || mStartListenersCalled) && mListeners != null) { + if (!mRunning) { + // If it's not yet running, then start listeners weren't called. Call them now. + notifyStartListeners(mReversing); + } + notifyListeners(AnimatorCaller.ON_CANCEL, false); + } + 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(mReversing); + } + mLastFrameTime = -1; + mFirstFrameTime = -1; + mStartTime = -1; + mRunning = false; + mStarted = false; + notifyEndListeners(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; + } + + notifyStartListeners(mReversing); + } + + /** + * 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 + * repeatCount 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 + notifyListeners(AnimatorCaller.ON_REPEAT, false); + } 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 animateValuesInRange(long currentPlayTime, long lastPlayTime) { + if (currentPlayTime < 0 || lastPlayTime < -1) { + throw new UnsupportedOperationException("Error: Play time should never be negative."); + } + + initAnimation(); + long duration = getTotalDuration(); + if (lastPlayTime < 0 || (lastPlayTime == 0 && currentPlayTime > 0)) { + notifyStartListeners(false); + } else if (lastPlayTime > duration + || (lastPlayTime == duration && currentPlayTime < duration) + ) { + notifyStartListeners(true); + } + if (duration >= 0) { + lastPlayTime = Math.min(duration, lastPlayTime); + } + lastPlayTime -= mStartDelay; + currentPlayTime -= mStartDelay; + + // Check whether repeat callback is needed only when repeat count is non-zero + if (mRepeatCount > 0) { + int iteration = Math.max(0, (int) (currentPlayTime / mDuration)); + int lastIteration = Math.max(0, (int) (lastPlayTime / mDuration)); + + // Clamp iteration to [0, mRepeatCount] + iteration = Math.min(iteration, mRepeatCount); + lastIteration = Math.min(lastIteration, mRepeatCount); + + if (iteration != lastIteration) { + notifyListeners(AnimatorCaller.ON_REPEAT, false); + } + } + + if (mRepeatCount != INFINITE && currentPlayTime > (mRepeatCount + 1) * mDuration) { + throw new IllegalStateException("Can't animate a value outside of the duration"); + } else { + // Find the current fraction: + float fraction = Math.max(0, currentPlayTime) / (float) mDuration; + fraction = getCurrentIterationFraction(fraction, false); + animateValue(fraction); + } + } + + @Override + void animateSkipToEnds(long currentPlayTime, long lastPlayTime) { + boolean inReverse = currentPlayTime < lastPlayTime; + boolean doSkip; + if (currentPlayTime <= 0 && lastPlayTime > 0) { + doSkip = true; + } else { + long duration = getTotalDuration(); + doSkip = duration >= 0 && currentPlayTime >= duration && lastPlayTime < duration; + } + if (doSkip) { + notifyStartListeners(inReverse); + skipToEndValue(inReverse); + notifyEndListeners(inReverse); + } + } + + /** + * 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 * resolveDurationScale()); + } + + // 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 1.0f; + return mCurrentFraction; } - public void setObjectValues(Object[] values) {} + /** + * This method is called with the elapsed fraction of the animation during every + * animation frame. This function turns the elapsed fraction into an interpolated fraction + * and then into an animated value (from the evaluator. The function is called mostly during + * animation updates, but it is also called when the end() + * function is called, to set the final value on the property. + * + *

Overrides of this method must call the superclass to perform the calculation + * of the animated value.

+ * + * @param fraction The elapsed fraction of the animation. + */ + void animateValue(float fraction) { + if (TRACE_ANIMATION_FRACTION) { + Trace.traceCounter(Trace.TRACE_TAG_VIEW, getNameForTrace() + hashCode(), + (int) (fraction * 1000)); + } + if (mValues == null) { + return; + } + fraction = mInterpolator.getInterpolation(fraction); + mCurrentFraction = fraction; + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].calculateValue(fraction); + } + if (mSeekFraction >= 0 || mStartListenersCalled) { + callOnList(mUpdateListeners, AnimatorCaller.ON_UPDATE, this, false); + } + } + + @Override + public ValueAnimator clone() { + final ValueAnimator anim = (ValueAnimator) super.clone(); + if (mUpdateListeners != null) { + anim.mUpdateListeners = new ArrayList(mUpdateListeners); + } + anim.mSeekFraction = -1; + anim.mReversing = false; + anim.mInitialized = false; + anim.mStarted = false; + anim.mRunning = false; + anim.mPaused = false; + anim.mResumed = 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(numValues); + for (int i = 0; i < numValues; ++i) { + PropertyValuesHolder newValuesHolder = oldValues[i].clone(); + anim.mValues[i] = newValuesHolder; + anim.mValuesMap.put(newValuesHolder.getProperty_name(), newValuesHolder); + } + } + return anim; + } /** * Implementors of this interface can add themselves as update listeners @@ -87,6 +1637,100 @@ public class ValueAnimator extends Animator { * * @param animation The animation which was repeated. */ - void onAnimationUpdate(ValueAnimator animation); + void onAnimationUpdate(@NonNull 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; + } + + /** + *

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.

+ * + *

Regardless of whether or not the animation runs asynchronously, all + * listener callbacks will be called on the UI thread.

+ * + *

To be able to use this hint the following must be true:

+ *
    + *
  1. {@link #getAnimatedFraction()} is not needed (it will return undefined values).
  2. + *
  3. The animator is immutable while {@link #isStarted()} is true. Requests + * to change values, duration, delay, etc... may be ignored.
  4. + *
  5. 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.
  6. + *
  7. 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.
  8. + *
+ * @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 mAnimationHandler != null ? mAnimationHandler : AnimationHandler.getInstance(); + } + + /** + * Sets the animation handler used to schedule updates for this animator or {@code null} to use + * the default handler. + * @hide + */ + public void setAnimationHandler(@Nullable AnimationHandler animationHandler) { + mAnimationHandler = animationHandler; + } + + /** + * Listener interface for the system-wide scaling factor for Animator-based animations. + * + * @see #registerDurationScaleChangeListener(DurationScaleChangeListener) + * @see #unregisterDurationScaleChangeListener(DurationScaleChangeListener) + */ + public interface DurationScaleChangeListener { + /** + * Called when the duration scale changes. + * @param scale the duration scale + */ + void onChanged(float scale); } } diff --git a/src/api-impl/android/util/LongArray.java b/src/api-impl/android/util/LongArray.java new file mode 100644 index 00000000..654beccc --- /dev/null +++ b/src/api-impl/android/util/LongArray.java @@ -0,0 +1,229 @@ +/* + * 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.util; + +import java.util.Arrays; + +import com.android.internal.util.ArrayUtils; + +import android.annotation.Nullable; +import android.os.Build; + +/** + * Implements a growing array of long primitives. + * + * @hide + */ +public class LongArray implements Cloneable { + private static final int MIN_CAPACITY_INCREMENT = 12; + + private long[] mValues; + private int mSize; + + private LongArray(long[] array, int size) { + mValues = array; + mSize = size; //Preconditions.checkArgumentInRange(size, 0, array.length, "size"); + } + + /** + * Creates an empty LongArray with the default initial capacity. + */ + public LongArray() { + this(0); + } + + /** + * Creates an empty LongArray with the specified initial capacity. + */ + public LongArray(int initialCapacity) { + if (initialCapacity == 0) { + mValues = new long[0]; + } else { + mValues = ArrayUtils.newUnpaddedLongArray(initialCapacity); + } + mSize = 0; + } + + /** + * Creates an LongArray wrapping the given primitive long array. + */ + public static LongArray wrap(long[] array) { + return new LongArray(array, array.length); + } + + /** + * Creates an LongArray from the given primitive long array, copying it. + */ + public static LongArray fromArray(long[] array, int size) { + return wrap(Arrays.copyOf(array, size)); + } + + /** + * Changes the size of this LongArray. If this LongArray is shrinked, the backing array capacity + * is unchanged. If the new size is larger than backing array capacity, a new backing array is + * created from the current content of this LongArray padded with 0s. + */ + public void resize(int newSize) { + // Preconditions.checkArgumentNonnegative(newSize); + if (newSize <= mValues.length) { + Arrays.fill(mValues, newSize, mValues.length, 0); + } else { + ensureCapacity(newSize - mSize); + } + mSize = newSize; + } + + /** + * Appends the specified value to the end of this array. + */ + public void add(long value) { + add(mSize, value); + } + + /** + * Inserts a value at the specified position in this array. If the specified index is equal to + * the length of the array, the value is added at the end. + * + * @throws IndexOutOfBoundsException when index < 0 || index > size() + */ + public void add(int index, long value) { + ensureCapacity(1); + int rightSegment = mSize - index; + mSize++; + // ArrayUtils.checkBounds(mSize, index); + + if (rightSegment != 0) { + // Move by 1 all values from the right of 'index' + System.arraycopy(mValues, index, mValues, index + 1, rightSegment); + } + + mValues[index] = value; + } + + /** + * Adds the values in the specified array to this array. + */ + public void addAll(LongArray values) { + final int count = values.mSize; + ensureCapacity(count); + + System.arraycopy(values.mValues, 0, mValues, mSize, count); + mSize += count; + } + + /** + * Ensures capacity to append at least count values. + */ + private void ensureCapacity(int count) { + final int currentSize = mSize; + final int minCapacity = currentSize + count; + if (minCapacity >= mValues.length) { + final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ? + MIN_CAPACITY_INCREMENT : currentSize >> 1); + final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity; + final long[] newValues = ArrayUtils.newUnpaddedLongArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, currentSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + @Override + public LongArray clone() { + LongArray clone = null; + try { + clone = (LongArray) super.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Returns the value at the specified position in this array. + */ + public long get(int index) { + // ArrayUtils.checkBounds(mSize, index); + return mValues[index]; + } + + /** + * Sets the value at the specified position in this array. + */ + public void set(int index, long value) { + // ArrayUtils.checkBounds(mSize, index); + mValues[index] = value; + } + + /** + * Returns the index of the first occurrence of the specified value in this + * array, or -1 if this array does not contain the value. + */ + public int indexOf(long value) { + final int n = mSize; + for (int i = 0; i < n; i++) { + if (mValues[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes the value at the specified index from this array. + */ + public void remove(int index) { + // ArrayUtils.checkBounds(mSize, index); + System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1); + mSize--; + } + + /** + * Returns the number of values in this array. + */ + public int size() { + return mSize; + } + + /** + * Returns a new array with the contents of this LongArray. + */ + public long[] toArray() { + return Arrays.copyOf(mValues, mSize); + } + + /** + * Test if each element of {@code a} equals corresponding element from {@code b} + */ + public static boolean elementsEqual(@Nullable LongArray a, @Nullable LongArray b) { + if (a == null || b == null) return a == b; + if (a.mSize != b.mSize) return false; + for (int i = 0; i < a.mSize; i++) { + if (a.get(i) != b.get(i)) { + return false; + } + } + return true; + } +} diff --git a/src/api-impl/android/view/ViewAnimationUtils.java b/src/api-impl/android/view/ViewAnimationUtils.java index fdb17252..7cee927c 100644 --- a/src/api-impl/android/view/ViewAnimationUtils.java +++ b/src/api-impl/android/view/ViewAnimationUtils.java @@ -1,10 +1,11 @@ package android.view; import android.animation.Animator; +import android.animation.ValueAnimator; public class ViewAnimationUtils { public static Animator createCircularReveal(View view, int centerX, int centerY, float startRadius, float endRadius) { - return new Animator(); + return new ValueAnimator(); } } diff --git a/src/api-impl/android/view/ViewPropertyAnimator.java b/src/api-impl/android/view/ViewPropertyAnimator.java index 7fc9c036..e865c521 100644 --- a/src/api-impl/android/view/ViewPropertyAnimator.java +++ b/src/api-impl/android/view/ViewPropertyAnimator.java @@ -2,6 +2,7 @@ package android.view; import android.animation.Animator; import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; import android.os.Handler; public class ViewPropertyAnimator { @@ -71,7 +72,7 @@ public class ViewPropertyAnimator { @Override public void run() { if (listener != null) - listener.onAnimationEnd(new Animator()); + listener.onAnimationEnd(new ValueAnimator()); } }, startDelay+duration); } diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index 5f4dd669..4bdef1f0 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -3,6 +3,7 @@ srcs = [ 'android/R.java', 'android/accounts/Account.java', 'android/accounts/AccountManager.java', + 'android/animation/AnimationHandler.java', 'android/animation/Animator.java', 'android/animation/AnimatorInflater.java', 'android/animation/AnimatorListenerAdapter.java', @@ -436,6 +437,7 @@ srcs = [ 'android/util/MathUtils.java', 'android/util/LayoutDirection.java', 'android/util/Log.java', + 'android/util/LongArray.java', 'android/util/LongSparseArray.java', 'android/util/LruCache.java', 'android/util/MapCollections.java',