/* * 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.content.Context; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import java.util.ArrayList; import java.util.HashMap; /** * This class provides a simple timing engine for running animations * which calculate animated values and set them on target objects. * *
There is a single timing pulse that all animations use. It runs in a * custom handler to ensure that property changes happen on the UI thread.
* *By default, Animator 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 Animator#setInterpolator(Interpolator)}.
*/ public class AnimatorrepeatCount
is INFINITE
* or a positive value, the animation restarts from the beginning.
*/
public static final int RESTART = 1;
/**
* When the animation reaches the end and repeatCount
is INFINITE
* or a positive value, the animation reverses direction on every iteration.
*/
public static final int REVERSE = 2;
/**
* This value used used with the {@link #setRepeatCount(int)} property to repeat
* the animation indefinitely.
*/
public static final int INFINITE = -1;
/**
* Creates a new Animator object. This default constructor is primarily for
* use internally; the other constructors which take parameters are more generally
* useful.
*/
public Animator() {
}
/**
* Constructs an Animator object with the specified duration and set of
* values. If the values are a set of PropertyValuesHolder objects, then these objects
* define the potentially multiple properties being animated and the values the properties are
* animated between. Otherwise, the values define a single set of values animated between.
*
* @param duration The length of the animation, in milliseconds.
* @param values The set of values to animate between. If these values are not
* PropertyValuesHolder objects, then there should be more than one value, since the values
* determine the interval to animate between.
*/
public Animator(long duration, T...values) {
mDuration = duration;
if (values.length > 0) {
setValues(values);
}
}
public void setValues(PropertyValuesHolder... values) {
int numValues = values.length;
mValues = values;
mValuesMap = new HashMapvalues
is
* a set of PropertyValuesHolder objects, these objects will become the set of properties
* animated and the values that those properties are animated between. Otherwise, this method
* will set only one set of values for the Animator. Also, if the values are not
* PropertyValuesHolder objects and if there are already multiple sets of
* values defined for this Animator via
* more than one PropertyValuesHolder objects, this method will set the values for
* the first of those objects.
*
* @param values The set of values to animate between.
*/
public void setValues(T... values) {
if (mValues == null || mValues.length == 0) {
setValues(new PropertyValuesHolder[]{
new PropertyValuesHolder("", (Object[])values)});
} else {
PropertyValuesHolder valuesHolder = mValues[0];
valuesHolder.setValues(values);
}
}
/**
* 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() { int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { mValues[i].init(); } mCurrentIteration = 0; mInitialized = true; } /** * Sets the length of the animation. * * @param duration The length of the animation, in milliseconds. */ public void setDuration(long duration) { mDuration = duration; } /** * Gets the length of the animation. * * @return The length of the animation, in milliseconds. */ public long getDuration() { return mDuration; } /** * Sets the position of the animation to the specified point in time. This time should * be between 0 and the total duration of the animation, including any repetition. If * the animation has not yet been started, then it will not advance forward after it is * set to this time; it will simply set the time to this value and perform any appropriate * actions based on that time. If the animation is already running, then setCurrentPlayTime() * will set the current playing time to this value and continue playing from that point. * * @param playTime The time, in milliseconds, to which the animation is advanced or rewound. */ public void setCurrentPlayTime(long playTime) { if (!mInitialized) { initAnimation(); } long currentTime = AnimationUtils.currentAnimationTimeMillis(); if (mPlayingState != RUNNING) { mSeekTime = playTime; mPlayingState = SEEKED; } mStartTime = currentTime - playTime; animationFrame(currentTime); } /** * Gets the current position of the animation in time, which is equal to the current * time minus the time that the animation started. An animation that is not yet started will * return a value of zero. * * @return The current position in time of the animation. */ public long getCurrentPlayTime() { if (!mInitialized || mPlayingState == STOPPED) { return 0; } return AnimationUtils.currentAnimationTimeMillis() - mStartTime; } /** * This custom, static handler handles the timing pulse that is shared by * all active animations. This approach ensures that the setting of animation * values will happen on the UI thread and that all animations will share * the same times for calculating their values, which makes synchronizing * animations possible. * */ private static class AnimationHandler extends Handler { /** * There are only two messages that we care about: ANIMATION_START and * ANIMATION_FRAME. The START message is sent when an animation's start() * method is called. It cannot start synchronously when start() is called * because the call may be on the wrong thread, and it would also not be * synchronized with other animations because it would not start on a common * timing pulse. So each animation sends a START message to the handler, which * causes the handler to place the animation on the active animations queue and * start processing frames for that animation. * The FRAME message is the one that is sent over and over while there are any * active animations to process. */ @Override public void handleMessage(Message msg) { boolean callAgain = true; switch (msg.what) { // TODO: should we avoid sending frame message when starting if we // were already running? case ANIMATION_START: if (sAnimations.size() > 0 || sDelayedAnims.size() > 0) { callAgain = false; } // pendingAnims holds any animations that have requested to be started // We're going to clear sPendingAnimations, but starting animation may // cause more to be added to the pending list (for example, if one animation // starting triggers another starting). So we loop until sPendingAnimations // is empty. while (sPendingAnimations.size() > 0) { ArrayListAnimator
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 Animator
* during a call to {@link AnimatorUpdateListener#onAnimationUpdate(Animator)}, which
* is called during each animation frame, immediately after the value is calculated.
*
* @return animatedValue The value most recently calculated by this Animator
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 Animator was set up wrong
return null;
}
/**
* The most recent value calculated by this Animator
for propertyName
.
* The main purpose for this read-only property is to retrieve the value from the
* Animator
during a call to
* {@link AnimatorUpdateListener#onAnimationUpdate(Animator)}, 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 Animator
.
*/
public Object getAnimatedValue(String propertyName) {
PropertyValuesHolder valuesHolder = mValuesMap.get(propertyName);
if (valuesHolder != null) {
return valuesHolder.getAnimatedValue();
} else {
// At least avoid crashing if called with bogus propertyName
return null;
}
}
/**
* Sets how many times the animation should be repeated. If the repeat
* count is 0, the animation is never repeated. If the repeat count is
* greater than 0 or {@link #INFINITE}, the repeat mode will be taken
* into account. The repeat count is 0 by default.
*
* @param value the number of times the animation should be repeated
*/
public void setRepeatCount(int value) {
mRepeatCount = value;
}
/**
* Defines how many times the animation should repeat. The default value
* is 0.
*
* @return the number of times the animation should repeat, or {@link #INFINITE}
*/
public int getRepeatCount() {
return mRepeatCount;
}
/**
* Defines what this animation should do when it reaches the end. This
* setting is applied only when the repeat count is either greater than
* 0 or {@link #INFINITE}. Defaults to {@link #RESTART}.
*
* @param value {@link #RESTART} or {@link #REVERSE}
*/
public void setRepeatMode(int value) {
mRepeatMode = value;
}
/**
* Defines what this animation should do when it reaches the end.
*
* @return either one of {@link #REVERSE} or {@link #RESTART}
*/
public int getRepeatMode() {
return mRepeatMode;
}
/**
* Adds a listener to the set of listeners that are sent update events through the life of
* an animation. This method is called on all listeners for every frame of the animation,
* after the values for the animation have been calculated.
*
* @param listener the listener to be added to the current set of listeners for this animation.
*/
public void addUpdateListener(AnimatorUpdateListener listener) {
if (mUpdateListeners == null) {
mUpdateListeners = new ArrayListstartValue
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 RGBEvaluator}
* should be used to get correct RGB color interpolation.
*
* If this Animator 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 Animator, 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/ * * @param playBackwards Whether the Animator should start playing in reverse. */ private void start(boolean playBackwards) { mPlayingBackwards = playBackwards; if ((mStartDelay == 0) && (Thread.currentThread() == Looper.getMainLooper().getThread())) { // This sets the initial value of the animation, prior to actually starting it running setCurrentPlayTime(getCurrentPlayTime()); } mPlayingState = STOPPED; sPendingAnimations.add(this); if (sAnimationHandler == null) { sAnimationHandler = new AnimationHandler(); } // TODO: does this put too many messages on the queue if the handler // is already running? sAnimationHandler.sendEmptyMessage(ANIMATION_START); } @Override public void start() { start(false); } @Override public void cancel() { if (mListeners != null) { ArrayListstartDelay
phase. The return value indicates whether it
* should be woken up and put on the active animations queue.
*
* @param currentTime The current animation time, used to calculate whether the animation
* has exceeded its startDelay
and should be started.
* @return True if the animation's startDelay
has been exceeded and the animation
* should be added to the set of active animations.
*/
private boolean delayedAnimationFrame(long currentTime) {
if (!mStartedDelay) {
mStartedDelay = true;
mDelayStartTime = currentTime;
} else {
long deltaTime = currentTime - mDelayStartTime;
if (deltaTime > mStartDelay) {
// startDelay ended - start the anim and record the
// mStartTime appropriately
mStartTime = currentTime - (deltaTime - mStartDelay);
mPlayingState = RUNNING;
return true;
}
}
return false;
}
/**
* This internal function processes a single animation frame for a given animation. The
* currentTime parameter is the timing pulse sent by the handler, used to calculate the
* elapsed duration, and therefore
* the elapsed fraction, of the animation. The return value indicates whether the animation
* should be ended (which happens when the elapsed time of the animation exceeds the
* animation's duration, including the repeatCount).
*
* @param currentTime The current time, as tracked by the static timing handler
* @return true if the animation's duration, including any repetitions due to
* repeatCount
has been exceeded and the animation should be ended.
*/
private boolean animationFrame(long currentTime) {
boolean done = false;
if (mPlayingState == STOPPED) {
mPlayingState = RUNNING;
if (mSeekTime < 0) {
mStartTime = currentTime;
} else {
mStartTime = currentTime - mSeekTime;
// Now that we're playing, reset the seek time
mSeekTime = -1;
}
}
switch (mPlayingState) {
case RUNNING:
case SEEKED:
float fraction = (float)(currentTime - mStartTime) / mDuration;
if (fraction >= 1f) {
if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) {
// Time to repeat
if (mListeners != null) {
for (AnimatableListener listener : mListeners) {
listener.onAnimationRepeat(this);
}
}
++mCurrentIteration;
if (mRepeatMode == REVERSE) {
mPlayingBackwards = mPlayingBackwards ? false : true;
}
// TODO: doesn't account for fraction going Wayyyyy over 1, like 2+
fraction = fraction - 1f;
mStartTime += mDuration;
} else {
done = true;
fraction = Math.min(fraction, 1.0f);
}
}
if (mPlayingBackwards) {
fraction = 1f - fraction;
}
animateValue(fraction);
break;
case ENDED:
// The final value set on the target varies, depending on whether the animation
// was supposed to repeat an odd number of times
if (mRepeatCount > 0 && (mRepeatCount & 0x01) == 1) {
animateValue(0f);
} else {
animateValue(1f);
}
// Fall through to set done flag
case CANCELED:
done = true;
break;
}
return done;
}
/**
* This method is called with the elapsed fraction of the animation during every
* animation frame. This function turns the elapsed fraction into an interpolated fraction
* and then into an animated value (from the evaluator. The function is called mostly during
* animation updates, but it is also called when the end()
* function is called, to set the final value on the property.
*
* Overrides of this method must call the superclass to perform the calculation * of the animated value.
* * @param fraction The elapsed fraction of the animation. */ void animateValue(float fraction) { fraction = mInterpolator.getInterpolation(fraction); int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { mValues[i].calculateValue(fraction); } if (mUpdateListeners != null) { int numListeners = mUpdateListeners.size(); for (int i = 0; i < numListeners; ++i) { mUpdateListeners.get(i).onAnimationUpdate(this); } } } @Override public Animator clone() throws CloneNotSupportedException { final Animator anim = (Animator) super.clone(); if (mUpdateListeners != null) { ArrayListAnimator
instance to receive callbacks on every animation
* frame, after the current frame's values have been calculated for that
* Animator
.
*/
public static interface AnimatorUpdateListener {
/**
* Notifies the occurrence of another frame of the animation.
* * @param animation The animation which was repeated. */ void onAnimationUpdate(Animator animation); } }