/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.nineoldandroids.view; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.Set; import android.view.View; import android.view.animation.Interpolator; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.ValueAnimator; import com.nineoldandroids.view.animation.AnimatorProxy; class ViewPropertyAnimatorPreHC extends ViewPropertyAnimator { /** * Proxy animation class which will allow us access to post-Honeycomb properties that were not * otherwise available. */ private final AnimatorProxy mProxy; /** * A WeakReference holding the View whose properties are being animated by this class. This is * set at construction time. */ private final WeakReference<View> mView; /** * The duration of the underlying Animator object. By default, we don't set the duration * on the Animator and just use its default duration. If the duration is ever set on this * Animator, then we use the duration that it was set to. */ private long mDuration; /** * A flag indicating whether the duration has been set on this object. If not, we don't set * the duration on the underlying Animator, but instead just use its default duration. */ private boolean mDurationSet = false; /** * The startDelay of the underlying Animator object. By default, we don't set the startDelay * on the Animator and just use its default startDelay. If the startDelay is ever set on this * Animator, then we use the startDelay that it was set to. */ private long mStartDelay = 0; /** * A flag indicating whether the startDelay has been set on this object. If not, we don't set * the startDelay on the underlying Animator, but instead just use its default startDelay. */ private boolean mStartDelaySet = false; /** * The interpolator of the underlying Animator object. By default, we don't set the interpolator * on the Animator and just use its default interpolator. If the interpolator is ever set on * this Animator, then we use the interpolator that it was set to. */ private /*Time*/Interpolator mInterpolator; /** * A flag indicating whether the interpolator has been set on this object. If not, we don't set * the interpolator on the underlying Animator, but instead just use its default interpolator. */ private boolean mInterpolatorSet = false; /** * Listener for the lifecycle events of the underlying */ private Animator.AnimatorListener mListener = null; /** * This listener is the mechanism by which the underlying Animator causes changes to the * properties currently being animated, as well as the cleanup after an animation is * complete. */ private AnimatorEventListener mAnimatorEventListener = new AnimatorEventListener(); /** * This list holds the properties that have been asked to animate. We allow the caller to * request several animations prior to actually starting the underlying animator. This * enables us to run one single animator to handle several properties in parallel. Each * property is tossed onto the pending list until the animation actually starts (which is * done by posting it onto mView), at which time the pending list is cleared and the properties * on that list are added to the list of properties associated with that animator. */ ArrayList<NameValuesHolder> mPendingAnimations = new ArrayList<NameValuesHolder>(); /** * Constants used to associate a property being requested and the mechanism used to set * the property (this class calls directly into View to set the properties in question). */ private static final int NONE = 0x0000; private static final int TRANSLATION_X = 0x0001; private static final int TRANSLATION_Y = 0x0002; private static final int SCALE_X = 0x0004; private static final int SCALE_Y = 0x0008; private static final int ROTATION = 0x0010; private static final int ROTATION_X = 0x0020; private static final int ROTATION_Y = 0x0040; private static final int X = 0x0080; private static final int Y = 0x0100; private static final int ALPHA = 0x0200; private static final int TRANSFORM_MASK = TRANSLATION_X | TRANSLATION_Y | SCALE_X | SCALE_Y | ROTATION | ROTATION_X | ROTATION_Y | X | Y; /** * The mechanism by which the user can request several properties that are then animated * together works by posting this Runnable to start the underlying Animator. Every time * a property animation is requested, we cancel any previous postings of the Runnable * and re-post it. This means that we will only ever run the Runnable (and thus start the * underlying animator) after the caller is done setting the properties that should be * animated together. */ private Runnable mAnimationStarter = new Runnable() { @Override public void run() { startAnimation(); } }; /** * This class holds information about the overall animation being run on the set of * properties. The mask describes which properties are being animated and the * values holder is the list of all property/value objects. */ private static class PropertyBundle { int mPropertyMask; ArrayList<NameValuesHolder> mNameValuesHolder; PropertyBundle(int propertyMask, ArrayList<NameValuesHolder> nameValuesHolder) { mPropertyMask = propertyMask; mNameValuesHolder = nameValuesHolder; } /** * Removes the given property from being animated as a part of this * PropertyBundle. If the property was a part of this bundle, it returns * true to indicate that it was, in fact, canceled. This is an indication * to the caller that a cancellation actually occurred. * * @param propertyConstant The property whose cancellation is requested. * @return true if the given property is a part of this bundle and if it * has therefore been canceled. */ boolean cancel(int propertyConstant) { if ((mPropertyMask & propertyConstant) != 0 && mNameValuesHolder != null) { int count = mNameValuesHolder.size(); for (int i = 0; i < count; ++i) { NameValuesHolder nameValuesHolder = mNameValuesHolder.get(i); if (nameValuesHolder.mNameConstant == propertyConstant) { mNameValuesHolder.remove(i); mPropertyMask &= ~propertyConstant; return true; } } } return false; } } /** * This list tracks the list of properties being animated by any particular animator. * In most situations, there would only ever be one animator running at a time. But it is * possible to request some properties to animate together, then while those properties * are animating, to request some other properties to animate together. The way that * works is by having this map associate the group of properties being animated with the * animator handling the animation. On every update event for an Animator, we ask the * map for the associated properties and set them accordingly. */ private HashMap<Animator, PropertyBundle> mAnimatorMap = new HashMap<Animator, PropertyBundle>(); /** * This is the information we need to set each property during the animation. * mNameConstant is used to set the appropriate field in View, and the from/delta * values are used to calculate the animated value for a given animation fraction * during the animation. */ private static class NameValuesHolder { int mNameConstant; float mFromValue; float mDeltaValue; NameValuesHolder(int nameConstant, float fromValue, float deltaValue) { mNameConstant = nameConstant; mFromValue = fromValue; mDeltaValue = deltaValue; } } /** * Constructor, called by View. This is private by design, as the user should only * get a ViewPropertyAnimator by calling View.animate(). * * @param view The View associated with this ViewPropertyAnimator */ ViewPropertyAnimatorPreHC(View view) { mView = new WeakReference<View>(view); mProxy = AnimatorProxy.wrap(view); } /** * Sets the duration for the underlying animator that animates the requested properties. * By default, the animator uses the default value for ValueAnimator. Calling this method * will cause the declared value to be used instead. * @param duration The length of ensuing property animations, in milliseconds. The value * cannot be negative. * @return This object, allowing calls to methods in this class to be chained. */ public ViewPropertyAnimator setDuration(long duration) { if (duration < 0) { throw new IllegalArgumentException("Animators cannot have negative duration: " + duration); } mDurationSet = true; mDuration = duration; return this; } /** * Returns the current duration of property animations. If the duration was set on this * object, that value is returned. Otherwise, the default value of the underlying Animator * is returned. * * @see #setDuration(long) * @return The duration of animations, in milliseconds. */ public long getDuration() { if (mDurationSet) { return mDuration; } else { // Just return the default from ValueAnimator, since that's what we'd get if // the value has not been set otherwise return new ValueAnimator().getDuration(); } } @Override public long getStartDelay() { if (mStartDelaySet) { return mStartDelay; } else { // Just return the default from ValueAnimator (0), since that's what we'd get if // the value has not been set otherwise return 0; } } @Override public ViewPropertyAnimator setStartDelay(long startDelay) { if (startDelay < 0) { throw new IllegalArgumentException("Animators cannot have negative duration: " + startDelay); } mStartDelaySet = true; mStartDelay = startDelay; return this; } @Override public ViewPropertyAnimator setInterpolator(/*Time*/Interpolator interpolator) { mInterpolatorSet = true; mInterpolator = interpolator; return this; } @Override public ViewPropertyAnimator setListener(Animator.AnimatorListener listener) { mListener = listener; return this; } @Override public void start() { startAnimation(); } @Override public void cancel() { if (mAnimatorMap.size() > 0) { HashMap<Animator, PropertyBundle> mAnimatorMapCopy = (HashMap<Animator, PropertyBundle>)mAnimatorMap.clone(); Set<Animator> animatorSet = mAnimatorMapCopy.keySet(); for (Animator runningAnim : animatorSet) { runningAnim.cancel(); } } mPendingAnimations.clear(); View v = mView.get(); if (v != null) { v.removeCallbacks(mAnimationStarter); } } @Override public ViewPropertyAnimator x(float value) { animateProperty(X, value); return this; } @Override public ViewPropertyAnimator xBy(float value) { animatePropertyBy(X, value); return this; } @Override public ViewPropertyAnimator y(float value) { animateProperty(Y, value); return this; } @Override public ViewPropertyAnimator yBy(float value) { animatePropertyBy(Y, value); return this; } @Override public ViewPropertyAnimator rotation(float value) { animateProperty(ROTATION, value); return this; } @Override public ViewPropertyAnimator rotationBy(float value) { animatePropertyBy(ROTATION, value); return this; } @Override public ViewPropertyAnimator rotationX(float value) { animateProperty(ROTATION_X, value); return this; } @Override public ViewPropertyAnimator rotationXBy(float value) { animatePropertyBy(ROTATION_X, value); return this; } @Override public ViewPropertyAnimator rotationY(float value) { animateProperty(ROTATION_Y, value); return this; } @Override public ViewPropertyAnimator rotationYBy(float value) { animatePropertyBy(ROTATION_Y, value); return this; } @Override public ViewPropertyAnimator translationX(float value) { animateProperty(TRANSLATION_X, value); return this; } @Override public ViewPropertyAnimator translationXBy(float value) { animatePropertyBy(TRANSLATION_X, value); return this; } @Override public ViewPropertyAnimator translationY(float value) { animateProperty(TRANSLATION_Y, value); return this; } @Override public ViewPropertyAnimator translationYBy(float value) { animatePropertyBy(TRANSLATION_Y, value); return this; } @Override public ViewPropertyAnimator scaleX(float value) { animateProperty(SCALE_X, value); return this; } @Override public ViewPropertyAnimator scaleXBy(float value) { animatePropertyBy(SCALE_X, value); return this; } @Override public ViewPropertyAnimator scaleY(float value) { animateProperty(SCALE_Y, value); return this; } @Override public ViewPropertyAnimator scaleYBy(float value) { animatePropertyBy(SCALE_Y, value); return this; } @Override public ViewPropertyAnimator alpha(float value) { animateProperty(ALPHA, value); return this; } @Override public ViewPropertyAnimator alphaBy(float value) { animatePropertyBy(ALPHA, value); return this; } /** * Starts the underlying Animator for a set of properties. We use a single animator that * simply runs from 0 to 1, and then use that fractional value to set each property * value accordingly. */ private void startAnimation() { ValueAnimator animator = ValueAnimator.ofFloat(1.0f); ArrayList<NameValuesHolder> nameValueList = (ArrayList<NameValuesHolder>) mPendingAnimations.clone(); mPendingAnimations.clear(); int propertyMask = 0; int propertyCount = nameValueList.size(); for (int i = 0; i < propertyCount; ++i) { NameValuesHolder nameValuesHolder = nameValueList.get(i); propertyMask |= nameValuesHolder.mNameConstant; } mAnimatorMap.put(animator, new PropertyBundle(propertyMask, nameValueList)); animator.addUpdateListener(mAnimatorEventListener); animator.addListener(mAnimatorEventListener); if (mStartDelaySet) { animator.setStartDelay(mStartDelay); } if (mDurationSet) { animator.setDuration(mDuration); } if (mInterpolatorSet) { animator.setInterpolator(mInterpolator); } animator.start(); } /** * Utility function, called by the various x(), y(), etc. methods. This stores the * constant name for the property along with the from/delta values that will be used to * calculate and set the property during the animation. This structure is added to the * pending animations, awaiting the eventual start() of the underlying animator. A * Runnable is posted to start the animation, and any pending such Runnable is canceled * (which enables us to end up starting just one animator for all of the properties * specified at one time). * * @param constantName The specifier for the property being animated * @param toValue The value to which the property will animate */ private void animateProperty(int constantName, float toValue) { float fromValue = getValue(constantName); float deltaValue = toValue - fromValue; animatePropertyBy(constantName, fromValue, deltaValue); } /** * Utility function, called by the various xBy(), yBy(), etc. methods. This method is * just like animateProperty(), except the value is an offset from the property's * current value, instead of an absolute "to" value. * * @param constantName The specifier for the property being animated * @param byValue The amount by which the property will change */ private void animatePropertyBy(int constantName, float byValue) { float fromValue = getValue(constantName); animatePropertyBy(constantName, fromValue, byValue); } /** * Utility function, called by animateProperty() and animatePropertyBy(), which handles the * details of adding a pending animation and posting the request to start the animation. * * @param constantName The specifier for the property being animated * @param startValue The starting value of the property * @param byValue The amount by which the property will change */ private void animatePropertyBy(int constantName, float startValue, float byValue) { // First, cancel any existing animations on this property if (mAnimatorMap.size() > 0) { Animator animatorToCancel = null; Set<Animator> animatorSet = mAnimatorMap.keySet(); for (Animator runningAnim : animatorSet) { PropertyBundle bundle = mAnimatorMap.get(runningAnim); if (bundle.cancel(constantName)) { // property was canceled - cancel the animation if it's now empty // Note that it's safe to break out here because every new animation // on a property will cancel a previous animation on that property, so // there can only ever be one such animation running. if (bundle.mPropertyMask == NONE) { // the animation is no longer changing anything - cancel it animatorToCancel = runningAnim; break; } } } if (animatorToCancel != null) { animatorToCancel.cancel(); } } NameValuesHolder nameValuePair = new NameValuesHolder(constantName, startValue, byValue); mPendingAnimations.add(nameValuePair); View v = mView.get(); if (v != null) { v.removeCallbacks(mAnimationStarter); v.post(mAnimationStarter); } } /** * This method handles setting the property values directly in the View object's fields. * propertyConstant tells it which property should be set, value is the value to set * the property to. * * @param propertyConstant The property to be set * @param value The value to set the property to */ private void setValue(int propertyConstant, float value) { //final View.TransformationInfo info = mView.mTransformationInfo; switch (propertyConstant) { case TRANSLATION_X: //info.mTranslationX = value; mProxy.setTranslationX(value); break; case TRANSLATION_Y: //info.mTranslationY = value; mProxy.setTranslationY(value); break; case ROTATION: //info.mRotation = value; mProxy.setRotation(value); break; case ROTATION_X: //info.mRotationX = value; mProxy.setRotationX(value); break; case ROTATION_Y: //info.mRotationY = value; mProxy.setRotationY(value); break; case SCALE_X: //info.mScaleX = value; mProxy.setScaleX(value); break; case SCALE_Y: //info.mScaleY = value; mProxy.setScaleY(value); break; case X: //info.mTranslationX = value - mView.mLeft; mProxy.setX(value); break; case Y: //info.mTranslationY = value - mView.mTop; mProxy.setY(value); break; case ALPHA: //info.mAlpha = value; mProxy.setAlpha(value); break; } } /** * This method gets the value of the named property from the View object. * * @param propertyConstant The property whose value should be returned * @return float The value of the named property */ private float getValue(int propertyConstant) { //final View.TransformationInfo info = mView.mTransformationInfo; switch (propertyConstant) { case TRANSLATION_X: //return info.mTranslationX; return mProxy.getTranslationX(); case TRANSLATION_Y: //return info.mTranslationY; return mProxy.getTranslationY(); case ROTATION: //return info.mRotation; return mProxy.getRotation(); case ROTATION_X: //return info.mRotationX; return mProxy.getRotationX(); case ROTATION_Y: //return info.mRotationY; return mProxy.getRotationY(); case SCALE_X: //return info.mScaleX; return mProxy.getScaleX(); case SCALE_Y: //return info.mScaleY; return mProxy.getScaleY(); case X: //return mView.mLeft + info.mTranslationX; return mProxy.getX(); case Y: //return mView.mTop + info.mTranslationY; return mProxy.getY(); case ALPHA: //return info.mAlpha; return mProxy.getAlpha(); } return 0; } /** * Utility class that handles the various Animator events. The only ones we care * about are the end event (which we use to clean up the animator map when an animator * finishes) and the update event (which we use to calculate the current value of each * property and then set it on the view object). */ private class AnimatorEventListener implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { @Override public void onAnimationStart(Animator animation) { if (mListener != null) { mListener.onAnimationStart(animation); } } @Override public void onAnimationCancel(Animator animation) { if (mListener != null) { mListener.onAnimationCancel(animation); } } @Override public void onAnimationRepeat(Animator animation) { if (mListener != null) { mListener.onAnimationRepeat(animation); } } @Override public void onAnimationEnd(Animator animation) { if (mListener != null) { mListener.onAnimationEnd(animation); } mAnimatorMap.remove(animation); // If the map is empty, it means all animation are done or canceled, so the listener // isn't needed anymore. Not nulling it would cause it to leak any objects used in // its implementation if (mAnimatorMap.isEmpty()) { mListener = null; } } /** * Calculate the current value for each property and set it on the view. Invalidate * the view object appropriately, depending on which properties are being animated. * * @param animation The animator associated with the properties that need to be * set. This animator holds the animation fraction which we will use to calculate * the current value of each property. */ @Override public void onAnimationUpdate(ValueAnimator animation) { // alpha requires slightly different treatment than the other (transform) properties. // The logic in setAlpha() is not simply setting mAlpha, plus the invalidation // logic is dependent on how the view handles an internal call to onSetAlpha(). // We track what kinds of properties are set, and how alpha is handled when it is // set, and perform the invalidation steps appropriately. //boolean alphaHandled = false; //mView.invalidateParentCaches(); float fraction = animation.getAnimatedFraction(); PropertyBundle propertyBundle = mAnimatorMap.get(animation); int propertyMask = propertyBundle.mPropertyMask; if ((propertyMask & TRANSFORM_MASK) != 0) { View v = mView.get(); if (v != null) { v.invalidate(/*false*/); } } ArrayList<NameValuesHolder> valueList = propertyBundle.mNameValuesHolder; if (valueList != null) { int count = valueList.size(); for (int i = 0; i < count; ++i) { NameValuesHolder values = valueList.get(i); float value = values.mFromValue + fraction * values.mDeltaValue; //if (values.mNameConstant == ALPHA) { // alphaHandled = mView.setAlphaNoInvalidation(value); //} else { setValue(values.mNameConstant, value); //} } } /*if ((propertyMask & TRANSFORM_MASK) != 0) { mView.mTransformationInfo.mMatrixDirty = true; mView.mPrivateFlags |= View.DRAWN; // force another invalidation }*/ // invalidate(false) in all cases except if alphaHandled gets set to true // via the call to setAlphaNoInvalidation(), above View v = mView.get(); if (v != null) { v.invalidate(/*alphaHandled*/); } } } }