diff --git a/app/src/main/java/com/hippo/widget/FloatLabelEditText.java b/app/src/main/java/com/hippo/widget/FloatLabelEditText.java new file mode 100644 index 000000000..79962d63f --- /dev/null +++ b/app/src/main/java/com/hippo/widget/FloatLabelEditText.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2015 Hippo Seven + * + * 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 + * 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.hippo.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.hippo.ehviewer.R; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.animation.ObjectAnimator; + +// Get code from https://gist.github.com/jromero/233cfd8ef22a8b8c29ea +/** + * Layout which an {@link android.widget.EditText} to show a floating label when the hint is hidden + * due to the user inputting text. + * + * @see Matt D. Smith on Dribble + * @see Brad Frost's blog post + */ +public class FloatLabelEditText extends FrameLayout { + + private static final long ANIMATION_DURATION = 150; + + private static final Trigger[] sTriggerArray = { + Trigger.TEXT, + Trigger.FOCUS + }; + + private TextView mLabel; + private EditText mEditText; + + private Trigger mTrigger = Trigger.TEXT; + + private String mHint; + + public FloatLabelEditText(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public FloatLabelEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + mLabel = new TextView(context); + mLabel.setVisibility(View.INVISIBLE); + + final TypedArray a = context + .obtainStyledAttributes(attrs, R.styleable.FloatLabelEditText); + mLabel.setTextAppearance(context, + a.getResourceId(R.styleable.FloatLabelEditText_fllLabelAppearance, + android.R.style.TextAppearance_Small)); + mHint = a.getString(R.styleable.FloatLabelEditText_fllHint); + final int index = a.getInt(R.styleable.FloatLabelEditText_fllTrigger, -1); + if (index >= 0) { + setTrigger(sTriggerArray[index]); + } + a.recycle(); + addView(mLabel, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + + mEditText = (EditText) LayoutInflater.from(getContext()).inflate( + R.layout.widget_float_label_edit_text, this, false); + final LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.BOTTOM; + lp.topMargin = (int) mLabel.getTextSize(); + addView(mEditText, lp); + + mEditText.setHint(mHint); + mLabel.setText(mHint); + + // Add a TextWatcher so that we know when the text input has changed + mEditText.addTextChangedListener(mTextWatcher); + + // Add focus listener to the EditText so that we can notify the label that it is activated. + // Allows the use of a ColorStateList for the text color on the label + mEditText.setOnFocusChangeListener(mOnFocusChangeListener); + + // Align with label and edittext + mLabel.setPadding(mEditText.getPaddingLeft(), 0, 0, 0); + } + + public void setTrigger(Trigger trigger) { + mTrigger = trigger; + } + + /** + * Show the label using an animation + */ + protected void showLabel() { + if (mLabel.getVisibility() != View.VISIBLE) { + mLabel.setVisibility(View.VISIBLE); + + final ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mLabel, "alpha", + 0f, 1f); + alphaAnim.setAutoCancel(true); + alphaAnim.setDuration(ANIMATION_DURATION); + + final ObjectAnimator translationYAnim = ObjectAnimator.ofFloat(mLabel, + "translationY", mLabel.getHeight(), 0f); + translationYAnim.setAutoCancel(true); + translationYAnim.setDuration(ANIMATION_DURATION); + + alphaAnim.start(); + translationYAnim.start(); + } + } + + /** + * Hide the label using an animation + */ + protected void hideLabel() { + if (mLabel.getVisibility() != View.GONE) { + final ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mLabel, "alpha", + 1f, 0f); + alphaAnim.setAutoCancel(true); + alphaAnim.setDuration(ANIMATION_DURATION); + + final ObjectAnimator translationYAnim = ObjectAnimator.ofFloat(mLabel, + "translationY", 0f, mLabel.getHeight()); + translationYAnim.setAutoCancel(true); + translationYAnim.setDuration(ANIMATION_DURATION); + translationYAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLabel.setVisibility(View.GONE); + } + }); + + alphaAnim.start(); + translationYAnim.start(); + } + } + + private TextWatcher mTextWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(Editable s) { + // Only takes affect if mTrigger is set to TEXT + if (mTrigger != Trigger.TEXT) { + return; + } + + if (TextUtils.isEmpty(s)) { + hideLabel(); + } else { + showLabel(); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Empty + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Empty + } + }; + + private OnFocusChangeListener mOnFocusChangeListener = new OnFocusChangeListener() { + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void onFocusChange(View view, boolean focused) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mLabel.setActivated(focused); + } + + if (mTrigger != Trigger.FOCUS) { + return; + } + + if (focused) { + mEditText.setHint(""); + showLabel(); + } else { + mEditText.setHint(mHint); + hideLabel(); + } + } + }; + + /** + * Options for trigger to show a floating label + */ + public enum Trigger { + /** + * Active the trigger when input text + */ + TEXT (0), + + /** + * Active the trigger when get focus + */ + FOCUS (1); + + Trigger(int ni) { + nativeInt = ni; + } + final int nativeInt; + } +} diff --git a/app/src/main/java/com/nineoldandroids/animation/Animator.java b/app/src/main/java/com/nineoldandroids/animation/Animator.java new file mode 100644 index 000000000..883dfe95f --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/Animator.java @@ -0,0 +1,473 @@ +/* + * 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 com.nineoldandroids.animation; + +import android.view.animation.Interpolator; + +import java.util.ArrayList; + +/** + * 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 { + + /** + * The set of listeners to be sent events through the life of an animation. + */ + ArrayList mListeners = null; + + /** + * 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; + + /** + * 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() { + } + + /** + * Cancels the animation. Unlike {@link #end()}, cancel() causes the animation to + * stop in its tracks, sending an + * {@link com.nineoldandroids.animation.Animator.AnimatorListener#onAnimationCancel(Animator)} to + * its listeners, followed by an + * {@link com.nineoldandroids.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 com.nineoldandroids.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() { + if (isStarted() && !mPaused) { + mPaused = true; + if (mPauseListeners != null) { + ArrayList tmpListeners = + (ArrayList) mPauseListeners.clone(); + for (AnimatorPauseListener tmpListener : tmpListeners) { + tmpListener.onAnimationPause(this); + } + } + } + } + + /** + * Resumes a paused animation, causing the animator to pick up where it left off + * when it was paused. This method should only be called on the same thread on + * which the animation was started. Calls to resume() on an animator that is + * not currently paused will be ignored. + * + * @see #pause() + * @see #isPaused() + * @see AnimatorPauseListener + */ + public void resume() { + if (mPaused) { + mPaused = false; + if (mPauseListeners != null) { + ArrayList tmpListeners = + (ArrayList) mPauseListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationResume(this); + } + } + } + } + + /** + * Returns whether this animator is currently in a paused state. + * + * @return True if the animator is currently paused, false otherwise. + * + * @see #pause() + * @see #resume() + */ + public boolean isPaused() { + return mPaused; + } + + /** + * The amount of time, in milliseconds, to delay processing the animation + * after {@link #start()} is called. + * + * @return the number of milliseconds to delay running the animation + */ + public abstract long getStartDelay(); + + /** + * The amount of time, in milliseconds, to delay processing the animation + * after {@link #start()} is called. + + * @param startDelay The amount of the delay, in milliseconds + */ + public abstract void setStartDelay(long startDelay); + + /** + * Sets the duration of the animation. + * + * @param duration The length of the animation, in milliseconds. + */ + public abstract Animator setDuration(long duration); + + /** + * Gets the duration of the animation. + * + * @return The length of the animation, in milliseconds. + */ + public abstract long getDuration(); + + /** + * 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(Interpolator value); + + /** + * Returns the timing interpolator that this animation uses. + * + * @return The timing interpolator for this animation. + */ + public Interpolator 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. This state is a superset + * of the state 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. + * + * @return Whether the Animator has been started and not yet ended. + */ + public boolean isStarted() { + // Default method returns value for isRunning(). Subclasses should override to return a + // real value. + return isRunning(); + } + + /** + * Adds a listener to the set of listeners that are sent events through the life of an + * animation, such as start, repeat, and end. + * + * @param listener the listener to be added to the current set of listeners for this animation. + */ + public void addListener(AnimatorListener listener) { + if (mListeners == null) { + mListeners = new ArrayList(); + } + mListeners.add(listener); + } + + /** + * Removes a listener from the set listening to this animation. + * + * @param listener the listener to be removed from the current set of listeners for this + * animation. + */ + public void removeListener(AnimatorListener listener) { + if (mListeners == null) { + return; + } + mListeners.remove(listener); + if (mListeners.size() == 0) { + mListeners = null; + } + } + + /** + * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently + * listening for events on this Animator object. + * + * @return ArrayList The set of listeners. + */ + public ArrayList getListeners() { + return mListeners; + } + + /** + * Adds a pause listener to this animator. + * + * @param listener the listener to be added to the current set of pause listeners + * for this animation. + */ + public void addPauseListener(AnimatorPauseListener listener) { + if (mPauseListeners == null) { + mPauseListeners = new ArrayList(); + } + mPauseListeners.add(listener); + } + + /** + * Removes a pause listener from the set listening to this animation. + * + * @param listener the listener to be removed from the current set of pause + * listeners for this animation. + */ + public void removePauseListener(AnimatorPauseListener listener) { + if (mPauseListeners == null) { + return; + } + mPauseListeners.remove(listener); + if (mPauseListeners.size() == 0) { + mPauseListeners = null; + } + } + + /** + * Removes all {@link #addListener(com.nineoldandroids.animation.Animator.AnimatorListener)} listeners} + * and {@link #addPauseListener(com.nineoldandroids.animation.Animator.AnimatorPauseListener)} + * pauseListeners} from this object. + */ + public void removeAllListeners() { + if (mListeners != null) { + mListeners.clear(); + mListeners = null; + } + if (mPauseListeners != null) { + mPauseListeners.clear(); + mPauseListeners = null; + } + } + + @Override + public Animator clone() { + try { + final Animator anim = (Animator) super.clone(); + if (mListeners != null) { + ArrayList oldListeners = mListeners; + anim.mListeners = new ArrayList(); + int numListeners = oldListeners.size(); + for (int i = 0; i < numListeners; ++i) { + anim.mListeners.add(oldListeners.get(i)); + } + } + if (mPauseListeners != null) { + ArrayList oldListeners = mPauseListeners; + anim.mPauseListeners = new ArrayList(); + int numListeners = oldListeners.size(); + for (int i = 0; i < numListeners; ++i) { + anim.mPauseListeners.add(oldListeners.get(i)); + } + } + 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 android.animation.ValueAnimator}, but this method + * is on the superclass for the convenience of dealing generically with those subclasses + * that do handle targets. + * + * @param target The object being animated + */ + public void setTarget(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"); + } + + /** + *

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.

+ * + * @param animation The started animation. + */ + void onAnimationStart(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(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(Animator animation); + + /** + *

Notifies the repetition of the animation.

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

Notifies that the animation was paused.

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

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

+ * + * @param animation The animation being resumed. + * @see #resume() + */ + void onAnimationResume(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. + } +} diff --git a/app/src/main/java/com/nineoldandroids/animation/AnimatorInflater.java b/app/src/main/java/com/nineoldandroids/animation/AnimatorInflater.java new file mode 100644 index 000000000..82a98534a --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/AnimatorInflater.java @@ -0,0 +1,344 @@ +/* + * 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 com.nineoldandroids.animation; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.content.res.Resources.NotFoundException; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.util.Xml; +import android.view.animation.AnimationUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * This class is used to instantiate animator XML files into Animator objects. + *

+ * For performance reasons, inflation relies heavily on pre-processing of + * XML files that is done at build time. Therefore, it is not currently possible + * to use this inflater with an XmlPullParser over a plain XML file at runtime; + * it only works with an XmlPullParser returned from a compiled resource (R. + * something file.) + */ +public class AnimatorInflater { + private static final int[] AnimatorSet = new int[] { + /* 0 */ android.R.attr.ordering, + }; + private static final int AnimatorSet_ordering = 0; + + private static final int[] PropertyAnimator = new int[] { + /* 0 */ android.R.attr.propertyName, + }; + private static final int PropertyAnimator_propertyName = 0; + + private static final int[] Animator = new int[] { + /* 0 */ android.R.attr.interpolator, + /* 1 */ android.R.attr.duration, + /* 2 */ android.R.attr.startOffset, + /* 3 */ android.R.attr.repeatCount, + /* 4 */ android.R.attr.repeatMode, + /* 5 */ android.R.attr.valueFrom, + /* 6 */ android.R.attr.valueTo, + /* 7 */ android.R.attr.valueType, + }; + private static final int Animator_interpolator = 0; + private static final int Animator_duration = 1; + private static final int Animator_startOffset = 2; + private static final int Animator_repeatCount = 3; + private static final int Animator_repeatMode = 4; + private static final int Animator_valueFrom = 5; + private static final int Animator_valueTo = 6; + private static final int Animator_valueType = 7; + + /** + * These flags are used when parsing AnimatorSet objects + */ + private static final int TOGETHER = 0; + //private static final int SEQUENTIALLY = 1; + + /** + * Enum values used in XML attributes to indicate the value for mValueType + */ + private static final int VALUE_TYPE_FLOAT = 0; + //private static final int VALUE_TYPE_INT = 1; + //private static final int VALUE_TYPE_COLOR = 4; + //private static final int VALUE_TYPE_CUSTOM = 5; + + /** + * Loads an {@link com.nineoldandroids.animation.Animator} object from a resource + * + * @param context Application context used to access resources + * @param id The resource id of the animation to load + * @return The animator object reference by the specified id + * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded + */ + public static Animator loadAnimator(Context context, int id) + throws NotFoundException { + + XmlResourceParser parser = null; + try { + parser = context.getResources().getAnimation(id); + return createAnimatorFromXml(context, parser); + } catch (XmlPullParserException ex) { + NotFoundException rnf = + new NotFoundException("Can't load animation resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(ex); + throw rnf; + } catch (IOException ex) { + NotFoundException rnf = + new NotFoundException("Can't load animation resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(ex); + throw rnf; + } finally { + if (parser != null) parser.close(); + } + } + + private static Animator createAnimatorFromXml(Context c, XmlPullParser parser) + throws XmlPullParserException, IOException { + + return createAnimatorFromXml(c, parser, Xml.asAttributeSet(parser), null, 0); + } + + private static Animator createAnimatorFromXml(Context c, XmlPullParser parser, + AttributeSet attrs, AnimatorSet parent, int sequenceOrdering) + throws XmlPullParserException, IOException { + + Animator anim = null; + ArrayList childAnims = null; + + // Make sure we are on a start tag. + int type; + int depth = parser.getDepth(); + + while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) + && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + + if (name.equals("objectAnimator")) { + anim = loadObjectAnimator(c, attrs); + } else if (name.equals("animator")) { + anim = loadAnimator(c, attrs, null); + } else if (name.equals("set")) { + anim = new AnimatorSet(); + TypedArray a = c.obtainStyledAttributes(attrs, + /*com.android.internal.R.styleable.*/AnimatorSet); + + TypedValue orderingValue = new TypedValue(); + a.getValue(/*com.android.internal.R.styleable.*/AnimatorSet_ordering, orderingValue); + int ordering = orderingValue.type == TypedValue.TYPE_INT_DEC ? orderingValue.data : TOGETHER; + + createAnimatorFromXml(c, parser, attrs, (AnimatorSet) anim, ordering); + a.recycle(); + } else { + throw new RuntimeException("Unknown animator name: " + parser.getName()); + } + + if (parent != null) { + if (childAnims == null) { + childAnims = new ArrayList(); + } + childAnims.add(anim); + } + } + if (parent != null && childAnims != null) { + Animator[] animsArray = new Animator[childAnims.size()]; + int index = 0; + for (Animator a : childAnims) { + animsArray[index++] = a; + } + if (sequenceOrdering == TOGETHER) { + parent.playTogether(animsArray); + } else { + parent.playSequentially(animsArray); + } + } + + return anim; + + } + + private static ObjectAnimator loadObjectAnimator(Context context, AttributeSet attrs) + throws NotFoundException { + + ObjectAnimator anim = new ObjectAnimator(); + + loadAnimator(context, attrs, anim); + + TypedArray a = + context.obtainStyledAttributes(attrs, /*com.android.internal.R.styleable.*/PropertyAnimator); + + String propertyName = a.getString(/*com.android.internal.R.styleable.*/PropertyAnimator_propertyName); + + anim.setPropertyName(propertyName); + + a.recycle(); + + return anim; + } + + /** + * Creates a new animation whose parameters come from the specified context and + * attributes set. + * + * @param context the application environment + * @param attrs the set of attributes holding the animation parameters + */ + private static ValueAnimator loadAnimator(Context context, AttributeSet attrs, ValueAnimator anim) + throws NotFoundException { + + TypedArray a = + context.obtainStyledAttributes(attrs, /*com.android.internal.R.styleable.*/Animator); + + long duration = a.getInt(/*com.android.internal.R.styleable.*/Animator_duration, 0); + + long startDelay = a.getInt(/*com.android.internal.R.styleable.*/Animator_startOffset, 0); + + int valueType = a.getInt(/*com.android.internal.R.styleable.*/Animator_valueType, + VALUE_TYPE_FLOAT); + + if (anim == null) { + anim = new ValueAnimator(); + } + //TypeEvaluator evaluator = null; + + int valueFromIndex = /*com.android.internal.R.styleable.*/Animator_valueFrom; + int valueToIndex = /*com.android.internal.R.styleable.*/Animator_valueTo; + + boolean getFloats = (valueType == VALUE_TYPE_FLOAT); + + TypedValue tvFrom = a.peekValue(valueFromIndex); + boolean hasFrom = (tvFrom != null); + int fromType = hasFrom ? tvFrom.type : 0; + TypedValue tvTo = a.peekValue(valueToIndex); + boolean hasTo = (tvTo != null); + int toType = hasTo ? tvTo.type : 0; + + if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) || + (hasTo && (toType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (toType <= TypedValue.TYPE_LAST_COLOR_INT))) { + // special case for colors: ignore valueType and get ints + getFloats = false; + anim.setEvaluator(new ArgbEvaluator()); + } + + if (getFloats) { + float valueFrom; + float valueTo; + if (hasFrom) { + if (fromType == TypedValue.TYPE_DIMENSION) { + valueFrom = a.getDimension(valueFromIndex, 0f); + } else { + valueFrom = a.getFloat(valueFromIndex, 0f); + } + if (hasTo) { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = a.getDimension(valueToIndex, 0f); + } else { + valueTo = a.getFloat(valueToIndex, 0f); + } + anim.setFloatValues(valueFrom, valueTo); + } else { + anim.setFloatValues(valueFrom); + } + } else { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = a.getDimension(valueToIndex, 0f); + } else { + valueTo = a.getFloat(valueToIndex, 0f); + } + anim.setFloatValues(valueTo); + } + } else { + int valueFrom; + int valueTo; + if (hasFrom) { + if (fromType == TypedValue.TYPE_DIMENSION) { + valueFrom = (int) a.getDimension(valueFromIndex, 0f); + } else if ((fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueFrom = a.getColor(valueFromIndex, 0); + } else { + valueFrom = a.getInt(valueFromIndex, 0); + } + if (hasTo) { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = (int) a.getDimension(valueToIndex, 0f); + } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueTo = a.getColor(valueToIndex, 0); + } else { + valueTo = a.getInt(valueToIndex, 0); + } + anim.setIntValues(valueFrom, valueTo); + } else { + anim.setIntValues(valueFrom); + } + } else { + if (hasTo) { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = (int) a.getDimension(valueToIndex, 0f); + } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueTo = a.getColor(valueToIndex, 0); + } else { + valueTo = a.getInt(valueToIndex, 0); + } + anim.setIntValues(valueTo); + } + } + } + + anim.setDuration(duration); + anim.setStartDelay(startDelay); + + if (a.hasValue(/*com.android.internal.R.styleable.*/Animator_repeatCount)) { + anim.setRepeatCount( + a.getInt(/*com.android.internal.R.styleable.*/Animator_repeatCount, 0)); + } + if (a.hasValue(/*com.android.internal.R.styleable.*/Animator_repeatMode)) { + anim.setRepeatMode( + a.getInt(/*com.android.internal.R.styleable.*/Animator_repeatMode, + ValueAnimator.RESTART)); + } + //if (evaluator != null) { + // anim.setEvaluator(evaluator); + //} + + final int resID = + a.getResourceId(/*com.android.internal.R.styleable.*/Animator_interpolator, 0); + if (resID > 0) { + anim.setInterpolator(AnimationUtils.loadInterpolator(context, resID)); + } + a.recycle(); + + return anim; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nineoldandroids/animation/AnimatorListenerAdapter.java b/app/src/main/java/com/nineoldandroids/animation/AnimatorListenerAdapter.java new file mode 100644 index 000000000..fa5cef3b9 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/AnimatorListenerAdapter.java @@ -0,0 +1,54 @@ +/* + * 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 com.nineoldandroids.animation; + +/** + * This adapter class provides empty implementations of the methods from {@link android.animation.Animator.AnimatorListener}. + * Any custom listener that cares only about a subset of the methods of this listener can + * simply subclass this adapter class instead of implementing the interface directly. + */ +public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener { + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationCancel(Animator animation) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationEnd(Animator animation) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationRepeat(Animator animation) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationStart(Animator animation) { + } + +} diff --git a/app/src/main/java/com/nineoldandroids/animation/AnimatorSet.java b/app/src/main/java/com/nineoldandroids/animation/AnimatorSet.java new file mode 100644 index 000000000..b12f2d535 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/AnimatorSet.java @@ -0,0 +1,1114 @@ +/* + * 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 com.nineoldandroids.animation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +import android.animation.TimeInterpolator; +import android.view.animation.Interpolator; + +/** + * This class plays a set of {@link com.nineoldandroids.animation.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 com.nineoldandroids.animation.AnimatorSet#playTogether(com.nineoldandroids.animation.Animator[]) playTogether()} or + * {@link com.nineoldandroids.animation.AnimatorSet#playSequentially(com.nineoldandroids.animation.Animator[]) playSequentially()} methods can be called to add + * a set of animations all at once, or the {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} can be + * used in conjunction with methods in the {@link com.nineoldandroids.animation.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 { + + /** + * 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 HashMap mNodeMap = new HashMap(); + + /** + * 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(); + + /** + * The sorted list of nodes. This is the order in which the animations will + * be played. The details about when exactly they will be played depend + * on the dependency relationships of the nodes. + */ + private ArrayList mSortedNodes = new ArrayList(); + + /** + * Flag indicating whether the nodes should be sorted prior to playing. This + * flag allows us to cache the previous sorted nodes so that if the sequence + * is replayed with no changes, it does not have to re-sort the nodes again. + */ + private boolean mNeedsSort = true; + + private AnimatorSetListener mSetListener = null; + + /** + * Flag indicating that the AnimatorSet has been manually + * terminated (by calling cancel() or end()). + * This flag is used to avoid starting other animations when currently-playing + * child animations of this AnimatorSet end. It also determines whether cancel/end + * notifications are sent out via the normal AnimatorSetListener mechanism. + */ + boolean mTerminated = 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 = null; + + + // 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; + + + /** + * 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(Animator... items) { + if (items != null) { + mNeedsSort = true; + Builder builder = play(items[0]); + for (int i = 1; i < items.length; ++i) { + builder.with(items[i]); + } + } + } + + /** + * Sets up this AnimatorSet to play all of the supplied animations at the same time. + * + * @param items The animations that will be started simultaneously. + */ + public void playTogether(Collection items) { + if (items != null && items.size() > 0) { + mNeedsSort = true; + Builder builder = null; + for (Animator anim : items) { + if (builder == null) { + builder = play(anim); + } else { + builder.with(anim); + } + } + } + } + + /** + * Sets up this AnimatorSet to play each of the supplied animations when the + * previous animation ends. + * + * @param items The animations that will be started one after another. + */ + public void playSequentially(Animator... items) { + if (items != null) { + mNeedsSort = true; + if (items.length == 1) { + play(items[0]); + } else { + for (int i = 0; i < items.length - 1; ++i) { + play(items[i]).before(items[i+1]); + } + } + } + } + + /** + * Sets up this AnimatorSet to play each of the supplied animations when the + * previous animation ends. + * + * @param items The animations that will be started one after another. + */ + public void playSequentially(List items) { + if (items != null && items.size() > 0) { + mNeedsSort = true; + 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(); + for (Node node : mNodes) { + childList.add(node.animation); + } + 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) { + for (Node node : mNodes) { + Animator animation = node.animation; + if (animation instanceof AnimatorSet) { + ((AnimatorSet)animation).setTarget(target); + } else if (animation instanceof ObjectAnimator) { + ((ObjectAnimator)animation).setTarget(target); + } + } + } + + /** + * Sets the TimeInterpolator for all current {@link #getChildAnimations() child animations} + * of this AnimatorSet. + * + * @param interpolator the interpolator to be used by each child animation of this AnimatorSet + */ + @Override + public void setInterpolator(Interpolator interpolator) { + for (Node node : mNodes) { + node.animation.setInterpolator(interpolator); + } + } + + /** + * 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() { + mTerminated = true; + if (isStarted()) { + ArrayList tmpListeners = null; + if (mListeners != null) { + tmpListeners = (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationCancel(this); + } + } + if (mDelayAnim != null && mDelayAnim.isRunning()) { + // If we're currently in the startDelay period, just cancel that animator and + // send out the end event to all listeners + mDelayAnim.cancel(); + } else if (mSortedNodes.size() > 0) { + for (Node node : mSortedNodes) { + node.animation.cancel(); + } + } + if (tmpListeners != null) { + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationEnd(this); + } + } + mStarted = false; + } + } + + /** + * {@inheritDoc} + * + *

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

+ */ + @Override + public void end() { + mTerminated = true; + if (isStarted()) { + if (mSortedNodes.size() != mNodes.size()) { + // hasn't been started yet - sort the nodes now, then end them + sortNodes(); + for (Node node : mSortedNodes) { + if (mSetListener == null) { + mSetListener = new AnimatorSetListener(this); + } + node.animation.addListener(mSetListener); + } + } + if (mDelayAnim != null) { + mDelayAnim.cancel(); + } + if (mSortedNodes.size() > 0) { + for (Node node : mSortedNodes) { + node.animation.end(); + } + } + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationEnd(this); + } + } + mStarted = false; + } + } + + /** + * Returns true if any of the child animations of this AnimatorSet have been started and have + * not yet ended. + * @return Whether this AnimatorSet has been started and has not yet ended. + */ + @Override + public boolean isRunning() { + for (Node node : mNodes) { + if (node.animation.isRunning()) { + return true; + } + } + return false; + } + + @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. + + * @param startDelay The amount of the delay, in milliseconds + */ + @Override + public void setStartDelay(long startDelay) { + mStartDelay = startDelay; + } + + /** + * 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"); + } + for (Node node : mNodes) { + // TODO: don't set the duration of the timing-only nodes created by AnimatorSet to + // insert "play-after" delays + node.animation.setDuration(duration); + } + mDuration = duration; + return this; + } + + @Override + public void setupStartValues() { + for (Node node : mNodes) { + node.animation.setupStartValues(); + } + } + + @Override + public void setupEndValues() { + for (Node node : mNodes) { + node.animation.setupEndValues(); + } + } + + /** + * {@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. + */ + @SuppressWarnings("unchecked") + @Override + public void start() { + mTerminated = false; + mStarted = true; + + // First, sort the nodes (if necessary). This will ensure that sortedNodes + // contains the animation nodes in the correct order. + sortNodes(); + + int numSortedNodes = mSortedNodes.size(); + for (int i = 0; i < numSortedNodes; ++i) { + Node node = mSortedNodes.get(i); + // First, clear out the old listeners + ArrayList oldListeners = node.animation.getListeners(); + if (oldListeners != null && oldListeners.size() > 0) { + final ArrayList clonedListeners = new + ArrayList(oldListeners); + + for (AnimatorListener listener : clonedListeners) { + if (listener instanceof DependencyListener || + listener instanceof AnimatorSetListener) { + node.animation.removeListener(listener); + } + } + } + } + + // nodesToStart holds the list of nodes to be started immediately. We don't want to + // start the animations in the loop directly because we first need to set up + // dependencies on all of the nodes. For example, we don't want to start an animation + // when some other animation also wants to start when the first animation begins. + final ArrayList nodesToStart = new ArrayList(); + for (int i = 0; i < numSortedNodes; ++i) { + Node node = mSortedNodes.get(i); + if (mSetListener == null) { + mSetListener = new AnimatorSetListener(this); + } + if (node.dependencies == null || node.dependencies.size() == 0) { + nodesToStart.add(node); + } else { + int numDependencies = node.dependencies.size(); + for (int j = 0; j < numDependencies; ++j) { + Dependency dependency = node.dependencies.get(j); + dependency.node.animation.addListener( + new DependencyListener(this, node, dependency.rule)); + } + node.tmpDependencies = (ArrayList) node.dependencies.clone(); + } + node.animation.addListener(mSetListener); + } + // Now that all dependencies are set up, start the animations that should be started. + if (mStartDelay <= 0) { + for (Node node : nodesToStart) { + node.animation.start(); + mPlayingSet.add(node.animation); + } + } else { + mDelayAnim = ValueAnimator.ofFloat(0f, 1f); + mDelayAnim.setDuration(mStartDelay); + mDelayAnim.addListener(new AnimatorListenerAdapter() { + boolean canceled = false; + public void onAnimationCancel(Animator anim) { + canceled = true; + } + public void onAnimationEnd(Animator anim) { + if (!canceled) { + int numNodes = nodesToStart.size(); + for (int i = 0; i < numNodes; ++i) { + Node node = nodesToStart.get(i); + node.animation.start(); + mPlayingSet.add(node.animation); + } + } + } + }); + mDelayAnim.start(); + } + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationStart(this); + } + } + if (mNodes.size() == 0 && mStartDelay == 0) { + // Handle unusual case where empty AnimatorSet is started - should send out + // end event immediately since the event will not be sent out at all otherwise + mStarted = false; + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationEnd(this); + } + } + } + } + + @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. + */ + anim.mNeedsSort = true; + anim.mTerminated = false; + anim.mStarted = false; + anim.mPlayingSet = new ArrayList(); + anim.mNodeMap = new HashMap(); + anim.mNodes = new ArrayList(); + anim.mSortedNodes = new ArrayList(); + + // 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 nodeCloneMap = new HashMap(); // + for (Node node : mNodes) { + Node nodeClone = node.clone(); + nodeCloneMap.put(node, nodeClone); + anim.mNodes.add(nodeClone); + anim.mNodeMap.put(nodeClone.animation, nodeClone); + // Clear out the dependencies in the clone; we'll set these up manually later + nodeClone.dependencies = null; + nodeClone.tmpDependencies = null; + nodeClone.nodeDependents = null; + nodeClone.nodeDependencies = null; + // clear out any listeners that were set up by the AnimatorSet; these will + // be set up when the clone's nodes are sorted + ArrayList cloneListeners = nodeClone.animation.getListeners(); + if (cloneListeners != null) { + ArrayList listenersToRemove = null; + for (AnimatorListener listener : cloneListeners) { + if (listener instanceof AnimatorSetListener) { + if (listenersToRemove == null) { + listenersToRemove = new ArrayList(); + } + listenersToRemove.add(listener); + } + } + if (listenersToRemove != null) { + for (AnimatorListener listener : listenersToRemove) { + cloneListeners.remove(listener); + } + } + } + } + // 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 (Node node : mNodes) { + Node nodeClone = nodeCloneMap.get(node); + if (node.dependencies != null) { + for (Dependency dependency : node.dependencies) { + Node clonedDependencyNode = nodeCloneMap.get(dependency.node); + Dependency cloneDependency = new Dependency(clonedDependencyNode, + dependency.rule); + nodeClone.addDependency(cloneDependency); + } + } + } + + return anim; + } + + /** + * This class is the mechanism by which animations are started based on events in other + * animations. If an animation has multiple dependencies on other animations, then + * all dependencies must be satisfied before the animation is started. + */ + private static class DependencyListener implements AnimatorListener { + + private AnimatorSet mAnimatorSet; + + // The node upon which the dependency is based. + private Node mNode; + + // The Dependency rule (WITH or AFTER) that the listener should wait for on + // the node + private int mRule; + + public DependencyListener(AnimatorSet animatorSet, Node node, int rule) { + this.mAnimatorSet = animatorSet; + this.mNode = node; + this.mRule = rule; + } + + /** + * Ignore cancel events for now. We may want to handle this eventually, + * to prevent follow-on animations from running when some dependency + * animation is canceled. + */ + public void onAnimationCancel(Animator animation) { + } + + /** + * An end event is received - see if this is an event we are listening for + */ + public void onAnimationEnd(Animator animation) { + if (mRule == Dependency.AFTER) { + startIfReady(animation); + } + } + + /** + * Ignore repeat events for now + */ + public void onAnimationRepeat(Animator animation) { + } + + /** + * A start event is received - see if this is an event we are listening for + */ + public void onAnimationStart(Animator animation) { + if (mRule == Dependency.WITH) { + startIfReady(animation); + } + } + + /** + * Check whether the event received is one that the node was waiting for. + * If so, mark it as complete and see whether it's time to start + * the animation. + * @param dependencyAnimation the animation that sent the event. + */ + private void startIfReady(Animator dependencyAnimation) { + if (mAnimatorSet.mTerminated) { + // if the parent AnimatorSet was canceled, then don't start any dependent anims + return; + } + Dependency dependencyToRemove = null; + int numDependencies = mNode.tmpDependencies.size(); + for (int i = 0; i < numDependencies; ++i) { + Dependency dependency = mNode.tmpDependencies.get(i); + if (dependency.rule == mRule && + dependency.node.animation == dependencyAnimation) { + // rule fired - remove the dependency and listener and check to + // see whether it's time to start the animation + dependencyToRemove = dependency; + dependencyAnimation.removeListener(this); + break; + } + } + mNode.tmpDependencies.remove(dependencyToRemove); + if (mNode.tmpDependencies.size() == 0) { + // all dependencies satisfied: start the animation + mNode.animation.start(); + mAnimatorSet.mPlayingSet.add(mNode.animation); + } + } + + } + + private class AnimatorSetListener implements AnimatorListener { + + private AnimatorSet mAnimatorSet; + + AnimatorSetListener(AnimatorSet animatorSet) { + mAnimatorSet = animatorSet; + } + + public void onAnimationCancel(Animator animation) { + if (!mTerminated) { + // Listeners are already notified of the AnimatorSet canceling in cancel(). + // The logic below only kicks in when animations end normally + if (mPlayingSet.size() == 0) { + if (mListeners != null) { + int numListeners = mListeners.size(); + for (int i = 0; i < numListeners; ++i) { + mListeners.get(i).onAnimationCancel(mAnimatorSet); + } + } + } + } + } + + @SuppressWarnings("unchecked") + public void onAnimationEnd(Animator animation) { + animation.removeListener(this); + mPlayingSet.remove(animation); + Node animNode = mAnimatorSet.mNodeMap.get(animation); + animNode.done = true; + if (!mTerminated) { + // Listeners are already notified of the AnimatorSet ending in cancel() or + // end(); the logic below only kicks in when animations end normally + ArrayList sortedNodes = mAnimatorSet.mSortedNodes; + boolean allDone = true; + int numSortedNodes = sortedNodes.size(); + for (int i = 0; i < numSortedNodes; ++i) { + if (!sortedNodes.get(i).done) { + allDone = false; + break; + } + } + if (allDone) { + // If this was the last child animation to end, then notify listeners that this + // AnimatorSet has ended + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationEnd(mAnimatorSet); + } + } + mAnimatorSet.mStarted = false; + } + } + } + + // Nothing to do + public void onAnimationRepeat(Animator animation) { + } + + // Nothing to do + public void onAnimationStart(Animator animation) { + } + + } + + /** + * This method sorts the current set of nodes, if needed. The sort is a simple + * DependencyGraph sort, which goes like this: + * - All nodes without dependencies become 'roots' + * - while roots list is not null + * - for each root r + * - add r to sorted list + * - remove r as a dependency from any other node + * - any nodes with no dependencies are added to the roots list + */ + private void sortNodes() { + if (mNeedsSort) { + mSortedNodes.clear(); + ArrayList roots = new ArrayList(); + int numNodes = mNodes.size(); + for (int i = 0; i < numNodes; ++i) { + Node node = mNodes.get(i); + if (node.dependencies == null || node.dependencies.size() == 0) { + roots.add(node); + } + } + ArrayList tmpRoots = new ArrayList(); + while (roots.size() > 0) { + int numRoots = roots.size(); + for (int i = 0; i < numRoots; ++i) { + Node root = roots.get(i); + mSortedNodes.add(root); + if (root.nodeDependents != null) { + int numDependents = root.nodeDependents.size(); + for (int j = 0; j < numDependents; ++j) { + Node node = root.nodeDependents.get(j); + node.nodeDependencies.remove(root); + if (node.nodeDependencies.size() == 0) { + tmpRoots.add(node); + } + } + } + } + roots.clear(); + roots.addAll(tmpRoots); + tmpRoots.clear(); + } + mNeedsSort = false; + if (mSortedNodes.size() != mNodes.size()) { + throw new IllegalStateException("Circular dependencies cannot exist" + + " in AnimatorSet"); + } + } else { + // Doesn't need sorting, but still need to add in the nodeDependencies list + // because these get removed as the event listeners fire and the dependencies + // are satisfied + int numNodes = mNodes.size(); + for (int i = 0; i < numNodes; ++i) { + Node node = mNodes.get(i); + if (node.dependencies != null && node.dependencies.size() > 0) { + int numDependencies = node.dependencies.size(); + for (int j = 0; j < numDependencies; ++j) { + Dependency dependency = node.dependencies.get(j); + if (node.nodeDependencies == null) { + node.nodeDependencies = new ArrayList(); + } + if (!node.nodeDependencies.contains(dependency.node)) { + node.nodeDependencies.add(dependency.node); + } + } + } + // nodes are 'done' by default; they become un-done when started, and done + // again when ended + node.done = false; + } + } + } + + /** + * Dependency holds information about the node that some other node is + * dependent upon and the nature of that dependency. + * + */ + private static class Dependency { + static final int WITH = 0; // dependent node must start with this dependency node + static final int AFTER = 1; // dependent node must start when this dependency node finishes + + // The node that the other node with this Dependency is dependent upon + public Node node; + + // The nature of the dependency (WITH or AFTER) + public int rule; + + public Dependency(Node node, int rule) { + this.node = node; + this.rule = rule; + } + } + + /** + * 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 { + public Animator animation; + + /** + * These are the dependencies that this node's animation has on other + * nodes. For example, if this node's animation should begin with some + * other animation ends, then there will be an item in this node's + * dependencies list for that other animation's node. + */ + public ArrayList dependencies = null; + + /** + * tmpDependencies is a runtime detail. We use the dependencies list for sorting. + * But we also use the list to keep track of when multiple dependencies are satisfied, + * but removing each dependency as it is satisfied. We do not want to remove + * the dependency itself from the list, because we need to retain that information + * if the AnimatorSet is launched in the future. So we create a copy of the dependency + * list when the AnimatorSet starts and use this tmpDependencies list to track the + * list of satisfied dependencies. + */ + public ArrayList tmpDependencies = null; + + /** + * nodeDependencies is just a list of the nodes that this Node is dependent upon. + * This information is used in sortNodes(), to determine when a node is a root. + */ + public ArrayList nodeDependencies = null; + + /** + * nodeDepdendents is the list of nodes that have this node as a dependency. This + * is a utility field used in sortNodes to facilitate removing this node as a + * dependency when it is a root node. + */ + public ArrayList nodeDependents = 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 done and it's time to send out an end event for the entire AnimatorSet. + */ + public boolean done = false; + + /** + * 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.animation = animation; + } + + /** + * Add a dependency to this Node. The dependency includes information about the + * node that this node is dependency upon and the nature of the dependency. + * @param dependency + */ + public void addDependency(Dependency dependency) { + if (dependencies == null) { + dependencies = new ArrayList(); + nodeDependencies = new ArrayList(); + } + dependencies.add(dependency); + if (!nodeDependencies.contains(dependency.node)) { + nodeDependencies.add(dependency.node); + } + Node dependencyNode = dependency.node; + if (dependencyNode.nodeDependents == null) { + dependencyNode.nodeDependents = new ArrayList(); + } + dependencyNode.nodeDependents.add(this); + } + + @Override + public Node clone() { + try { + Node node = (Node) super.clone(); + node.animation = (Animator) animation.clone(); + return node; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + } + + /** + * 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 + * com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.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 com.nineoldandroids.animation.AnimatorSet#playTogether(com.nineoldandroids.animation.Animator[]) playTogether()} and {@link + * com.nineoldandroids.animation.AnimatorSet#playSequentially(com.nineoldandroids.animation.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 com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.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 com.nineoldandroids.animation.AnimatorSet.Builder#before(com.nineoldandroids.animation.Animator)} and {@link + * com.nineoldandroids.animation.AnimatorSet.Builder#after(com.nineoldandroids.animation.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 com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.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) { + mCurrentNode = mNodeMap.get(anim); + if (mCurrentNode == null) { + mCurrentNode = new Node(anim); + mNodeMap.put(anim, mCurrentNode); + mNodes.add(mCurrentNode); + } + } + + /** + * Sets up the given animation to play at the same time as the animation supplied in the + * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} call that created this Builder object. + * + * @param anim The animation that will play when the animation supplied to the + * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} method starts. + */ + public Builder with(Animator anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + Dependency dependency = new Dependency(mCurrentNode, Dependency.WITH); + node.addDependency(dependency); + return this; + } + + /** + * Sets up the given animation to play when the animation supplied in the + * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} call that created this Builder object + * ends. + * + * @param anim The animation that will play when the animation supplied to the + * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} method ends. + */ + public Builder before(Animator anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + Dependency dependency = new Dependency(mCurrentNode, Dependency.AFTER); + node.addDependency(dependency); + return this; + } + + /** + * Sets up the given animation to play when the animation supplied in the + * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.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 com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.Animator)} method to play. + */ + public Builder after(Animator anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + Dependency dependency = new Dependency(node, Dependency.AFTER); + mCurrentNode.addDependency(dependency); + return this; + } + + /** + * Sets up the animation supplied in the + * {@link com.nineoldandroids.animation.AnimatorSet#play(com.nineoldandroids.animation.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 dummy ValueAnimator just to run the clock + ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); + anim.setDuration(delay); + after(anim); + return this; + } + + } + +} diff --git a/app/src/main/java/com/nineoldandroids/animation/ArgbEvaluator.java b/app/src/main/java/com/nineoldandroids/animation/ArgbEvaluator.java new file mode 100644 index 000000000..6b550c3b0 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/ArgbEvaluator.java @@ -0,0 +1,72 @@ +/* + * 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 com.nineoldandroids.animation; + +/** + * This evaluator can be used to perform type interpolation between integer + * values that represent ARGB colors. + */ +public class ArgbEvaluator implements TypeEvaluator { + private static final ArgbEvaluator sInstance = new ArgbEvaluator(); + + /** + * Returns an instance of ArgbEvaluator that may be used in + * {@link ValueAnimator#setEvaluator(TypeEvaluator)}. The same instance may + * be used in multiple Animators because it holds no state. + * @return An instance of ArgbEvalutor. + * + * @hide + */ + public static ArgbEvaluator getInstance() { + return sInstance; + } + + /** + * This function returns the calculated in-between value for a color + * given integers that represent the start and end values in the four + * bytes of the 32-bit int. Each channel is separately linearly interpolated + * and the resulting calculated values are recombined into the return value. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue A 32-bit int value representing colors in the + * separate bytes of the parameter + * @param endValue A 32-bit int value representing colors in the + * separate bytes of the parameter + * @return A value that is calculated to be the linearly interpolated + * result, derived by separating the start and end values into separate + * color channels and interpolating each one separately, recombining the + * resulting values in the same way. + */ + public Object evaluate(float fraction, Object startValue, Object endValue) { + int startInt = (Integer) startValue; + int startA = (startInt >> 24) & 0xff; + int startR = (startInt >> 16) & 0xff; + int startG = (startInt >> 8) & 0xff; + int startB = startInt & 0xff; + + int endInt = (Integer) endValue; + int endA = (endInt >> 24) & 0xff; + int endR = (endInt >> 16) & 0xff; + int endG = (endInt >> 8) & 0xff; + int endB = endInt & 0xff; + + return (int)((startA + (int)(fraction * (endA - startA))) << 24) | + (int)((startR + (int)(fraction * (endR - startR))) << 16) | + (int)((startG + (int)(fraction * (endG - startG))) << 8) | + (int)((startB + (int)(fraction * (endB - startB)))); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nineoldandroids/animation/FloatEvaluator.java b/app/src/main/java/com/nineoldandroids/animation/FloatEvaluator.java new file mode 100644 index 000000000..c53cdfd39 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/FloatEvaluator.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nineoldandroids.animation; + +/** + * This evaluator can be used to perform type interpolation between float values. + */ +public class FloatEvaluator implements TypeEvaluator { + + /** + * This function returns the result of linearly interpolating the start and end values, with + * fraction representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0), + * where x0 is startValue, x1 is endValue, + * and t is fraction. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value; should be of type float or + * Float + * @param endValue The end value; should be of type float or Float + * @return A linear interpolation between the start and end values, given the + * fraction parameter. + */ + public Float evaluate(float fraction, Number startValue, Number endValue) { + float startFloat = startValue.floatValue(); + return startFloat + fraction * (endValue.floatValue() - startFloat); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nineoldandroids/animation/FloatKeyframeSet.java b/app/src/main/java/com/nineoldandroids/animation/FloatKeyframeSet.java new file mode 100644 index 000000000..1c32f9cfd --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/FloatKeyframeSet.java @@ -0,0 +1,136 @@ +/* + * 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 com.nineoldandroids.animation; + +import android.view.animation.Interpolator; + +import com.nineoldandroids.animation.Keyframe.FloatKeyframe; + +import java.util.ArrayList; + +/** + * This class holds a collection of FloatKeyframe objects and is called by ValueAnimator to calculate + * values between those keyframes for a given animation. The class internal to the animation + * package because it is an implementation detail of how Keyframes are stored and used. + * + *

This type-specific subclass of KeyframeSet, along with the other type-specific subclass for + * int, exists to speed up the getValue() method when there is no custom + * TypeEvaluator set for the animation, so that values can be calculated without autoboxing to the + * Object equivalents of these primitive types.

+ */ +class FloatKeyframeSet extends KeyframeSet { + private float firstValue; + private float lastValue; + private float deltaValue; + private boolean firstTime = true; + + public FloatKeyframeSet(FloatKeyframe... keyframes) { + super(keyframes); + } + + @Override + public Object getValue(float fraction) { + return getFloatValue(fraction); + } + + @Override + public FloatKeyframeSet clone() { + ArrayList keyframes = mKeyframes; + int numKeyframes = mKeyframes.size(); + FloatKeyframe[] newKeyframes = new FloatKeyframe[numKeyframes]; + for (int i = 0; i < numKeyframes; ++i) { + newKeyframes[i] = (FloatKeyframe) keyframes.get(i).clone(); + } + FloatKeyframeSet newSet = new FloatKeyframeSet(newKeyframes); + return newSet; + } + + public float getFloatValue(float fraction) { + if (mNumKeyframes == 2) { + if (firstTime) { + firstTime = false; + firstValue = ((FloatKeyframe) mKeyframes.get(0)).getFloatValue(); + lastValue = ((FloatKeyframe) mKeyframes.get(1)).getFloatValue(); + deltaValue = lastValue - firstValue; + } + if (mInterpolator != null) { + fraction = mInterpolator.getInterpolation(fraction); + } + if (mEvaluator == null) { + return firstValue + fraction * deltaValue; + } else { + return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).floatValue(); + } + } + if (fraction <= 0f) { + final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0); + final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(1); + float prevValue = prevKeyframe.getFloatValue(); + float nextValue = nextKeyframe.getFloatValue(); + float prevFraction = prevKeyframe.getFraction(); + float nextFraction = nextKeyframe.getFraction(); + final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); + return mEvaluator == null ? + prevValue + intervalFraction * (nextValue - prevValue) : + ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)). + floatValue(); + } else if (fraction >= 1f) { + final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 2); + final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 1); + float prevValue = prevKeyframe.getFloatValue(); + float nextValue = nextKeyframe.getFloatValue(); + float prevFraction = prevKeyframe.getFraction(); + float nextFraction = nextKeyframe.getFraction(); + final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); + return mEvaluator == null ? + prevValue + intervalFraction * (nextValue - prevValue) : + ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)). + floatValue(); + } + FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0); + for (int i = 1; i < mNumKeyframes; ++i) { + FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(i); + if (fraction < nextKeyframe.getFraction()) { + final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + float intervalFraction = (fraction - prevKeyframe.getFraction()) / + (nextKeyframe.getFraction() - prevKeyframe.getFraction()); + float prevValue = prevKeyframe.getFloatValue(); + float nextValue = nextKeyframe.getFloatValue(); + return mEvaluator == null ? + prevValue + intervalFraction * (nextValue - prevValue) : + ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)). + floatValue(); + } + prevKeyframe = nextKeyframe; + } + // shouldn't get here + return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).floatValue(); + } + +} + diff --git a/app/src/main/java/com/nineoldandroids/animation/IntEvaluator.java b/app/src/main/java/com/nineoldandroids/animation/IntEvaluator.java new file mode 100644 index 000000000..d60542b9e --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/IntEvaluator.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nineoldandroids.animation; + +/** + * This evaluator can be used to perform type interpolation between int values. + */ +public class IntEvaluator implements TypeEvaluator { + + /** + * This function returns the result of linearly interpolating the start and end values, with + * fraction representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0), + * where x0 is startValue, x1 is endValue, + * and t is fraction. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value; should be of type int or + * Integer + * @param endValue The end value; should be of type int or Integer + * @return A linear interpolation between the start and end values, given the + * fraction parameter. + */ + public Integer evaluate(float fraction, Integer startValue, Integer endValue) { + int startInt = startValue; + return (int)(startInt + fraction * (endValue - startInt)); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nineoldandroids/animation/IntKeyframeSet.java b/app/src/main/java/com/nineoldandroids/animation/IntKeyframeSet.java new file mode 100644 index 000000000..9c26c1e8c --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/IntKeyframeSet.java @@ -0,0 +1,135 @@ +/* + * 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 com.nineoldandroids.animation; + +import android.view.animation.Interpolator; + +import com.nineoldandroids.animation.Keyframe.IntKeyframe; + +import java.util.ArrayList; + +/** + * This class holds a collection of IntKeyframe objects and is called by ValueAnimator to calculate + * values between those keyframes for a given animation. The class internal to the animation + * package because it is an implementation detail of how Keyframes are stored and used. + * + *

This type-specific subclass of KeyframeSet, along with the other type-specific subclass for + * float, exists to speed up the getValue() method when there is no custom + * TypeEvaluator set for the animation, so that values can be calculated without autoboxing to the + * Object equivalents of these primitive types.

+ */ +class IntKeyframeSet extends KeyframeSet { + private int firstValue; + private int lastValue; + private int deltaValue; + private boolean firstTime = true; + + public IntKeyframeSet(IntKeyframe... keyframes) { + super(keyframes); + } + + @Override + public Object getValue(float fraction) { + return getIntValue(fraction); + } + + @Override + public IntKeyframeSet clone() { + ArrayList keyframes = mKeyframes; + int numKeyframes = mKeyframes.size(); + IntKeyframe[] newKeyframes = new IntKeyframe[numKeyframes]; + for (int i = 0; i < numKeyframes; ++i) { + newKeyframes[i] = (IntKeyframe) keyframes.get(i).clone(); + } + IntKeyframeSet newSet = new IntKeyframeSet(newKeyframes); + return newSet; + } + + public int getIntValue(float fraction) { + if (mNumKeyframes == 2) { + if (firstTime) { + firstTime = false; + firstValue = ((IntKeyframe) mKeyframes.get(0)).getIntValue(); + lastValue = ((IntKeyframe) mKeyframes.get(1)).getIntValue(); + deltaValue = lastValue - firstValue; + } + if (mInterpolator != null) { + fraction = mInterpolator.getInterpolation(fraction); + } + if (mEvaluator == null) { + return firstValue + (int)(fraction * deltaValue); + } else { + return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).intValue(); + } + } + if (fraction <= 0f) { + final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0); + final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(1); + int prevValue = prevKeyframe.getIntValue(); + int nextValue = nextKeyframe.getIntValue(); + float prevFraction = prevKeyframe.getFraction(); + float nextFraction = nextKeyframe.getFraction(); + final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); + return mEvaluator == null ? + prevValue + (int)(intervalFraction * (nextValue - prevValue)) : + ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)). + intValue(); + } else if (fraction >= 1f) { + final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 2); + final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 1); + int prevValue = prevKeyframe.getIntValue(); + int nextValue = nextKeyframe.getIntValue(); + float prevFraction = prevKeyframe.getFraction(); + float nextFraction = nextKeyframe.getFraction(); + final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); + return mEvaluator == null ? + prevValue + (int)(intervalFraction * (nextValue - prevValue)) : + ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue(); + } + IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0); + for (int i = 1; i < mNumKeyframes; ++i) { + IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(i); + if (fraction < nextKeyframe.getFraction()) { + final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + float intervalFraction = (fraction - prevKeyframe.getFraction()) / + (nextKeyframe.getFraction() - prevKeyframe.getFraction()); + int prevValue = prevKeyframe.getIntValue(); + int nextValue = nextKeyframe.getIntValue(); + return mEvaluator == null ? + prevValue + (int)(intervalFraction * (nextValue - prevValue)) : + ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)). + intValue(); + } + prevKeyframe = nextKeyframe; + } + // shouldn't get here + return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).intValue(); + } + +} + diff --git a/app/src/main/java/com/nineoldandroids/animation/Keyframe.java b/app/src/main/java/com/nineoldandroids/animation/Keyframe.java new file mode 100644 index 000000000..e7062a66a --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/Keyframe.java @@ -0,0 +1,360 @@ +/* + * 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 com.nineoldandroids.animation; + +import android.view.animation.Interpolator; + +/** + * This class holds a time/value pair for an animation. The Keyframe class is used + * by {@link ValueAnimator} to define the values that the animation target will have over the course + * of the animation. As the time proceeds from one keyframe to the other, the value of the + * target object will animate between the value at the previous keyframe and the value at the + * next keyframe. Each keyframe also holds an optional {@link TimeInterpolator} + * object, which defines the time interpolation over the intervalue preceding the keyframe. + * + *

The Keyframe class itself is abstract. The type-specific factory methods will return + * a subclass of Keyframe specific to the type of value being stored. This is done to improve + * performance when dealing with the most common cases (e.g., float and + * int values). Other types will fall into a more general Keyframe class that + * treats its values as Objects. Unless your animation requires dealing with a custom type + * or a data structure that needs to be animated directly (and evaluated using an implementation + * of {@link TypeEvaluator}), you should stick to using float and int as animations using those + * types have lower runtime overhead than other types.

+ */ +public abstract class Keyframe implements Cloneable { + /** + * The time at which mValue will hold true. + */ + float mFraction; + + /** + * The type of the value in this Keyframe. This type is determined at construction time, + * based on the type of the value object passed into the constructor. + */ + Class mValueType; + + /** + * The optional time interpolator for the interval preceding this keyframe. A null interpolator + * (the default) results in linear interpolation over the interval. + */ + private /*Time*/Interpolator mInterpolator = null; + + /** + * Flag to indicate whether this keyframe has a valid value. This flag is used when an + * animation first starts, to populate placeholder keyframes with real values derived + * from the target object. + */ + boolean mHasValue = false; + + /** + * Constructs a Keyframe object with the given time and value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public static Keyframe ofInt(float fraction, int value) { + return new IntKeyframe(fraction, value); + } + + /** + * Constructs a Keyframe object with the given time. The value at this time will be derived + * from the target object when the animation first starts (note that this implies that keyframes + * with no initial value must be used as part of an {@link ObjectAnimator}). + * The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + */ + public static Keyframe ofInt(float fraction) { + return new IntKeyframe(fraction); + } + + /** + * Constructs a Keyframe object with the given time and value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public static Keyframe ofFloat(float fraction, float value) { + return new FloatKeyframe(fraction, value); + } + + /** + * Constructs a Keyframe object with the given time. The value at this time will be derived + * from the target object when the animation first starts (note that this implies that keyframes + * with no initial value must be used as part of an {@link ObjectAnimator}). + * The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + */ + public static Keyframe ofFloat(float fraction) { + return new FloatKeyframe(fraction); + } + + /** + * Constructs a Keyframe object with the given time and value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public static Keyframe ofObject(float fraction, Object value) { + return new ObjectKeyframe(fraction, value); + } + + /** + * Constructs a Keyframe object with the given time. The value at this time will be derived + * from the target object when the animation first starts (note that this implies that keyframes + * with no initial value must be used as part of an {@link ObjectAnimator}). + * The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + */ + public static Keyframe ofObject(float fraction) { + return new ObjectKeyframe(fraction, null); + } + + /** + * Indicates whether this keyframe has a valid value. This method is called internally when + * an {@link ObjectAnimator} first starts; keyframes without values are assigned values at + * that time by deriving the value for the property from the target object. + * + * @return boolean Whether this object has a value assigned. + */ + public boolean hasValue() { + return mHasValue; + } + + /** + * Gets the value for this Keyframe. + * + * @return The value for this Keyframe. + */ + public abstract Object getValue(); + + /** + * Sets the value for this Keyframe. + * + * @param value value for this Keyframe. + */ + public abstract void setValue(Object value); + + /** + * Gets the time for this keyframe, as a fraction of the overall animation duration. + * + * @return The time associated with this keyframe, as a fraction of the overall animation + * duration. This should be a value between 0 and 1. + */ + public float getFraction() { + return mFraction; + } + + /** + * Sets the time for this keyframe, as a fraction of the overall animation duration. + * + * @param fraction time associated with this keyframe, as a fraction of the overall animation + * duration. This should be a value between 0 and 1. + */ + public void setFraction(float fraction) { + mFraction = fraction; + } + + /** + * Gets the optional interpolator for this Keyframe. A value of null indicates + * that there is no interpolation, which is the same as linear interpolation. + * + * @return The optional interpolator for this Keyframe. + */ + public /*Time*/Interpolator getInterpolator() { + return mInterpolator; + } + + /** + * Sets the optional interpolator for this Keyframe. A value of null indicates + * that there is no interpolation, which is the same as linear interpolation. + * + * @return The optional interpolator for this Keyframe. + */ + public void setInterpolator(/*Time*/Interpolator interpolator) { + mInterpolator = interpolator; + } + + /** + * Gets the type of keyframe. This information is used by ValueAnimator to determine the type of + * {@link TypeEvaluator} to use when calculating values between keyframes. The type is based + * on the type of Keyframe created. + * + * @return The type of the value stored in the Keyframe. + */ + public Class getType() { + return mValueType; + } + + @Override + public abstract Keyframe clone(); + + /** + * This internal subclass is used for all types which are not int or float. + */ + static class ObjectKeyframe extends Keyframe { + + /** + * The value of the animation at the time mFraction. + */ + Object mValue; + + ObjectKeyframe(float fraction, Object value) { + mFraction = fraction; + mValue = value; + mHasValue = (value != null); + mValueType = mHasValue ? value.getClass() : Object.class; + } + + public Object getValue() { + return mValue; + } + + public void setValue(Object value) { + mValue = value; + mHasValue = (value != null); + } + + @Override + public ObjectKeyframe clone() { + ObjectKeyframe kfClone = new ObjectKeyframe(getFraction(), mValue); + kfClone.setInterpolator(getInterpolator()); + return kfClone; + } + } + + /** + * Internal subclass used when the keyframe value is of type int. + */ + static class IntKeyframe extends Keyframe { + + /** + * The value of the animation at the time mFraction. + */ + int mValue; + + IntKeyframe(float fraction, int value) { + mFraction = fraction; + mValue = value; + mValueType = int.class; + mHasValue = true; + } + + IntKeyframe(float fraction) { + mFraction = fraction; + mValueType = int.class; + } + + public int getIntValue() { + return mValue; + } + + public Object getValue() { + return mValue; + } + + public void setValue(Object value) { + if (value != null && value.getClass() == Integer.class) { + mValue = ((Integer)value).intValue(); + mHasValue = true; + } + } + + @Override + public IntKeyframe clone() { + IntKeyframe kfClone = new IntKeyframe(getFraction(), mValue); + kfClone.setInterpolator(getInterpolator()); + return kfClone; + } + } + + /** + * Internal subclass used when the keyframe value is of type float. + */ + static class FloatKeyframe extends Keyframe { + /** + * The value of the animation at the time mFraction. + */ + float mValue; + + FloatKeyframe(float fraction, float value) { + mFraction = fraction; + mValue = value; + mValueType = float.class; + mHasValue = true; + } + + FloatKeyframe(float fraction) { + mFraction = fraction; + mValueType = float.class; + } + + public float getFloatValue() { + return mValue; + } + + public Object getValue() { + return mValue; + } + + public void setValue(Object value) { + if (value != null && value.getClass() == Float.class) { + mValue = ((Float)value).floatValue(); + mHasValue = true; + } + } + + @Override + public FloatKeyframe clone() { + FloatKeyframe kfClone = new FloatKeyframe(getFraction(), mValue); + kfClone.setInterpolator(getInterpolator()); + return kfClone; + } + } +} diff --git a/app/src/main/java/com/nineoldandroids/animation/KeyframeSet.java b/app/src/main/java/com/nineoldandroids/animation/KeyframeSet.java new file mode 100644 index 000000000..5694d4c3d --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/KeyframeSet.java @@ -0,0 +1,227 @@ +/* + * 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 com.nineoldandroids.animation; + +import java.util.ArrayList; +import java.util.Arrays; + +import android.view.animation.Interpolator; + +import com.nineoldandroids.animation.Keyframe.FloatKeyframe; +import com.nineoldandroids.animation.Keyframe.IntKeyframe; +import com.nineoldandroids.animation.Keyframe.ObjectKeyframe; + +/** + * This class holds a collection of Keyframe objects and is called by ValueAnimator to calculate + * values between those keyframes for a given animation. The class internal to the animation + * package because it is an implementation detail of how Keyframes are stored and used. + */ +class KeyframeSet { + + int mNumKeyframes; + + Keyframe mFirstKeyframe; + Keyframe mLastKeyframe; + /*Time*/Interpolator mInterpolator; // only used in the 2-keyframe case + ArrayList mKeyframes; // only used when there are not 2 keyframes + TypeEvaluator mEvaluator; + + + public KeyframeSet(Keyframe... keyframes) { + mNumKeyframes = keyframes.length; + mKeyframes = new ArrayList(); + mKeyframes.addAll(Arrays.asList(keyframes)); + mFirstKeyframe = mKeyframes.get(0); + mLastKeyframe = mKeyframes.get(mNumKeyframes - 1); + mInterpolator = mLastKeyframe.getInterpolator(); + } + + public static KeyframeSet ofInt(int... values) { + int numKeyframes = values.length; + IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)]; + if (numKeyframes == 1) { + keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f); + keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]); + } else { + keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]); + for (int i = 1; i < numKeyframes; ++i) { + keyframes[i] = (IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]); + } + } + return new IntKeyframeSet(keyframes); + } + + public static KeyframeSet ofFloat(float... values) { + int numKeyframes = values.length; + FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)]; + if (numKeyframes == 1) { + keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f); + keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]); + } else { + keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]); + for (int i = 1; i < numKeyframes; ++i) { + keyframes[i] = (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]); + } + } + return new FloatKeyframeSet(keyframes); + } + + public static KeyframeSet ofKeyframe(Keyframe... keyframes) { + // if all keyframes of same primitive type, create the appropriate KeyframeSet + int numKeyframes = keyframes.length; + boolean hasFloat = false; + boolean hasInt = false; + boolean hasOther = false; + for (int i = 0; i < numKeyframes; ++i) { + if (keyframes[i] instanceof FloatKeyframe) { + hasFloat = true; + } else if (keyframes[i] instanceof IntKeyframe) { + hasInt = true; + } else { + hasOther = true; + } + } + if (hasFloat && !hasInt && !hasOther) { + FloatKeyframe floatKeyframes[] = new FloatKeyframe[numKeyframes]; + for (int i = 0; i < numKeyframes; ++i) { + floatKeyframes[i] = (FloatKeyframe) keyframes[i]; + } + return new FloatKeyframeSet(floatKeyframes); + } else if (hasInt && !hasFloat && !hasOther) { + IntKeyframe intKeyframes[] = new IntKeyframe[numKeyframes]; + for (int i = 0; i < numKeyframes; ++i) { + intKeyframes[i] = (IntKeyframe) keyframes[i]; + } + return new IntKeyframeSet(intKeyframes); + } else { + return new KeyframeSet(keyframes); + } + } + + public static KeyframeSet ofObject(Object... values) { + int numKeyframes = values.length; + ObjectKeyframe keyframes[] = new ObjectKeyframe[Math.max(numKeyframes,2)]; + if (numKeyframes == 1) { + keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f); + keyframes[1] = (ObjectKeyframe) Keyframe.ofObject(1f, values[0]); + } else { + keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f, values[0]); + for (int i = 1; i < numKeyframes; ++i) { + keyframes[i] = (ObjectKeyframe) Keyframe.ofObject((float) i / (numKeyframes - 1), values[i]); + } + } + return new KeyframeSet(keyframes); + } + + /** + * Sets the TypeEvaluator to be used when calculating animated values. This object + * is required only for KeyframeSets that are not either IntKeyframeSet or FloatKeyframeSet, + * both of which assume their own evaluator to speed up calculations with those primitive + * types. + * + * @param evaluator The TypeEvaluator to be used to calculate animated values. + */ + public void setEvaluator(TypeEvaluator evaluator) { + mEvaluator = evaluator; + } + + @Override + public KeyframeSet clone() { + ArrayList keyframes = mKeyframes; + int numKeyframes = mKeyframes.size(); + Keyframe[] newKeyframes = new Keyframe[numKeyframes]; + for (int i = 0; i < numKeyframes; ++i) { + newKeyframes[i] = keyframes.get(i).clone(); + } + KeyframeSet newSet = new KeyframeSet(newKeyframes); + return newSet; + } + + /** + * Gets the animated value, given the elapsed fraction of the animation (interpolated by the + * animation's interpolator) and the evaluator used to calculate in-between values. This + * function maps the input fraction to the appropriate keyframe interval and a fraction + * between them and returns the interpolated value. Note that the input fraction may fall + * outside the [0-1] bounds, if the animation's interpolator made that happen (e.g., a + * spring interpolation that might send the fraction past 1.0). We handle this situation by + * just using the two keyframes at the appropriate end when the value is outside those bounds. + * + * @param fraction The elapsed fraction of the animation + * @return The animated value. + */ + public Object getValue(float fraction) { + + // Special-case optimization for the common case of only two keyframes + if (mNumKeyframes == 2) { + if (mInterpolator != null) { + fraction = mInterpolator.getInterpolation(fraction); + } + return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(), + mLastKeyframe.getValue()); + } + if (fraction <= 0f) { + final Keyframe nextKeyframe = mKeyframes.get(1); + final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + final float prevFraction = mFirstKeyframe.getFraction(); + float intervalFraction = (fraction - prevFraction) / + (nextKeyframe.getFraction() - prevFraction); + return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(), + nextKeyframe.getValue()); + } else if (fraction >= 1f) { + final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2); + final /*Time*/Interpolator interpolator = mLastKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + final float prevFraction = prevKeyframe.getFraction(); + float intervalFraction = (fraction - prevFraction) / + (mLastKeyframe.getFraction() - prevFraction); + return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), + mLastKeyframe.getValue()); + } + Keyframe prevKeyframe = mFirstKeyframe; + for (int i = 1; i < mNumKeyframes; ++i) { + Keyframe nextKeyframe = mKeyframes.get(i); + if (fraction < nextKeyframe.getFraction()) { + final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + final float prevFraction = prevKeyframe.getFraction(); + float intervalFraction = (fraction - prevFraction) / + (nextKeyframe.getFraction() - prevFraction); + return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), + nextKeyframe.getValue()); + } + prevKeyframe = nextKeyframe; + } + // shouldn't reach here + return mLastKeyframe.getValue(); + } + + @Override + public String toString() { + String returnVal = " "; + for (int i = 0; i < mNumKeyframes; ++i) { + returnVal += mKeyframes.get(i).getValue() + " "; + } + return returnVal; + } +} diff --git a/app/src/main/java/com/nineoldandroids/animation/ObjectAnimator.java b/app/src/main/java/com/nineoldandroids/animation/ObjectAnimator.java new file mode 100644 index 000000000..2dc2aa0a4 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/ObjectAnimator.java @@ -0,0 +1,583 @@ +/* + * 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 com.nineoldandroids.animation; + +import android.util.Log; +import android.view.View; + +import com.nineoldandroids.util.Property; +import com.nineoldandroids.view.animation.AnimatorProxy; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * This subclass of {@link com.nineoldandroids.animation.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. + * + * @see #setPropertyName(String) + * + */ +public final class ObjectAnimator extends ValueAnimator { + private static final boolean DBG = false; + private static final Map PROXY_PROPERTIES = new HashMap(); + + static { + PROXY_PROPERTIES.put("alpha", PreHoneycombCompat.ALPHA); + PROXY_PROPERTIES.put("pivotX", PreHoneycombCompat.PIVOT_X); + PROXY_PROPERTIES.put("pivotY", PreHoneycombCompat.PIVOT_Y); + PROXY_PROPERTIES.put("translationX", PreHoneycombCompat.TRANSLATION_X); + PROXY_PROPERTIES.put("translationY", PreHoneycombCompat.TRANSLATION_Y); + PROXY_PROPERTIES.put("rotation", PreHoneycombCompat.ROTATION); + PROXY_PROPERTIES.put("rotationX", PreHoneycombCompat.ROTATION_X); + PROXY_PROPERTIES.put("rotationY", PreHoneycombCompat.ROTATION_Y); + PROXY_PROPERTIES.put("scaleX", PreHoneycombCompat.SCALE_X); + PROXY_PROPERTIES.put("scaleY", PreHoneycombCompat.SCALE_Y); + PROXY_PROPERTIES.put("scrollX", PreHoneycombCompat.SCROLL_X); + PROXY_PROPERTIES.put("scrollY", PreHoneycombCompat.SCROLL_Y); + PROXY_PROPERTIES.put("x", PreHoneycombCompat.X); + PROXY_PROPERTIES.put("y", PreHoneycombCompat.Y); + } + + // The target object on which the property exists, set in the constructor + 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(String propertyName) { + // mValues could be null if this is being constructed piecemeal. Just record the + // propertyName to be used later when setValues() is called if so. + if (mValues != null) { + PropertyValuesHolder valuesHolder = mValues[0]; + String oldName = valuesHolder.getPropertyName(); + valuesHolder.setPropertyName(propertyName); + mValuesMap.remove(oldName); + mValuesMap.put(propertyName, valuesHolder); + } + mPropertyName = propertyName; + // New property/values/target should cause re-initialization prior to starting + mInitialized = false; + } + + /** + * Sets the property that will be animated. Property objects will take precedence over + * properties specified by the {@link #setPropertyName(String)} method. Animations should + * be set up to use one or the other, not both. + * + * @param property The property being animated. Should not be null. + */ + public void setProperty(Property property) { + // mValues could be null if this is being constructed piecemeal. Just record the + // propertyName to be used later when setValues() is called if so. + if (mValues != null) { + PropertyValuesHolder valuesHolder = mValues[0]; + String oldName = valuesHolder.getPropertyName(); + valuesHolder.setProperty(property); + mValuesMap.remove(oldName); + mValuesMap.put(mPropertyName, valuesHolder); + } + if (mProperty != null) { + mPropertyName = property.getName(); + } + mProperty = property; + // New property/values/target should cause re-initialization prior to starting + mInitialized = false; + } + + /** + * Gets the name of the property that will be animated. This name will be used to derive + * a setter function that will be called to set animated values. + * For example, a property name of 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. + */ + public String getPropertyName() { + return mPropertyName; + } + + /** + * Creates a new ObjectAnimator object. This default constructor is primarily for + * use internally; the other constructors which take parameters are more generally + * useful. + */ + public ObjectAnimator() { + } + + /** + * Private utility constructor that initializes the target object and name of the + * property being animated. + * + * @param target The object whose property is to be animated. This object should + * have a public method on it called 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) { + mTarget = 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) { + mTarget = 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. Two values imply a 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. Two values imply a 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 float values. A single + * value implies that that value is the one being animated to. Two values imply a 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) { + ObjectAnimator anim = new ObjectAnimator(target, propertyName); + anim.setFloatValues(values); + return anim; + } + + /** + * Constructs and returns an ObjectAnimator that animates between float values. A single + * value implies that that value is the one being animated to. Two values imply a 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; + } + + /** + * Constructs and returns an ObjectAnimator that animates between Object values. A single + * value implies that that value is the one being animated to. Two values imply a 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 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 Object values. A single + * value implies that that value is the one being animated to. Two values imply a 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 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(T target, Property property, + TypeEvaluator evaluator, V... values) { + ObjectAnimator anim = new ObjectAnimator(target, property); + anim.setObjectValues(values); + anim.setEvaluator(evaluator); + return anim; + } + + /** + * Constructs and returns an ObjectAnimator that animates between 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. + */ + public static ObjectAnimator ofPropertyValuesHolder(Object target, + PropertyValuesHolder... values) { + ObjectAnimator anim = new ObjectAnimator(); + anim.mTarget = 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(Animator anim) { + if (anim instanceof ObjectAnimator) { + PropertyValuesHolder[] theirValues = ((ObjectAnimator) anim).getValues(); + if (((ObjectAnimator) anim).getTarget() == getTarget() && + mValues.length == theirValues.length) { + for (int i = 0; i < mValues.length; ++i) { + PropertyValuesHolder pvhMine = mValues[i]; + PropertyValuesHolder pvhTheirs = theirValues[i]; + if (pvhMine.getPropertyName() == null || + !pvhMine.getPropertyName().equals(pvhTheirs.getPropertyName())) { + return false; + } + } + return true; + } + } + return false; + } + + @Override + public void start() { + // See if any of the current active/pending animators need to be canceled + AnimationHandler handler = sAnimationHandler.get(); + if (handler != null) { + int numAnims = sAnimations.get().size(); + for (int i = numAnims - 1; i >= 0; i--) { + if (sAnimations.get().get(i) instanceof ObjectAnimator) { + ObjectAnimator anim = (ObjectAnimator) sAnimations.get().get(i); + if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { + anim.cancel(); + } + } + } + numAnims = sPendingAnimations.get().size(); + for (int i = numAnims - 1; i >= 0; i--) { + if (sPendingAnimations.get().get(i) instanceof ObjectAnimator) { + ObjectAnimator anim = (ObjectAnimator) sPendingAnimations.get().get(i); + if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { + anim.cancel(); + } + } + } + numAnims = sDelayedAnims.get().size(); + for (int i = numAnims - 1; i >= 0; i--) { + if (sDelayedAnims.get().get(i) instanceof ObjectAnimator) { + ObjectAnimator anim = (ObjectAnimator) sDelayedAnims.get().get(i); + if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { + anim.cancel(); + } + } + } + } + if (DBG) { + Log.d("ObjectAnimator", "Anim target, duration: " + mTarget + ", " + getDuration()); + for (int i = 0; i < mValues.length; ++i) { + PropertyValuesHolder pvh = mValues[i]; + ArrayList keyframes = pvh.mKeyframeSet.mKeyframes; + Log.d("ObjectAnimator", " Values[" + i + "]: " + + pvh.getPropertyName() + ", " + keyframes.get(0).getValue() + ", " + + keyframes.get(pvh.mKeyframeSet.mNumKeyframes - 1).getValue()); + } + } + super.start(); + } + + /** + * 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. + if ((mProperty == null) && AnimatorProxy.NEEDS_PROXY && (mTarget instanceof View) && PROXY_PROPERTIES.containsKey(mPropertyName)) { + setProperty(PROXY_PROPERTIES.get(mPropertyName)); + } + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].setupSetterAndGetter(mTarget); + } + 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 + 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 + */ + public Object getTarget() { + return mTarget; + } + + /** + * Sets the target object whose property will be animated by this animation + * + * @param target The object being animated + */ + @Override + public void setTarget(Object target) { + if (mTarget != target) { + final Object oldTarget = mTarget; + mTarget = target; + if (oldTarget != null && target != null && oldTarget.getClass() == target.getClass()) { + return; + } + // New target type should cause re-initialization prior to starting + mInitialized = false; + } + } + + @Override + public void setupStartValues() { + initAnimation(); + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].setupStartValue(mTarget); + } + } + + @Override + public void setupEndValues() { + initAnimation(); + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].setupEndValue(mTarget); + } + } + + /** + * 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) { + super.animateValue(fraction); + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].setAnimatedValue(mTarget); + } + } + + @Override + public ObjectAnimator clone() { + final ObjectAnimator anim = (ObjectAnimator) super.clone(); + return anim; + } + + @Override + public String toString() { + String returnVal = "ObjectAnimator@" + Integer.toHexString(hashCode()) + ", target " + + mTarget; + if (mValues != null) { + for (int i = 0; i < mValues.length; ++i) { + returnVal += "\n " + mValues[i].toString(); + } + } + return returnVal; + } +} diff --git a/app/src/main/java/com/nineoldandroids/animation/PreHoneycombCompat.java b/app/src/main/java/com/nineoldandroids/animation/PreHoneycombCompat.java new file mode 100644 index 000000000..8a68d82c0 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/PreHoneycombCompat.java @@ -0,0 +1,168 @@ +package com.nineoldandroids.animation; + +import android.view.View; +import com.nineoldandroids.util.FloatProperty; +import com.nineoldandroids.util.IntProperty; +import com.nineoldandroids.util.Property; +import com.nineoldandroids.view.animation.AnimatorProxy; + +final class PreHoneycombCompat { + static Property ALPHA = new FloatProperty("alpha") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setAlpha(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getAlpha(); + } + }; + static Property PIVOT_X = new FloatProperty("pivotX") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setPivotX(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getPivotX(); + } + }; + static Property PIVOT_Y = new FloatProperty("pivotY") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setPivotY(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getPivotY(); + } + }; + static Property TRANSLATION_X = new FloatProperty("translationX") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setTranslationX(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getTranslationX(); + } + }; + static Property TRANSLATION_Y = new FloatProperty("translationY") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setTranslationY(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getTranslationY(); + } + }; + static Property ROTATION = new FloatProperty("rotation") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setRotation(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getRotation(); + } + }; + static Property ROTATION_X = new FloatProperty("rotationX") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setRotationX(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getRotationX(); + } + }; + static Property ROTATION_Y = new FloatProperty("rotationY") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setRotationY(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getRotationY(); + } + }; + static Property SCALE_X = new FloatProperty("scaleX") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setScaleX(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getScaleX(); + } + }; + static Property SCALE_Y = new FloatProperty("scaleY") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setScaleY(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getScaleY(); + } + }; + static Property SCROLL_X = new IntProperty("scrollX") { + @Override + public void setValue(View object, int value) { + AnimatorProxy.wrap(object).setScrollX(value); + } + + @Override + public Integer get(View object) { + return AnimatorProxy.wrap(object).getScrollX(); + } + }; + static Property SCROLL_Y = new IntProperty("scrollY") { + @Override + public void setValue(View object, int value) { + AnimatorProxy.wrap(object).setScrollY(value); + } + + @Override + public Integer get(View object) { + return AnimatorProxy.wrap(object).getScrollY(); + } + }; + static Property X = new FloatProperty("x") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setX(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getX(); + } + }; + static Property Y = new FloatProperty("y") { + @Override + public void setValue(View object, float value) { + AnimatorProxy.wrap(object).setY(value); + } + + @Override + public Float get(View object) { + return AnimatorProxy.wrap(object).getY(); + } + }; + + + //No instances + private PreHoneycombCompat() {} +} diff --git a/app/src/main/java/com/nineoldandroids/animation/PropertyValuesHolder.java b/app/src/main/java/com/nineoldandroids/animation/PropertyValuesHolder.java new file mode 100644 index 000000000..a78e6b5e5 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/PropertyValuesHolder.java @@ -0,0 +1,1030 @@ +/* + * 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 com.nineoldandroids.animation; + +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import com.nineoldandroids.util.FloatProperty; +import com.nineoldandroids.util.IntProperty; +import com.nineoldandroids.util.Property; + +/** + * This class holds information about a property and the values that that property + * should take on during an animation. PropertyValuesHolder objects can be used to create + * animations with ValueAnimator or ObjectAnimator that operate on several different properties + * in parallel. + */ +public class PropertyValuesHolder implements Cloneable { + + /** + * The name of the property associated with the values. This need not be a real property, + * unless this object is being used with ObjectAnimator. But this is the name by which + * aniamted values are looked up with getAnimatedValue(String) in ValueAnimator. + */ + String mPropertyName; + + /** + * @hide + */ + protected Property mProperty; + + /** + * The setter function, if needed. ObjectAnimator hands off this functionality to + * PropertyValuesHolder, since it holds all of the per-property information. This + * property is automatically + * derived when the animation starts in setupSetterAndGetter() if using ObjectAnimator. + */ + Method mSetter = null; + + /** + * The getter function, if needed. ObjectAnimator hands off this functionality to + * PropertyValuesHolder, since it holds all of the per-property information. This + * property is automatically + * derived when the animation starts in setupSetterAndGetter() if using ObjectAnimator. + * The getter is only derived and used if one of the values is null. + */ + private Method mGetter = null; + + /** + * The type of values supplied. This information is used both in deriving the setter/getter + * functions and in deriving the type of TypeEvaluator. + */ + Class mValueType; + + /** + * The set of keyframes (time/value pairs) that define this animation. + */ + KeyframeSet mKeyframeSet = null; + + + // type evaluators for the primitive types handled by this implementation + private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); + private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); + + // We try several different types when searching for appropriate setter/getter functions. + // The caller may have supplied values in a type that does not match the setter/getter + // functions (such as the integers 0 and 1 to represent floating point values for alpha). + // Also, the use of generics in constructors means that we end up with the Object versions + // of primitive types (Float vs. float). But most likely, the setter/getter functions + // will take primitive types instead. + // So we supply an ordered array of other types to try before giving up. + private static Class[] FLOAT_VARIANTS = {float.class, Float.class, double.class, int.class, + Double.class, Integer.class}; + private static Class[] INTEGER_VARIANTS = {int.class, Integer.class, float.class, double.class, + Float.class, Double.class}; + private static Class[] DOUBLE_VARIANTS = {double.class, Double.class, float.class, int.class, + Float.class, Integer.class}; + + // These maps hold all property entries for a particular class. This map + // is used to speed up property/setter/getter lookups for a given class/property + // combination. No need to use reflection on the combination more than once. + private static final HashMap> sSetterPropertyMap = + new HashMap>(); + private static final HashMap> sGetterPropertyMap = + new HashMap>(); + + // This lock is used to ensure that only one thread is accessing the property maps + // at a time. + final ReentrantReadWriteLock mPropertyMapLock = new ReentrantReadWriteLock(); + + // Used to pass single value to varargs parameter in setter invocation + final Object[] mTmpValueArray = new Object[1]; + + /** + * The type evaluator used to calculate the animated values. This evaluator is determined + * automatically based on the type of the start/end objects passed into the constructor, + * but the system only knows about the primitive types int and float. Any other + * type will need to set the evaluator to a custom evaluator for that type. + */ + private TypeEvaluator mEvaluator; + + /** + * The value most recently calculated by calculateValue(). This is set during + * that function and might be retrieved later either by ValueAnimator.animatedValue() or + * by the property-setting logic in ObjectAnimator.animatedValue(). + */ + private Object mAnimatedValue; + + /** + * Internal utility constructor, used by the factory methods to set the property name. + * @param propertyName The name of the property for this holder. + */ + private PropertyValuesHolder(String propertyName) { + mPropertyName = propertyName; + } + + /** + * Internal utility constructor, used by the factory methods to set the property. + * @param property The property for this holder. + */ + private PropertyValuesHolder(Property property) { + mProperty = property; + if (property != null) { + mPropertyName = property.getName(); + } + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name and + * set of int values. + * @param propertyName The name of the property being animated. + * @param values The values that the named property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofInt(String propertyName, int... values) { + return new IntPropertyValuesHolder(propertyName, values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of int values. + * @param property The property being animated. Should not be null. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofInt(Property property, int... values) { + return new IntPropertyValuesHolder(property, values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name and + * set of float values. + * @param propertyName The name of the property being animated. + * @param values The values that the named property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofFloat(String propertyName, float... values) { + return new FloatPropertyValuesHolder(propertyName, values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of float values. + * @param property The property being animated. Should not be null. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofFloat(Property property, float... values) { + return new FloatPropertyValuesHolder(property, values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name and + * set of Object values. This variant also takes a TypeEvaluator because the system + * cannot automatically interpolate between objects of unknown type. + * + * @param propertyName The name of the property being animated. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values The values that the named property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofObject(String propertyName, TypeEvaluator evaluator, + Object... values) { + PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName); + pvh.setObjectValues(values); + pvh.setEvaluator(evaluator); + return pvh; + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of Object values. This variant also takes a TypeEvaluator because the system + * cannot automatically interpolate between objects of unknown type. + * + * @param property The property being animated. Should not be null. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofObject(Property property, + TypeEvaluator evaluator, V... values) { + PropertyValuesHolder pvh = new PropertyValuesHolder(property); + pvh.setObjectValues(values); + pvh.setEvaluator(evaluator); + return pvh; + } + + /** + * Constructs and returns a PropertyValuesHolder object with the specified property name and set + * of values. These values can be of any type, but the type should be consistent so that + * an appropriate {@link android.animation.TypeEvaluator} can be found that matches + * the common type. + *

If there is only one value, it is assumed to be the end value of an animation, + * and an initial value will be derived, if possible, by calling a getter function + * on the object. Also, if any value is null, the value will be filled in when the animation + * starts in the same way. This mechanism of automatically getting null values only works + * if the PropertyValuesHolder object is used in conjunction + * {@link com.nineoldandroids.animation.ObjectAnimator}, and with a getter function + * derived automatically from propertyName, since otherwise PropertyValuesHolder has + * no way of determining what the value should be. + * @param propertyName The name of the property associated with this set of values. This + * can be the actual property name to be used when using a ObjectAnimator object, or + * just a name used to get animated values, such as if this object is used with an + * ValueAnimator object. + * @param values The set of values to animate between. + */ + public static PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values) { + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); + if (keyframeSet instanceof IntKeyframeSet) { + return new IntPropertyValuesHolder(propertyName, (IntKeyframeSet) keyframeSet); + } else if (keyframeSet instanceof FloatKeyframeSet) { + return new FloatPropertyValuesHolder(propertyName, (FloatKeyframeSet) keyframeSet); + } + else { + PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName); + pvh.mKeyframeSet = keyframeSet; + pvh.mValueType = ((Keyframe)values[0]).getType(); + return pvh; + } + } + + /** + * Constructs and returns a PropertyValuesHolder object with the specified property and set + * of values. These values can be of any type, but the type should be consistent so that + * an appropriate {@link android.animation.TypeEvaluator} can be found that matches + * the common type. + *

If there is only one value, it is assumed to be the end value of an animation, + * and an initial value will be derived, if possible, by calling the property's + * {@link android.util.Property#get(Object)} function. + * Also, if any value is null, the value will be filled in when the animation + * starts in the same way. This mechanism of automatically getting null values only works + * if the PropertyValuesHolder object is used in conjunction with + * {@link com.nineoldandroids.animation.ObjectAnimator}, since otherwise PropertyValuesHolder has + * no way of determining what the value should be. + * @param property The property associated with this set of values. Should not be null. + * @param values The set of values to animate between. + */ + public static PropertyValuesHolder ofKeyframe(Property property, Keyframe... values) { + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); + if (keyframeSet instanceof IntKeyframeSet) { + return new IntPropertyValuesHolder(property, (IntKeyframeSet) keyframeSet); + } else if (keyframeSet instanceof FloatKeyframeSet) { + return new FloatPropertyValuesHolder(property, (FloatKeyframeSet) keyframeSet); + } + else { + PropertyValuesHolder pvh = new PropertyValuesHolder(property); + pvh.mKeyframeSet = keyframeSet; + pvh.mValueType = ((Keyframe)values[0]).getType(); + return pvh; + } + } + + /** + * Set the animated values for this object to this set of ints. + * If there is only one value, it is assumed to be the end value of an animation, + * and an initial value will be derived, if possible, by calling a getter function + * on the object. Also, if any value is null, the value will be filled in when the animation + * starts in the same way. This mechanism of automatically getting null values only works + * if the PropertyValuesHolder object is used in conjunction + * {@link com.nineoldandroids.animation.ObjectAnimator}, and with a getter function + * derived automatically from propertyName, since otherwise PropertyValuesHolder has + * no way of determining what the value should be. + * + * @param values One or more values that the animation will animate between. + */ + public void setIntValues(int... values) { + mValueType = int.class; + mKeyframeSet = KeyframeSet.ofInt(values); + } + + /** + * Set the animated values for this object to this set of floats. + * If there is only one value, it is assumed to be the end value of an animation, + * and an initial value will be derived, if possible, by calling a getter function + * on the object. Also, if any value is null, the value will be filled in when the animation + * starts in the same way. This mechanism of automatically getting null values only works + * if the PropertyValuesHolder object is used in conjunction + * {@link com.nineoldandroids.animation.ObjectAnimator}, and with a getter function + * derived automatically from propertyName, since otherwise PropertyValuesHolder has + * no way of determining what the value should be. + * + * @param values One or more values that the animation will animate between. + */ + public void setFloatValues(float... values) { + mValueType = float.class; + mKeyframeSet = KeyframeSet.ofFloat(values); + } + + /** + * Set the animated values for this object to this set of Keyframes. + * + * @param values One or more values that the animation will animate between. + */ + public void setKeyframes(Keyframe... values) { + int numKeyframes = values.length; + Keyframe keyframes[] = new Keyframe[Math.max(numKeyframes,2)]; + mValueType = ((Keyframe)values[0]).getType(); + for (int i = 0; i < numKeyframes; ++i) { + keyframes[i] = (Keyframe)values[i]; + } + mKeyframeSet = new KeyframeSet(keyframes); + } + + /** + * Set the animated values for this object to this set of Objects. + * If there is only one value, it is assumed to be the end value of an animation, + * and an initial value will be derived, if possible, by calling a getter function + * on the object. Also, if any value is null, the value will be filled in when the animation + * starts in the same way. This mechanism of automatically getting null values only works + * if the PropertyValuesHolder object is used in conjunction + * {@link com.nineoldandroids.animation.ObjectAnimator}, and with a getter function + * derived automatically from propertyName, since otherwise PropertyValuesHolder has + * no way of determining what the value should be. + * + * @param values One or more values that the animation will animate between. + */ + public void setObjectValues(Object... values) { + mValueType = values[0].getClass(); + mKeyframeSet = KeyframeSet.ofObject(values); + } + + /** + * Determine the setter or getter function using the JavaBeans convention of setFoo or + * getFoo for a property named 'foo'. This function figures out what the name of the + * function should be and uses reflection to find the Method with that name on the + * target object. + * + * @param targetClass The class to search for the method + * @param prefix "set" or "get", depending on whether we need a setter or getter. + * @param valueType The type of the parameter (in the case of a setter). This type + * is derived from the values set on this PropertyValuesHolder. This type is used as + * a first guess at the parameter type, but we check for methods with several different + * types to avoid problems with slight mis-matches between supplied values and actual + * value types used on the setter. + * @return Method the method associated with mPropertyName. + */ + private Method getPropertyFunction(Class targetClass, String prefix, Class valueType) { + // TODO: faster implementation... + Method returnVal = null; + String methodName = getMethodName(prefix, mPropertyName); + Class args[] = null; + if (valueType == null) { + try { + returnVal = targetClass.getMethod(methodName, args); + } catch (NoSuchMethodException e) { + /* The native implementation uses JNI to do reflection, which allows access to private methods. + * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback. + */ + try { + returnVal = targetClass.getDeclaredMethod(methodName, args); + returnVal.setAccessible(true); + } catch (NoSuchMethodException e2) { + Log.e("PropertyValuesHolder", + "Couldn't find no-arg method for property " + mPropertyName + ": " + e); + } + } + } else { + args = new Class[1]; + Class typeVariants[]; + if (mValueType.equals(Float.class)) { + typeVariants = FLOAT_VARIANTS; + } else if (mValueType.equals(Integer.class)) { + typeVariants = INTEGER_VARIANTS; + } else if (mValueType.equals(Double.class)) { + typeVariants = DOUBLE_VARIANTS; + } else { + typeVariants = new Class[1]; + typeVariants[0] = mValueType; + } + for (Class typeVariant : typeVariants) { + args[0] = typeVariant; + try { + returnVal = targetClass.getMethod(methodName, args); + // change the value type to suit + mValueType = typeVariant; + return returnVal; + } catch (NoSuchMethodException e) { + /* The native implementation uses JNI to do reflection, which allows access to private methods. + * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback. + */ + try { + returnVal = targetClass.getDeclaredMethod(methodName, args); + returnVal.setAccessible(true); + // change the value type to suit + mValueType = typeVariant; + return returnVal; + } catch (NoSuchMethodException e2) { + // Swallow the error and keep trying other variants + } + } + } + // If we got here, then no appropriate function was found + Log.e("PropertyValuesHolder", + "Couldn't find setter/getter for property " + mPropertyName + + " with value type "+ mValueType); + } + + return returnVal; + } + + + /** + * Returns the setter or getter requested. This utility function checks whether the + * requested method exists in the propertyMapMap cache. If not, it calls another + * utility function to request the Method from the targetClass directly. + * @param targetClass The Class on which the requested method should exist. + * @param propertyMapMap The cache of setters/getters derived so far. + * @param prefix "set" or "get", for the setter or getter. + * @param valueType The type of parameter passed into the method (null for getter). + * @return Method the method associated with mPropertyName. + */ + private Method setupSetterOrGetter(Class targetClass, + HashMap> propertyMapMap, + String prefix, Class valueType) { + Method setterOrGetter = null; + try { + // Have to lock property map prior to reading it, to guard against + // another thread putting something in there after we've checked it + // but before we've added an entry to it + mPropertyMapLock.writeLock().lock(); + HashMap propertyMap = propertyMapMap.get(targetClass); + if (propertyMap != null) { + setterOrGetter = propertyMap.get(mPropertyName); + } + if (setterOrGetter == null) { + setterOrGetter = getPropertyFunction(targetClass, prefix, valueType); + if (propertyMap == null) { + propertyMap = new HashMap(); + propertyMapMap.put(targetClass, propertyMap); + } + propertyMap.put(mPropertyName, setterOrGetter); + } + } finally { + mPropertyMapLock.writeLock().unlock(); + } + return setterOrGetter; + } + + /** + * Utility function to get the setter from targetClass + * @param targetClass The Class on which the requested method should exist. + */ + void setupSetter(Class targetClass) { + mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", mValueType); + } + + /** + * Utility function to get the getter from targetClass + */ + private void setupGetter(Class targetClass) { + mGetter = setupSetterOrGetter(targetClass, sGetterPropertyMap, "get", null); + } + + /** + * Internal function (called from ObjectAnimator) to set up the setter and getter + * prior to running the animation. If the setter has not been manually set for this + * object, it will be derived automatically given the property name, target object, and + * types of values supplied. If no getter has been set, it will be supplied iff any of the + * supplied values was null. If there is a null value, then the getter (supplied or derived) + * will be called to set those null values to the current value of the property + * on the target object. + * @param target The object on which the setter (and possibly getter) exist. + */ + void setupSetterAndGetter(Object target) { + if (mProperty != null) { + // check to make sure that mProperty is on the class of target + try { + Object testValue = mProperty.get(target); + for (Keyframe kf : mKeyframeSet.mKeyframes) { + if (!kf.hasValue()) { + kf.setValue(mProperty.get(target)); + } + } + return; + } catch (ClassCastException e) { + Log.e("PropertyValuesHolder","No such property (" + mProperty.getName() + + ") on target object " + target + ". Trying reflection instead"); + mProperty = null; + } + } + Class targetClass = target.getClass(); + if (mSetter == null) { + setupSetter(targetClass); + } + for (Keyframe kf : mKeyframeSet.mKeyframes) { + if (!kf.hasValue()) { + if (mGetter == null) { + setupGetter(targetClass); + } + try { + kf.setValue(mGetter.invoke(target)); + } catch (InvocationTargetException e) { + Log.e("PropertyValuesHolder", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyValuesHolder", e.toString()); + } + } + } + } + + /** + * Utility function to set the value stored in a particular Keyframe. The value used is + * whatever the value is for the property name specified in the keyframe on the target object. + * + * @param target The target object from which the current value should be extracted. + * @param kf The keyframe which holds the property name and value. + */ + private void setupValue(Object target, Keyframe kf) { + if (mProperty != null) { + kf.setValue(mProperty.get(target)); + } + try { + if (mGetter == null) { + Class targetClass = target.getClass(); + setupGetter(targetClass); + } + kf.setValue(mGetter.invoke(target)); + } catch (InvocationTargetException e) { + Log.e("PropertyValuesHolder", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyValuesHolder", e.toString()); + } + } + + /** + * This function is called by ObjectAnimator when setting the start values for an animation. + * The start values are set according to the current values in the target object. The + * property whose value is extracted is whatever is specified by the propertyName of this + * PropertyValuesHolder object. + * + * @param target The object which holds the start values that should be set. + */ + void setupStartValue(Object target) { + setupValue(target, mKeyframeSet.mKeyframes.get(0)); + } + + /** + * This function is called by ObjectAnimator when setting the end values for an animation. + * The end values are set according to the current values in the target object. The + * property whose value is extracted is whatever is specified by the propertyName of this + * PropertyValuesHolder object. + * + * @param target The object which holds the start values that should be set. + */ + void setupEndValue(Object target) { + setupValue(target, mKeyframeSet.mKeyframes.get(mKeyframeSet.mKeyframes.size() - 1)); + } + + @Override + public PropertyValuesHolder clone() { + try { + PropertyValuesHolder newPVH = (PropertyValuesHolder) super.clone(); + newPVH.mPropertyName = mPropertyName; + newPVH.mProperty = mProperty; + newPVH.mKeyframeSet = mKeyframeSet.clone(); + newPVH.mEvaluator = mEvaluator; + return newPVH; + } catch (CloneNotSupportedException e) { + // won't reach here + return null; + } + } + + /** + * Internal function to set the value on the target object, using the setter set up + * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator + * to handle turning the value calculated by ValueAnimator into a value set on the object + * according to the name of the property. + * @param target The target object on which the value is set + */ + void setAnimatedValue(Object target) { + if (mProperty != null) { + mProperty.set(target, getAnimatedValue()); + } + if (mSetter != null) { + try { + mTmpValueArray[0] = getAnimatedValue(); + mSetter.invoke(target, mTmpValueArray); + } catch (InvocationTargetException e) { + Log.e("PropertyValuesHolder", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyValuesHolder", e.toString()); + } + } + } + + /** + * Internal function, called by ValueAnimator, to set up the TypeEvaluator that will be used + * to calculate animated values. + */ + void init() { + if (mEvaluator == null) { + // We already handle int and float automatically, but not their Object + // equivalents + mEvaluator = (mValueType == Integer.class) ? sIntEvaluator : + (mValueType == Float.class) ? sFloatEvaluator : + null; + } + if (mEvaluator != null) { + // KeyframeSet knows how to evaluate the common types - only give it a custom + // evaluator if one has been set on this class + mKeyframeSet.setEvaluator(mEvaluator); + } + } + + /** + * The TypeEvaluator will the automatically determined based on the type of values + * supplied to PropertyValuesHolder. The evaluator can be manually set, however, if so + * desired. This may be important in cases where either the type of the values supplied + * do not match the way that they should be interpolated between, or if the values + * are of a custom type or one not currently understood by the animation system. Currently, + * only values of type float and int (and their Object equivalents: Float + * and Integer) are correctly interpolated; all other types require setting a TypeEvaluator. + * @param evaluator + */ + public void setEvaluator(TypeEvaluator evaluator) { + mEvaluator = evaluator; + mKeyframeSet.setEvaluator(evaluator); + } + + /** + * Function used to calculate the value according to the evaluator set up for + * this PropertyValuesHolder object. This function is called by ValueAnimator.animateValue(). + * + * @param fraction The elapsed, interpolated fraction of the animation. + */ + void calculateValue(float fraction) { + mAnimatedValue = mKeyframeSet.getValue(fraction); + } + + /** + * 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. + * + *

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.

+ * + * @param propertyName The name of the property being animated. + */ + public void setPropertyName(String propertyName) { + mPropertyName = propertyName; + } + + /** + * Sets the property that will be animated. + * + *

Note that if this PropertyValuesHolder object is used with ObjectAnimator, the property + * must exist on the target object specified in that ObjectAnimator.

+ * + * @param property The property being animated. + */ + public void setProperty(Property property) { + mProperty = property; + } + + /** + * Gets the name of the property that will be animated. This name will be used to derive + * a setter function that will be called to set animated values. + * For example, a property name of 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. + */ + public String getPropertyName() { + return mPropertyName; + } + + /** + * Internal function, called by ValueAnimator and ObjectAnimator, to retrieve the value + * most recently calculated in calculateValue(). + * @return + */ + Object getAnimatedValue() { + return mAnimatedValue; + } + + @Override + public String toString() { + return mPropertyName + ": " + mKeyframeSet.toString(); + } + + /** + * Utility method to derive a setter/getter method name from a property name, where the + * prefix is typically "set" or "get" and the first letter of the property name is + * capitalized. + * + * @param prefix The precursor to the method name, before the property name begins, typically + * "set" or "get". + * @param propertyName The name of the property that represents the bulk of the method name + * after the prefix. The first letter of this word will be capitalized in the resulting + * method name. + * @return String the property name converted to a method name according to the conventions + * specified above. + */ + static String getMethodName(String prefix, String propertyName) { + if (propertyName == null || propertyName.length() == 0) { + // shouldn't get here + return prefix; + } + char firstLetter = Character.toUpperCase(propertyName.charAt(0)); + String theRest = propertyName.substring(1); + return prefix + firstLetter + theRest; + } + + static class IntPropertyValuesHolder extends PropertyValuesHolder { + + // Cache JNI functions to avoid looking them up twice + //private static final HashMap> sJNISetterPropertyMap = + // new HashMap>(); + //int mJniSetter; + private IntProperty mIntProperty; + + IntKeyframeSet mIntKeyframeSet; + int mIntAnimatedValue; + + public IntPropertyValuesHolder(String propertyName, IntKeyframeSet keyframeSet) { + super(propertyName); + mValueType = int.class; + mKeyframeSet = keyframeSet; + mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; + } + + public IntPropertyValuesHolder(Property property, IntKeyframeSet keyframeSet) { + super(property); + mValueType = int.class; + mKeyframeSet = keyframeSet; + mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; + if (property instanceof IntProperty) { + mIntProperty = (IntProperty) mProperty; + } + } + + public IntPropertyValuesHolder(String propertyName, int... values) { + super(propertyName); + setIntValues(values); + } + + public IntPropertyValuesHolder(Property property, int... values) { + super(property); + setIntValues(values); + if (property instanceof IntProperty) { + mIntProperty = (IntProperty) mProperty; + } + } + + @Override + public void setIntValues(int... values) { + super.setIntValues(values); + mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; + } + + @Override + void calculateValue(float fraction) { + mIntAnimatedValue = mIntKeyframeSet.getIntValue(fraction); + } + + @Override + Object getAnimatedValue() { + return mIntAnimatedValue; + } + + @Override + public IntPropertyValuesHolder clone() { + IntPropertyValuesHolder newPVH = (IntPropertyValuesHolder) super.clone(); + newPVH.mIntKeyframeSet = (IntKeyframeSet) newPVH.mKeyframeSet; + return newPVH; + } + + /** + * Internal function to set the value on the target object, using the setter set up + * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator + * to handle turning the value calculated by ValueAnimator into a value set on the object + * according to the name of the property. + * @param target The target object on which the value is set + */ + @Override + void setAnimatedValue(Object target) { + if (mIntProperty != null) { + mIntProperty.setValue(target, mIntAnimatedValue); + return; + } + if (mProperty != null) { + mProperty.set(target, mIntAnimatedValue); + return; + } + //if (mJniSetter != 0) { + // nCallIntMethod(target, mJniSetter, mIntAnimatedValue); + // return; + //} + if (mSetter != null) { + try { + mTmpValueArray[0] = mIntAnimatedValue; + mSetter.invoke(target, mTmpValueArray); + } catch (InvocationTargetException e) { + Log.e("PropertyValuesHolder", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyValuesHolder", e.toString()); + } + } + } + + @Override + void setupSetter(Class targetClass) { + if (mProperty != null) { + return; + } + // Check new static hashmap for setter method + //try { + // mPropertyMapLock.writeLock().lock(); + // HashMap propertyMap = sJNISetterPropertyMap.get(targetClass); + // if (propertyMap != null) { + // Integer mJniSetterInteger = propertyMap.get(mPropertyName); + // if (mJniSetterInteger != null) { + // mJniSetter = mJniSetterInteger; + // } + // } + // if (mJniSetter == 0) { + // String methodName = getMethodName("set", mPropertyName); + // mJniSetter = nGetIntMethod(targetClass, methodName); + // if (mJniSetter != 0) { + // if (propertyMap == null) { + // propertyMap = new HashMap(); + // sJNISetterPropertyMap.put(targetClass, propertyMap); + // } + // propertyMap.put(mPropertyName, mJniSetter); + // } + // } + //} catch (NoSuchMethodError e) { + // Log.d("PropertyValuesHolder", + // "Can't find native method using JNI, use reflection" + e); + //} finally { + // mPropertyMapLock.writeLock().unlock(); + //} + //if (mJniSetter == 0) { + // Couldn't find method through fast JNI approach - just use reflection + super.setupSetter(targetClass); + //} + } + } + + static class FloatPropertyValuesHolder extends PropertyValuesHolder { + + // Cache JNI functions to avoid looking them up twice + //private static final HashMap> sJNISetterPropertyMap = + // new HashMap>(); + //int mJniSetter; + private FloatProperty mFloatProperty; + + FloatKeyframeSet mFloatKeyframeSet; + float mFloatAnimatedValue; + + public FloatPropertyValuesHolder(String propertyName, FloatKeyframeSet keyframeSet) { + super(propertyName); + mValueType = float.class; + mKeyframeSet = keyframeSet; + mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; + } + + public FloatPropertyValuesHolder(Property property, FloatKeyframeSet keyframeSet) { + super(property); + mValueType = float.class; + mKeyframeSet = keyframeSet; + mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; + if (property instanceof FloatProperty) { + mFloatProperty = (FloatProperty) mProperty; + } + } + + public FloatPropertyValuesHolder(String propertyName, float... values) { + super(propertyName); + setFloatValues(values); + } + + public FloatPropertyValuesHolder(Property property, float... values) { + super(property); + setFloatValues(values); + if (property instanceof FloatProperty) { + mFloatProperty = (FloatProperty) mProperty; + } + } + + @Override + public void setFloatValues(float... values) { + super.setFloatValues(values); + mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; + } + + @Override + void calculateValue(float fraction) { + mFloatAnimatedValue = mFloatKeyframeSet.getFloatValue(fraction); + } + + @Override + Object getAnimatedValue() { + return mFloatAnimatedValue; + } + + @Override + public FloatPropertyValuesHolder clone() { + FloatPropertyValuesHolder newPVH = (FloatPropertyValuesHolder) super.clone(); + newPVH.mFloatKeyframeSet = (FloatKeyframeSet) newPVH.mKeyframeSet; + return newPVH; + } + + /** + * Internal function to set the value on the target object, using the setter set up + * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator + * to handle turning the value calculated by ValueAnimator into a value set on the object + * according to the name of the property. + * @param target The target object on which the value is set + */ + @Override + void setAnimatedValue(Object target) { + if (mFloatProperty != null) { + mFloatProperty.setValue(target, mFloatAnimatedValue); + return; + } + if (mProperty != null) { + mProperty.set(target, mFloatAnimatedValue); + return; + } + //if (mJniSetter != 0) { + // nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue); + // return; + //} + if (mSetter != null) { + try { + mTmpValueArray[0] = mFloatAnimatedValue; + mSetter.invoke(target, mTmpValueArray); + } catch (InvocationTargetException e) { + Log.e("PropertyValuesHolder", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyValuesHolder", e.toString()); + } + } + } + + @Override + void setupSetter(Class targetClass) { + if (mProperty != null) { + return; + } + // Check new static hashmap for setter method + //try { + // mPropertyMapLock.writeLock().lock(); + // HashMap propertyMap = sJNISetterPropertyMap.get(targetClass); + // if (propertyMap != null) { + // Integer mJniSetterInteger = propertyMap.get(mPropertyName); + // if (mJniSetterInteger != null) { + // mJniSetter = mJniSetterInteger; + // } + // } + // if (mJniSetter == 0) { + // String methodName = getMethodName("set", mPropertyName); + // mJniSetter = nGetFloatMethod(targetClass, methodName); + // if (mJniSetter != 0) { + // if (propertyMap == null) { + // propertyMap = new HashMap(); + // sJNISetterPropertyMap.put(targetClass, propertyMap); + // } + // propertyMap.put(mPropertyName, mJniSetter); + // } + // } + //} catch (NoSuchMethodError e) { + // Log.d("PropertyValuesHolder", + // "Can't find native method using JNI, use reflection" + e); + //} finally { + // mPropertyMapLock.writeLock().unlock(); + //} + //if (mJniSetter == 0) { + // Couldn't find method through fast JNI approach - just use reflection + super.setupSetter(targetClass); + //} + } + + } + + //native static private int nGetIntMethod(Class targetClass, String methodName); + //native static private int nGetFloatMethod(Class targetClass, String methodName); + //native static private void nCallIntMethod(Object target, int methodID, int arg); + //native static private void nCallFloatMethod(Object target, int methodID, float arg); +} diff --git a/app/src/main/java/com/nineoldandroids/animation/TimeAnimator.java b/app/src/main/java/com/nineoldandroids/animation/TimeAnimator.java new file mode 100644 index 000000000..7c5006311 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/TimeAnimator.java @@ -0,0 +1,78 @@ +package com.nineoldandroids.animation; + +/** + * This class provides a simple callback mechanism to listeners that is synchronized with other + * animators in the system. There is no duration, interpolation, or object value-setting + * with this Animator. Instead, it is simply started and proceeds to send out events on every + * animation frame to its TimeListener (if set), with information about this animator, + * the total elapsed time, and the time since the last animation frame. + * + * @hide + */ +public class TimeAnimator extends ValueAnimator { + + private TimeListener mListener; + private long mPreviousTime = -1; + + @Override + boolean animationFrame(long currentTime) { + 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; + } + } + if (mListener != null) { + long totalTime = currentTime - mStartTime; + long deltaTime = (mPreviousTime < 0) ? 0 : (currentTime - mPreviousTime); + mPreviousTime = currentTime; + mListener.onTimeUpdate(this, totalTime, deltaTime); + } + return false; + } + + /** + * Sets a listener that is sent update events throughout the life of + * an animation. + * + * @param listener the listener to be set. + */ + public void setTimeListener(TimeListener listener) { + mListener = listener; + } + + @Override + void animateValue(float fraction) { + // Noop + } + + @Override + void initAnimation() { + // noop + } + + /** + * Implementors of this interface can set themselves as update listeners + * to a TimeAnimator instance to receive callbacks on every animation + * frame to receive the total time since the animator started and the delta time + * since the last frame. The first time the listener is called, totalTime and + * deltaTime should both be zero. + * + * @hide + */ + public static interface TimeListener { + /** + *

Notifies listeners of the occurrence of another frame of the animation, + * along with information about the elapsed time.

+ * + * @param animation The animator sending out the notification. + * @param totalTime The + */ + void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime); + + } +} diff --git a/app/src/main/java/com/nineoldandroids/animation/TypeEvaluator.java b/app/src/main/java/com/nineoldandroids/animation/TypeEvaluator.java new file mode 100644 index 000000000..7393784ea --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/TypeEvaluator.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nineoldandroids.animation; + +/** + * Interface for use with the {@link ValueAnimator#setEvaluator(com.nineoldandroids.animation.TypeEvaluator)} function. Evaluators + * allow developers to create animations on arbitrary property types, by allowing them to supply + * custom evaulators for types that are not automatically understood and used by the animation + * system. + * + * @see ValueAnimator#setEvaluator(com.nineoldandroids.animation.TypeEvaluator) + */ +public interface TypeEvaluator { + + /** + * This function returns the result of linearly interpolating the start and end values, with + * fraction representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0), + * where x0 is startValue, x1 is endValue, + * and t is fraction. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value. + * @param endValue The end value. + * @return A linear interpolation between the start and end values, given the + * fraction parameter. + */ + public T evaluate(float fraction, T startValue, T endValue); + +} \ No newline at end of file diff --git a/app/src/main/java/com/nineoldandroids/animation/ValueAnimator.java b/app/src/main/java/com/nineoldandroids/animation/ValueAnimator.java new file mode 100644 index 000000000..8d47bd3f8 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/animation/ValueAnimator.java @@ -0,0 +1,1265 @@ +/* + * 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 com.nineoldandroids.animation; + + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.AndroidRuntimeException; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +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, 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(Interpolator)}.

+ */ +public class ValueAnimator extends Animator { + + /** + * Internal constants + */ + + /* + * The default amount of time in ms between animation frames + */ + private static final long DEFAULT_FRAME_DELAY = 10; + + /** + * Messages sent to timing handler: START is sent when an animation first begins, FRAME is sent + * by the handler to itself to process the next animation frame + */ + static final int ANIMATION_START = 0; + static final int ANIMATION_FRAME = 1; + + /** + * Values used with internal variable mPlayingState to indicate the current state of an + * animation. + */ + static final int STOPPED = 0; // Not yet playing + static final int RUNNING = 1; // Playing normally + static final int SEEKED = 2; // Seeked to some time value + + /** + * 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() + long mStartTime; + + /** + * Set when setCurrentPlayTime() is called. If negative, animation is not currently seeked + * to a value. + */ + long mSeekTime = -1; + + // TODO: We access the following ThreadLocal variables often, some of them on every update. + // If ThreadLocal access is significantly expensive, we may want to put all of these + // fields into a structure sot hat we just access ThreadLocal once to get the reference + // to that structure, then access the structure directly for each field. + + // The static sAnimationHandler processes the internal timing loop on which all animations + // are based + protected static ThreadLocal sAnimationHandler = + new ThreadLocal(); + + // The per-thread list of all active animations + protected static final ThreadLocal> sAnimations = + new ThreadLocal>() { + @Override + protected ArrayList initialValue() { + return new ArrayList(); + } + }; + + // The per-thread set of animations to be started on the next animation frame + protected static final ThreadLocal> sPendingAnimations = + new ThreadLocal>() { + @Override + protected ArrayList initialValue() { + return new ArrayList(); + } + }; + + /** + * Internal per-thread collections used to avoid set collisions as animations start and end + * while being processed. + */ + protected static final ThreadLocal> sDelayedAnims = + new ThreadLocal>() { + @Override + protected ArrayList initialValue() { + return new ArrayList(); + } + }; + + protected static final ThreadLocal> sEndingAnims = + new ThreadLocal>() { + @Override + protected ArrayList initialValue() { + return new ArrayList(); + } + }; + + protected static final ThreadLocal> sReadyAnims = + new ThreadLocal>() { + @Override + protected ArrayList initialValue() { + return new ArrayList(); + } + }; + + // The time interpolator to be used if none is set on the animation + private static final android.view.animation.Interpolator sDefaultInterpolator = + new AccelerateDecelerateInterpolator(); + + // type evaluators for the primitive types handled by this implementation + private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); + private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); + + /** + * Used to indicate whether the animation is currently playing in reverse. This causes the + * elapsed fraction to be inverted to calculate the appropriate values. + */ + private boolean mPlayingBackwards = false; + + /** + * This variable tracks the current iteration that is playing. When mCurrentIteration exceeds the + * repeatCount (if repeatCount!=INFINITE), the animation ends + */ + private int mCurrentIteration = 0; + + /** + * Tracks current elapsed/eased fraction, for querying in getAnimatedFraction(). + */ + private float mCurrentFraction = 0f; + + /** + * Tracks whether a startDelay'd animation has begun playing through the startDelay. + */ + private boolean mStartedDelay = false; + + /** + * Tracks the time at which the animation began playing through its startDelay. This is + * different from the mStartTime variable, which is used to track when the animation became + * active (which is when the startDelay expired and the animation was added to the active + * animations list). + */ + private long mDelayStartTime; + + /** + * Flag that represents the current state of the animation. Used to figure out when to start + * an animation (if state == STOPPED). Also used to end an animation that + * has been cancel()'d or end()'d since the last animation frame. Possible values are + * STOPPED, RUNNING, SEEKED. + */ + int mPlayingState = STOPPED; + + /** + * 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; + + // + // 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 + private long mStartDelay = 0; + + // The number of milliseconds between animation frames + private static long sFrameDelay = DEFAULT_FRAME_DELAY; + + // 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; + + /** + * 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 Interpolator mInterpolator = sDefaultInterpolator; + + /** + * The set of listeners to be sent events through the life of an animation. + */ + private 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; + + /** + * Public constants + */ + + /** + * 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; + + /** + * Creates a new ValueAnimator object. This default constructor is primarily for + * use internally; the factory methods which take parameters are more generally + * useful. + */ + public ValueAnimator() { + } + + /** + * Constructs and returns a ValueAnimator that animates between int values. A single + * value implies that that value is the one being animated to. However, this is not typically + * useful in a ValueAnimator object because there is no way for the object to determine the + * starting value for the animation (unlike ObjectAnimator, which can derive that value + * from the target object and property being animated). Therefore, there should typically + * be two or more values. + * + * @param values A set of values that the animation will animate between over time. + * @return A ValueAnimator object that is set up to animate between the given values. + */ + public static ValueAnimator ofInt(int... values) { + ValueAnimator anim = new ValueAnimator(); + anim.setIntValues(values); + return anim; + } + + /** + * Constructs and returns a ValueAnimator that animates between 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. + * + *

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(new PropertyValuesHolder[]{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(new PropertyValuesHolder[]{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. + * + *

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(new PropertyValuesHolder[]{PropertyValuesHolder.ofObject("", + (TypeEvaluator)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 an 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 = (PropertyValuesHolder) values[i]; + mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder); + } + // New property/values/target should cause re-initialization prior to starting + mInitialized = false; + } + + /** + * Returns the values that this ValueAnimator animates between. These values are stored in + * PropertyValuesHolder objects, even if the ValueAnimator was created with a simple list + * of value objects instead. + * + * @return PropertyValuesHolder[] An array of PropertyValuesHolder objects which hold the + * values, per property, that define the animation. + */ + public PropertyValuesHolder[] getValues() { + return mValues; + } + + /** + * This function is called immediately before processing the first animation + * frame of an animation. If there is a nonzero 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) { + 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(). + */ + public ValueAnimator setDuration(long duration) { + if (duration < 0) { + throw new IllegalArgumentException("Animators cannot have negative duration: " + + duration); + } + mDuration = duration; + return this; + } + + /** + * Gets the length of the animation. The default duration is 300 milliseconds. + * + * @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) { + 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. + * + */ + protected 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; + ArrayList animations = sAnimations.get(); + ArrayList delayedAnims = sDelayedAnims.get(); + switch (msg.what) { + // TODO: should we avoid sending frame message when starting if we + // were already running? + case ANIMATION_START: + ArrayList pendingAnimations = sPendingAnimations.get(); + if (animations.size() > 0 || delayedAnims.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 (pendingAnimations.size() > 0) { + ArrayList pendingCopy = + (ArrayList) pendingAnimations.clone(); + pendingAnimations.clear(); + int count = pendingCopy.size(); + for (int i = 0; i < count; ++i) { + ValueAnimator anim = pendingCopy.get(i); + // If the animation has a startDelay, place it on the delayed list + if (anim.mStartDelay == 0) { + anim.startAnimation(); + } else { + delayedAnims.add(anim); + } + } + } + // fall through to process first frame of new animations + case ANIMATION_FRAME: + // currentTime holds the common time for all animations processed + // during this frame + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + ArrayList readyAnims = sReadyAnims.get(); + ArrayList endingAnims = sEndingAnims.get(); + + // First, process animations currently sitting on the delayed queue, adding + // them to the active animations if they are ready + int numDelayedAnims = delayedAnims.size(); + for (int i = 0; i < numDelayedAnims; ++i) { + ValueAnimator anim = delayedAnims.get(i); + if (anim.delayedAnimationFrame(currentTime)) { + readyAnims.add(anim); + } + } + int numReadyAnims = readyAnims.size(); + if (numReadyAnims > 0) { + for (int i = 0; i < numReadyAnims; ++i) { + ValueAnimator anim = readyAnims.get(i); + anim.startAnimation(); + anim.mRunning = true; + delayedAnims.remove(anim); + } + readyAnims.clear(); + } + + // Now process all active animations. The return value from animationFrame() + // tells the handler whether it should now be ended + int numAnims = animations.size(); + int i = 0; + while (i < numAnims) { + ValueAnimator anim = animations.get(i); + if (anim.animationFrame(currentTime)) { + endingAnims.add(anim); + } + if (animations.size() == numAnims) { + ++i; + } else { + // An animation might be canceled or ended by client code + // during the animation frame. Check to see if this happened by + // seeing whether the current index is the same as it was before + // calling animationFrame(). Another approach would be to copy + // animations to a temporary list and process that list instead, + // but that entails garbage and processing overhead that would + // be nice to avoid. + --numAnims; + endingAnims.remove(anim); + } + } + if (endingAnims.size() > 0) { + for (i = 0; i < endingAnims.size(); ++i) { + endingAnims.get(i).endAnimation(); + } + endingAnims.clear(); + } + + // If there are still active or delayed animations, call the handler again + // after the frameDelay + if (callAgain && (!animations.isEmpty() || !delayedAnims.isEmpty())) { + sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay - + (AnimationUtils.currentAnimationTimeMillis() - currentTime))); + } + break; + } + } + } + + /** + * 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 + */ + public long getStartDelay() { + return mStartDelay; + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + + * @param startDelay The amount of the delay, in milliseconds + */ + public void setStartDelay(long startDelay) { + this.mStartDelay = startDelay; + } + + /** + * The amount of time, in milliseconds, between each frame of the animation. This is a + * requested time that the animation will attempt to honor, but the actual delay between + * frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + * + * @return the requested time between frames, in milliseconds + */ + public static long getFrameDelay() { + return sFrameDelay; + } + + /** + * The amount of time, in milliseconds, between each frame of the animation. This is a + * requested time that the animation will attempt to honor, but the actual delay between + * frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + * + * @param frameDelay the requested time between frames, in milliseconds + */ + public static void setFrameDelay(long frameDelay) { + sFrameDelay = frameDelay; + } + + /** + * 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; + } + + /** + * 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; + } + } + + /** + * 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 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(Interpolator 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. + */ + public Interpolator 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"); + } + mPlayingBackwards = playBackwards; + mCurrentIteration = 0; + mPlayingState = STOPPED; + mStarted = true; + mStartedDelay = false; + sPendingAnimations.get().add(this); + if (mStartDelay == 0) { + // This sets the initial value of the animation, prior to actually starting it running + setCurrentPlayTime(getCurrentPlayTime()); + mPlayingState = STOPPED; + mRunning = true; + + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationStart(this); + } + } + } + AnimationHandler animationHandler = sAnimationHandler.get(); + if (animationHandler == null) { + animationHandler = new AnimationHandler(); + sAnimationHandler.set(animationHandler); + } + animationHandler.sendEmptyMessage(ANIMATION_START); + } + + @Override + public void start() { + start(false); + } + + @Override + public void cancel() { + // Only cancel if the animation is actually running or has been started and is about + // to run + if (mPlayingState != STOPPED || sPendingAnimations.get().contains(this) || + sDelayedAnims.get().contains(this)) { + // Only notify listeners if the animator has actually started + if (mRunning && mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationCancel(this); + } + } + endAnimation(); + } + } + + @Override + public void end() { + if (!sAnimations.get().contains(this) && !sPendingAnimations.get().contains(this)) { + // Special case if the animation has not yet started; get it ready for ending + mStartedDelay = false; + startAnimation(); + } else if (!mInitialized) { + initAnimation(); + } + // 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); + } + endAnimation(); + } + + @Override + public boolean isRunning() { + return (mPlayingState == RUNNING || 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. + */ + public void reverse() { + mPlayingBackwards = !mPlayingBackwards; + if (mPlayingState == RUNNING) { + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + long currentPlayTime = currentTime - mStartTime; + long timeLeft = mDuration - currentPlayTime; + mStartTime = currentTime - timeLeft; + } else { + start(true); + } + } + + /** + * Called internally to end an animation by removing it from the animations list. Must be + * called on the UI thread. + */ + private void endAnimation() { + sAnimations.get().remove(this); + sPendingAnimations.get().remove(this); + sDelayedAnims.get().remove(this); + mPlayingState = STOPPED; + if (mRunning && mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationEnd(this); + } + } + mRunning = false; + mStarted = false; + } + + /** + * Called internally to start an animation by adding it to the active animations list. Must be + * called on the UI thread. + */ + private void startAnimation() { + initAnimation(); + sAnimations.get().add(this); + if (mStartDelay > 0 && mListeners != null) { + // Listeners were already notified in start() if startDelay is 0; this is + // just for delayed animations + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationStart(this); + } + } + } + + /** + * Internal function called to process an animation frame on an animation that is currently + * sleeping through its startDelay 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. + */ + 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 = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f; + if (fraction >= 1f) { + if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) { + // Time to repeat + if (mListeners != null) { + int numListeners = mListeners.size(); + for (int i = 0; i < numListeners; ++i) { + mListeners.get(i).onAnimationRepeat(this); + } + } + if (mRepeatMode == REVERSE) { + mPlayingBackwards = mPlayingBackwards ? false : true; + } + mCurrentIteration += (int)fraction; + fraction = fraction % 1f; + mStartTime += mDuration; + } else { + done = true; + fraction = Math.min(fraction, 1.0f); + } + } + if (mPlayingBackwards) { + fraction = 1f - fraction; + } + animateValue(fraction); + break; + } + + return done; + } + + /** + * Returns the current animation fraction, which is the elapsed/interpolated fraction used in + * the most recent frame update on the animation. + * + * @return Elapsed/interpolated fraction of the animation. + */ + public float getAnimatedFraction() { + return mCurrentFraction; + } + + /** + * This method is called with the elapsed fraction of the animation during every + * animation frame. This function turns the elapsed fraction into an interpolated fraction + * and then into an animated value (from the evaluator. The function is called mostly during + * animation updates, but it is also called when the 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); + mCurrentFraction = fraction; + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].calculateValue(fraction); + } + if (mUpdateListeners != null) { + int numListeners = mUpdateListeners.size(); + for (int i = 0; i < numListeners; ++i) { + mUpdateListeners.get(i).onAnimationUpdate(this); + } + } + } + + @Override + public ValueAnimator clone() { + final ValueAnimator anim = (ValueAnimator) super.clone(); + if (mUpdateListeners != null) { + ArrayList oldListeners = mUpdateListeners; + anim.mUpdateListeners = new ArrayList(); + int numListeners = oldListeners.size(); + for (int i = 0; i < numListeners; ++i) { + anim.mUpdateListeners.add(oldListeners.get(i)); + } + } + anim.mSeekTime = -1; + anim.mPlayingBackwards = false; + anim.mCurrentIteration = 0; + anim.mInitialized = false; + anim.mPlayingState = STOPPED; + anim.mStartedDelay = 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.getPropertyName(), newValuesHolder); + } + } + return anim; + } + + /** + * Implementors of this interface can add themselves as update listeners + * to an ValueAnimator instance to receive callbacks on every animation + * frame, after the current frame's values have been calculated for that + * ValueAnimator. + */ + public static interface AnimatorUpdateListener { + /** + *

Notifies the occurrence of another frame of the animation.

+ * + * @param animation The animation which was repeated. + */ + void onAnimationUpdate(ValueAnimator animation); + + } + + /** + * Return the number of animations currently running. + * + * Used by StrictMode internally to annotate violations. Only + * called on the main thread. + * + * @hide + */ + public static int getCurrentAnimationsCount() { + return sAnimations.get().size(); + } + + /** + * Clear all animations on this thread, without canceling or ending them. + * This should be used with caution. + * + * @hide + */ + public static void clearAllAnimations() { + sAnimations.get().clear(); + sPendingAnimations.get().clear(); + sDelayedAnims.get().clear(); + } + + @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; + } +} diff --git a/app/src/main/java/com/nineoldandroids/util/FloatProperty.java b/app/src/main/java/com/nineoldandroids/util/FloatProperty.java new file mode 100644 index 000000000..ba3f48f09 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/util/FloatProperty.java @@ -0,0 +1,46 @@ +/* + * 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.util; + +/** + * An implementation of {@link android.util.Property} to be used specifically with fields of type + * float. This type-specific subclass enables performance benefit by allowing + * calls to a {@link #set(Object, Float) set()} function that takes the primitive + * float type and avoids autoboxing and other overhead associated with the + * Float class. + * + * @param The class on which the Property is declared. + * + * @hide + */ +public abstract class FloatProperty extends Property { + + public FloatProperty(String name) { + super(Float.class, name); + } + + /** + * A type-specific override of the {@link #set(Object, Float)} that is faster when dealing + * with fields of type float. + */ + public abstract void setValue(T object, float value); + + @Override + final public void set(T object, Float value) { + setValue(object, value); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/nineoldandroids/util/IntProperty.java b/app/src/main/java/com/nineoldandroids/util/IntProperty.java new file mode 100644 index 000000000..73d155d2a --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/util/IntProperty.java @@ -0,0 +1,46 @@ +/* + * 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.util; + +/** + * An implementation of {@link android.util.Property} to be used specifically with fields of type + * int. This type-specific subclass enables performance benefit by allowing + * calls to a {@link #set(Object, Integer) set()} function that takes the primitive + * int type and avoids autoboxing and other overhead associated with the + * Integer class. + * + * @param The class on which the Property is declared. + * + * @hide + */ +public abstract class IntProperty extends Property { + + public IntProperty(String name) { + super(Integer.class, name); + } + + /** + * A type-specific override of the {@link #set(Object, Integer)} that is faster when dealing + * with fields of type int. + */ + public abstract void setValue(T object, int value); + + @Override + final public void set(T object, Integer value) { + set(object, value.intValue()); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/nineoldandroids/util/NoSuchPropertyException.java b/app/src/main/java/com/nineoldandroids/util/NoSuchPropertyException.java new file mode 100644 index 000000000..02fa234ab --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/util/NoSuchPropertyException.java @@ -0,0 +1,30 @@ +/* + * 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.util; + +/** + * Thrown when code requests a {@link com.nineoldandroids.util.Property} on a class that does + * not expose the appropriate method or field. + * + * @see com.nineoldandroids.util.Property#of(Class, Class, String) + */ +public class NoSuchPropertyException extends RuntimeException { + + public NoSuchPropertyException(String s) { + super(s); + } + +} diff --git a/app/src/main/java/com/nineoldandroids/util/Property.java b/app/src/main/java/com/nineoldandroids/util/Property.java new file mode 100644 index 000000000..1e108722a --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/util/Property.java @@ -0,0 +1,106 @@ +/* + * 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.util; + + +/** + * A property is an abstraction that can be used to represent a mutable value that is held + * in a host object. The Property's {@link #set(Object, Object)} or {@link #get(Object)} + * methods can be implemented in terms of the private fields of the host object, or via "setter" and + * "getter" methods or by some other mechanism, as appropriate. + * + * @param The class on which the property is declared. + * @param The type that this property represents. + */ +public abstract class Property { + + private final String mName; + private final Class mType; + + /** + * This factory method creates and returns a Property given the class and + * name parameters, where the "name" parameter represents either: + *
    + *
  • a public getName() method on the class which takes no arguments, plus an + * optional public setName() method which takes a value of the same type + * returned by getName() + *
  • a public isName() method on the class which takes no arguments, plus an + * optional public setName() method which takes a value of the same type + * returned by isName() + *
  • a public name field on the class + *
+ * + *

If either of the get/is method alternatives is found on the class, but an appropriate + * setName() method is not found, the Property will be + * {@link #isReadOnly() readOnly}. Calling the {@link #set(Object, Object)} method on such + * a property is allowed, but will have no effect.

+ * + *

If neither the methods nor the field are found on the class a + * {@link NoSuchPropertyException} exception will be thrown.

+ */ + public static Property of(Class hostType, Class valueType, String name) { + return new ReflectiveProperty(hostType, valueType, name); + } + + /** + * A constructor that takes an identifying name and {@link #getType() type} for the property. + */ + public Property(Class type, String name) { + mName = name; + mType = type; + } + + /** + * Returns true if the {@link #set(Object, Object)} method does not set the value on the target + * object (in which case the {@link #set(Object, Object) set()} method should throw a {@link + * NoSuchPropertyException} exception). This may happen if the Property wraps functionality that + * allows querying the underlying value but not setting it. For example, the {@link #of(Class, + * Class, String)} factory method may return a Property with name "foo" for an object that has + * only a getFoo() or isFoo() method, but no matching + * setFoo() method. + */ + public boolean isReadOnly() { + return false; + } + + /** + * Sets the value on object which this property represents. If the method is unable + * to set the value on the target object it will throw an {@link UnsupportedOperationException} + * exception. + */ + public void set(T object, V value) { + throw new UnsupportedOperationException("Property " + getName() +" is read-only"); + } + + /** + * Returns the current value that this property represents on the given object. + */ + public abstract V get(T object); + + /** + * Returns the name for this property. + */ + public String getName() { + return mName; + } + + /** + * Returns the type for this property. + */ + public Class getType() { + return mType; + } +} diff --git a/app/src/main/java/com/nineoldandroids/util/ReflectiveProperty.java b/app/src/main/java/com/nineoldandroids/util/ReflectiveProperty.java new file mode 100644 index 000000000..4b779c15f --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/util/ReflectiveProperty.java @@ -0,0 +1,182 @@ +/* + * 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.util; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Internal class to automatically generate a Property for a given class/name pair, given the + * specification of {@link com.nineoldandroids.util.Property#of(Class, Class, String)} + */ +class ReflectiveProperty extends Property { + + private static final String PREFIX_GET = "get"; + private static final String PREFIX_IS = "is"; + private static final String PREFIX_SET = "set"; + private Method mSetter; + private Method mGetter; + private Field mField; + + /** + * For given property name 'name', look for getName/isName method or 'name' field. + * Also look for setName method (optional - could be readonly). Failing method getters and + * field results in throwing NoSuchPropertyException. + * + * @param propertyHolder The class on which the methods or field are found + * @param name The name of the property, where this name is capitalized and appended to + * "get" and "is to search for the appropriate methods. If the get/is methods are not found, + * the constructor will search for a field with that exact name. + */ + public ReflectiveProperty(Class propertyHolder, Class valueType, String name) { + // TODO: cache reflection info for each new class/name pair + super(valueType, name); + char firstLetter = Character.toUpperCase(name.charAt(0)); + String theRest = name.substring(1); + String capitalizedName = firstLetter + theRest; + String getterName = PREFIX_GET + capitalizedName; + try { + mGetter = propertyHolder.getMethod(getterName, (Class[]) null); + } catch (NoSuchMethodException e) { + try { + /* The native implementation uses JNI to do reflection, which allows access to private methods. + * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback. + */ + mGetter = propertyHolder.getDeclaredMethod(getterName, (Class[]) null); + mGetter.setAccessible(true); + } catch (NoSuchMethodException e2) { + // getName() not available - try isName() instead + getterName = PREFIX_IS + capitalizedName; + try { + mGetter = propertyHolder.getMethod(getterName, (Class[]) null); + } catch (NoSuchMethodException e3) { + try { + /* The native implementation uses JNI to do reflection, which allows access to private methods. + * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback. + */ + mGetter = propertyHolder.getDeclaredMethod(getterName, (Class[]) null); + mGetter.setAccessible(true); + } catch (NoSuchMethodException e4) { + // Try public field instead + try { + mField = propertyHolder.getField(name); + Class fieldType = mField.getType(); + if (!typesMatch(valueType, fieldType)) { + throw new NoSuchPropertyException("Underlying type (" + fieldType + ") " + + "does not match Property type (" + valueType + ")"); + } + return; + } catch (NoSuchFieldException e5) { + // no way to access property - throw appropriate exception + throw new NoSuchPropertyException("No accessor method or field found for" + + " property with name " + name); + } + } + } + } + } + Class getterType = mGetter.getReturnType(); + // Check to make sure our getter type matches our valueType + if (!typesMatch(valueType, getterType)) { + throw new NoSuchPropertyException("Underlying type (" + getterType + ") " + + "does not match Property type (" + valueType + ")"); + } + String setterName = PREFIX_SET + capitalizedName; + try { + // mSetter = propertyHolder.getMethod(setterName, getterType); + // The native implementation uses JNI to do reflection, which allows access to private methods. + mSetter = propertyHolder.getDeclaredMethod(setterName, getterType); + mSetter.setAccessible(true); + } catch (NoSuchMethodException ignored) { + // Okay to not have a setter - just a readonly property + } + } + + /** + * Utility method to check whether the type of the underlying field/method on the target + * object matches the type of the Property. The extra checks for primitive types are because + * generics will force the Property type to be a class, whereas the type of the underlying + * method/field will probably be a primitive type instead. Accept float as matching Float, + * etc. + */ + private boolean typesMatch(Class valueType, Class getterType) { + if (getterType != valueType) { + if (getterType.isPrimitive()) { + return (getterType == float.class && valueType == Float.class) || + (getterType == int.class && valueType == Integer.class) || + (getterType == boolean.class && valueType == Boolean.class) || + (getterType == long.class && valueType == Long.class) || + (getterType == double.class && valueType == Double.class) || + (getterType == short.class && valueType == Short.class) || + (getterType == byte.class && valueType == Byte.class) || + (getterType == char.class && valueType == Character.class); + } + return false; + } + return true; + } + + @Override + public void set(T object, V value) { + if (mSetter != null) { + try { + mSetter.invoke(object, value); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } + } else if (mField != null) { + try { + mField.set(object, value); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } else { + throw new UnsupportedOperationException("Property " + getName() +" is read-only"); + } + } + + @Override + public V get(T object) { + if (mGetter != null) { + try { + return (V) mGetter.invoke(object, (Object[])null); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } + } else if (mField != null) { + try { + return (V) mField.get(object); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + // Should not get here: there should always be a non-null getter or field + throw new AssertionError(); + } + + /** + * Returns false if there is no setter or public field underlying this Property. + */ + @Override + public boolean isReadOnly() { + return (mSetter == null && mField == null); + } +} diff --git a/app/src/main/java/com/nineoldandroids/view/ViewHelper.java b/app/src/main/java/com/nineoldandroids/view/ViewHelper.java new file mode 100644 index 000000000..d79dc0f35 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/view/ViewHelper.java @@ -0,0 +1,292 @@ +package com.nineoldandroids.view; + +import android.view.View; + +import static com.nineoldandroids.view.animation.AnimatorProxy.NEEDS_PROXY; +import static com.nineoldandroids.view.animation.AnimatorProxy.wrap; + +public final class ViewHelper { + private ViewHelper() {} + + public static float getAlpha(View view) { + return NEEDS_PROXY ? wrap(view).getAlpha() : Honeycomb.getAlpha(view); + } + + public static void setAlpha(View view, float alpha) { + if (NEEDS_PROXY) { + wrap(view).setAlpha(alpha); + } else { + Honeycomb.setAlpha(view, alpha); + } + } + + public static float getPivotX(View view) { + return NEEDS_PROXY ? wrap(view).getPivotX() : Honeycomb.getPivotX(view); + } + + public static void setPivotX(View view, float pivotX) { + if (NEEDS_PROXY) { + wrap(view).setPivotX(pivotX); + } else { + Honeycomb.setPivotX(view, pivotX); + } + } + + public static float getPivotY(View view) { + return NEEDS_PROXY ? wrap(view).getPivotY() : Honeycomb.getPivotY(view); + } + + public static void setPivotY(View view, float pivotY) { + if (NEEDS_PROXY) { + wrap(view).setPivotY(pivotY); + } else { + Honeycomb.setPivotY(view, pivotY); + } + } + + public static float getRotation(View view) { + return NEEDS_PROXY ? wrap(view).getRotation() : Honeycomb.getRotation(view); + } + + public static void setRotation(View view, float rotation) { + if (NEEDS_PROXY) { + wrap(view).setRotation(rotation); + } else { + Honeycomb.setRotation(view, rotation); + } + } + + public static float getRotationX(View view) { + return NEEDS_PROXY ? wrap(view).getRotationX() : Honeycomb.getRotationX(view); + } + + public static void setRotationX(View view, float rotationX) { + if (NEEDS_PROXY) { + wrap(view).setRotationX(rotationX); + } else { + Honeycomb.setRotationX(view, rotationX); + } + } + + public static float getRotationY(View view) { + return NEEDS_PROXY ? wrap(view).getRotationY() : Honeycomb.getRotationY(view); + } + + public static void setRotationY(View view, float rotationY) { + if (NEEDS_PROXY) { + wrap(view).setRotationY(rotationY); + } else { + Honeycomb.setRotationY(view, rotationY); + } + } + + public static float getScaleX(View view) { + return NEEDS_PROXY ? wrap(view).getScaleX() : Honeycomb.getScaleX(view); + } + + public static void setScaleX(View view, float scaleX) { + if (NEEDS_PROXY) { + wrap(view).setScaleX(scaleX); + } else { + Honeycomb.setScaleX(view, scaleX); + } + } + + public static float getScaleY(View view) { + return NEEDS_PROXY ? wrap(view).getScaleY() : Honeycomb.getScaleY(view); + } + + public static void setScaleY(View view, float scaleY) { + if (NEEDS_PROXY) { + wrap(view).setScaleY(scaleY); + } else { + Honeycomb.setScaleY(view, scaleY); + } + } + + public static float getScrollX(View view) { + return NEEDS_PROXY ? wrap(view).getScrollX() : Honeycomb.getScrollX(view); + } + + public static void setScrollX(View view, int scrollX) { + if (NEEDS_PROXY) { + wrap(view).setScrollX(scrollX); + } else { + Honeycomb.setScrollX(view, scrollX); + } + } + + public static float getScrollY(View view) { + return NEEDS_PROXY ? wrap(view).getScrollY() : Honeycomb.getScrollY(view); + } + + public static void setScrollY(View view, int scrollY) { + if (NEEDS_PROXY) { + wrap(view).setScrollY(scrollY); + } else { + Honeycomb.setScrollY(view, scrollY); + } + } + + public static float getTranslationX(View view) { + return NEEDS_PROXY ? wrap(view).getTranslationX() : Honeycomb.getTranslationX(view); + } + + public static void setTranslationX(View view, float translationX) { + if (NEEDS_PROXY) { + wrap(view).setTranslationX(translationX); + } else { + Honeycomb.setTranslationX(view, translationX); + } + } + + public static float getTranslationY(View view) { + return NEEDS_PROXY ? wrap(view).getTranslationY() : Honeycomb.getTranslationY(view); + } + + public static void setTranslationY(View view, float translationY) { + if (NEEDS_PROXY) { + wrap(view).setTranslationY(translationY); + } else { + Honeycomb.setTranslationY(view, translationY); + } + } + + public static float getX(View view) { + return NEEDS_PROXY ? wrap(view).getX() : Honeycomb.getX(view); + } + + public static void setX(View view, float x) { + if (NEEDS_PROXY) { + wrap(view).setX(x); + } else { + Honeycomb.setX(view, x); + } + } + + public static float getY(View view) { + return NEEDS_PROXY ? wrap(view).getY() : Honeycomb.getY(view); + } + + public static void setY(View view, float y) { + if (NEEDS_PROXY) { + wrap(view).setY(y); + } else { + Honeycomb.setY(view, y); + } + } + + private static final class Honeycomb { + static float getAlpha(View view) { + return view.getAlpha(); + } + + static void setAlpha(View view, float alpha) { + view.setAlpha(alpha); + } + + static float getPivotX(View view) { + return view.getPivotX(); + } + + static void setPivotX(View view, float pivotX) { + view.setPivotX(pivotX); + } + + static float getPivotY(View view) { + return view.getPivotY(); + } + + static void setPivotY(View view, float pivotY) { + view.setPivotY(pivotY); + } + + static float getRotation(View view) { + return view.getRotation(); + } + + static void setRotation(View view, float rotation) { + view.setRotation(rotation); + } + + static float getRotationX(View view) { + return view.getRotationX(); + } + + static void setRotationX(View view, float rotationX) { + view.setRotationX(rotationX); + } + + static float getRotationY(View view) { + return view.getRotationY(); + } + + static void setRotationY(View view, float rotationY) { + view.setRotationY(rotationY); + } + + static float getScaleX(View view) { + return view.getScaleX(); + } + + static void setScaleX(View view, float scaleX) { + view.setScaleX(scaleX); + } + + static float getScaleY(View view) { + return view.getScaleY(); + } + + static void setScaleY(View view, float scaleY) { + view.setScaleY(scaleY); + } + + static float getScrollX(View view) { + return view.getScrollX(); + } + + static void setScrollX(View view, int scrollX) { + view.setScrollX(scrollX); + } + + static float getScrollY(View view) { + return view.getScrollY(); + } + + static void setScrollY(View view, int scrollY) { + view.setScrollY(scrollY); + } + + static float getTranslationX(View view) { + return view.getTranslationX(); + } + + static void setTranslationX(View view, float translationX) { + view.setTranslationX(translationX); + } + + static float getTranslationY(View view) { + return view.getTranslationY(); + } + + static void setTranslationY(View view, float translationY) { + view.setTranslationY(translationY); + } + + static float getX(View view) { + return view.getX(); + } + + static void setX(View view, float x) { + view.setX(x); + } + + static float getY(View view) { + return view.getY(); + } + + static void setY(View view, float y) { + view.setY(y); + } + } +} diff --git a/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimator.java b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimator.java new file mode 100644 index 000000000..4de664fda --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimator.java @@ -0,0 +1,346 @@ +/* + * 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.util.WeakHashMap; +import android.os.Build; +import android.view.View; +import android.view.animation.Interpolator; +import com.nineoldandroids.animation.Animator; + +/** + * This class enables automatic and optimized animation of select properties on View objects. + * If only one or two properties on a View object are being animated, then using an + * {@link android.animation.ObjectAnimator} is fine; the property setters called by ObjectAnimator + * are well equipped to do the right thing to set the property and invalidate the view + * appropriately. But if several properties are animated simultaneously, or if you just want a + * more convenient syntax to animate a specific property, then ViewPropertyAnimator might be + * more well-suited to the task. + * + *

This class may provide better performance for several simultaneous animations, because + * it will optimize invalidate calls to take place only once for several properties instead of each + * animated property independently causing its own invalidation. Also, the syntax of using this + * class could be easier to use because the caller need only tell the View object which + * property to animate, and the value to animate either to or by, and this class handles the + * details of configuring the underlying Animator class and starting it.

+ * + *

This class is not constructed by the caller, but rather by the View whose properties + * it will animate. Calls to {@link android.view.View#animate()} will return a reference + * to the appropriate ViewPropertyAnimator object for that View.

+ * + */ +public abstract class ViewPropertyAnimator { + private static final WeakHashMap ANIMATORS = + new WeakHashMap(0); + + /** + * This method returns a ViewPropertyAnimator object, which can be used to animate specific + * properties on this View. + * + * @param view View to animate. + * @return The ViewPropertyAnimator associated with this View. + */ + public static ViewPropertyAnimator animate(View view) { + ViewPropertyAnimator animator = ANIMATORS.get(view); + if (animator == null) { + final int version = Integer.valueOf(Build.VERSION.SDK); + if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + animator = new ViewPropertyAnimatorICS(view); + } else if (version >= Build.VERSION_CODES.HONEYCOMB) { + animator = new ViewPropertyAnimatorHC(view); + } else { + animator = new ViewPropertyAnimatorPreHC(view); + } + ANIMATORS.put(view, animator); + } + return animator; + } + + + /** + * 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 abstract ViewPropertyAnimator setDuration(long duration); + + /** + * 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 abstract long getDuration(); + + /** + * Returns the current startDelay of property animations. If the startDelay was set on this + * object, that value is returned. Otherwise, the default value of the underlying Animator + * is returned. + * + * @see #setStartDelay(long) + * @return The startDelay of animations, in milliseconds. + */ + public abstract long getStartDelay(); + + /** + * Sets the startDelay 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 startDelay The delay 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 abstract ViewPropertyAnimator setStartDelay(long startDelay); + + /** + * Sets the interpolator for the underlying animator that animates the requested properties. + * By default, the animator uses the default interpolator for ValueAnimator. Calling this method + * will cause the declared object to be used instead. + * + * @param interpolator The TimeInterpolator to be used for ensuing property animations. + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator setInterpolator(/*Time*/Interpolator interpolator); + + /** + * Sets a listener for events in the underlying Animators that run the property + * animations. + * + * @param listener The listener to be called with AnimatorListener events. + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator setListener(Animator.AnimatorListener listener); + + /** + * Starts the currently pending property animations immediately. Calling start() + * is optional because all animations start automatically at the next opportunity. However, + * if the animations are needed to start immediately and synchronously (not at the time when + * the next event is processed by the hierarchy, which is when the animations would begin + * otherwise), then this method can be used. + */ + public abstract void start(); + + /** + * Cancels all property animations that are currently running or pending. + */ + public abstract void cancel(); + + /** + * This method will cause the View's x property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setX(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator x(float value); + + /** + * This method will cause the View's x property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setX(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator xBy(float value); + + /** + * This method will cause the View's y property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setY(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator y(float value); + + /** + * This method will cause the View's y property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setY(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator yBy(float value); + + /** + * This method will cause the View's rotation property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setRotation(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator rotation(float value); + + /** + * This method will cause the View's rotation property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setRotation(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator rotationBy(float value); + + /** + * This method will cause the View's rotationX property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setRotationX(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator rotationX(float value); + + /** + * This method will cause the View's rotationX property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setRotationX(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator rotationXBy(float value); + + /** + * This method will cause the View's rotationY property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setRotationY(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator rotationY(float value); + + /** + * This method will cause the View's rotationY property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setRotationY(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator rotationYBy(float value); + + /** + * This method will cause the View's translationX property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setTranslationX(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator translationX(float value); + + /** + * This method will cause the View's translationX property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setTranslationX(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator translationXBy(float value); + + /** + * This method will cause the View's translationY property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setTranslationY(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator translationY(float value); + + /** + * This method will cause the View's translationY property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setTranslationY(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator translationYBy(float value); + + /** + * This method will cause the View's scaleX property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setScaleX(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator scaleX(float value); + + /** + * This method will cause the View's scaleX property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setScaleX(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator scaleXBy(float value); + + /** + * This method will cause the View's scaleY property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setScaleY(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator scaleY(float value); + + /** + * This method will cause the View's scaleY property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setScaleY(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator scaleYBy(float value); + + /** + * This method will cause the View's alpha property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see android.view.View#setAlpha(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator alpha(float value); + + /** + * This method will cause the View's alpha property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see android.view.View#setAlpha(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public abstract ViewPropertyAnimator alphaBy(float value); +} diff --git a/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorHC.java b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorHC.java new file mode 100644 index 000000000..7ad2adaa2 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorHC.java @@ -0,0 +1,723 @@ +/* + * 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; + +class ViewPropertyAnimatorHC extends ViewPropertyAnimator { + + /** + * A WeakReference holding the View whose properties are being animated by this class. + * This is set at construction time. + */ + private final WeakReference 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 mPendingAnimations = new ArrayList(); + + /** + * 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 mNameValuesHolder; + + PropertyBundle(int propertyMask, ArrayList 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 mAnimatorMap = + new HashMap(); + + /** + * 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 + */ + ViewPropertyAnimatorHC(View view) { + mView = new WeakReference(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 mAnimatorMapCopy = + (HashMap)mAnimatorMap.clone(); + Set 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 nameValueList = + (ArrayList) 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 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; + View v = mView.get(); + if (v != null) { + switch (propertyConstant) { + case TRANSLATION_X: + //info.mTranslationX = value; + v.setTranslationX(value); + break; + case TRANSLATION_Y: + //info.mTranslationY = value; + v.setTranslationY(value); + break; + case ROTATION: + //info.mRotation = value; + v.setRotation(value); + break; + case ROTATION_X: + //info.mRotationX = value; + v.setRotationX(value); + break; + case ROTATION_Y: + //info.mRotationY = value; + v.setRotationY(value); + break; + case SCALE_X: + //info.mScaleX = value; + v.setScaleX(value); + break; + case SCALE_Y: + //info.mScaleY = value; + v.setScaleY(value); + break; + case X: + //info.mTranslationX = value - v.mLeft; + v.setX(value); + break; + case Y: + //info.mTranslationY = value - v.mTop; + v.setY(value); + break; + case ALPHA: + //info.mAlpha = value; + v.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; + View v = mView.get(); + if (v != null) { + switch (propertyConstant) { + case TRANSLATION_X: + //return info.mTranslationX; + return v.getTranslationX(); + case TRANSLATION_Y: + //return info.mTranslationY; + return v.getTranslationY(); + case ROTATION: + //return info.mRotation; + return v.getRotation(); + case ROTATION_X: + //return info.mRotationX; + return v.getRotationX(); + case ROTATION_Y: + //return info.mRotationY; + return v.getRotationY(); + case SCALE_X: + //return info.mScaleX; + return v.getScaleX(); + case SCALE_Y: + //return info.mScaleY; + return v.getScaleY(); + case X: + //return v.mLeft + info.mTranslationX; + return v.getX(); + case Y: + //return v.mTop + info.mTranslationY; + return v.getY(); + case ALPHA: + //return info.mAlpha; + return v.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 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*/); + } + } + } +} diff --git a/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorICS.java b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorICS.java new file mode 100644 index 000000000..b60a820e4 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorICS.java @@ -0,0 +1,298 @@ +package com.nineoldandroids.view; + +import java.lang.ref.WeakReference; + +import android.view.View; +import android.view.animation.Interpolator; +import com.nineoldandroids.animation.Animator.AnimatorListener; + +class ViewPropertyAnimatorICS extends ViewPropertyAnimator { + /** + * A value to be returned when the WeakReference holding the native implementation + * returns null + */ + private final static long RETURN_WHEN_NULL = -1L; + + /** + * A WeakReference holding the native implementation of ViewPropertyAnimator + */ + private final WeakReference mNative; + + ViewPropertyAnimatorICS(View view) { + mNative = new WeakReference(view.animate()); + } + + @Override + public ViewPropertyAnimator setDuration(long duration) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.setDuration(duration); + } + return this; + } + + @Override + public long getDuration() { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + return n.getDuration(); + } + return RETURN_WHEN_NULL; + } + + @Override + public ViewPropertyAnimator setStartDelay(long startDelay) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.setStartDelay(startDelay); + } + return this; + } + + @Override + public long getStartDelay() { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + return n.getStartDelay(); + } + return RETURN_WHEN_NULL; + } + + @Override + public ViewPropertyAnimator setInterpolator(Interpolator interpolator) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.setInterpolator(interpolator); + } + return this; + } + + @Override + public ViewPropertyAnimator setListener(final AnimatorListener listener) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + if (listener == null) { + n.setListener(null); + } else { + n.setListener(new android.animation.Animator.AnimatorListener() { + @Override + public void onAnimationStart(android.animation.Animator animation) { + listener.onAnimationStart(null); + } + + @Override + public void onAnimationRepeat(android.animation.Animator animation) { + listener.onAnimationRepeat(null); + } + + @Override + public void onAnimationEnd(android.animation.Animator animation) { + listener.onAnimationEnd(null); + } + + @Override + public void onAnimationCancel(android.animation.Animator animation) { + listener.onAnimationCancel(null); + } + }); + } + } + return this; + } + + @Override + public void start() { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.start(); + } + } + + @Override + public void cancel() { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.cancel(); + } + } + + @Override + public ViewPropertyAnimator x(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.x(value); + } + return this; + } + + @Override + public ViewPropertyAnimator xBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.xBy(value); + } + return this; + } + + @Override + public ViewPropertyAnimator y(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.y(value); + } + return this; + } + + @Override + public ViewPropertyAnimator yBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.yBy(value); + } + return this; + } + + @Override + public ViewPropertyAnimator rotation(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.rotation(value); + } + return this; + } + + @Override + public ViewPropertyAnimator rotationBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.rotationBy(value); + } + return this; + } + + @Override + public ViewPropertyAnimator rotationX(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.rotationX(value); + } + return this; + } + + @Override + public ViewPropertyAnimator rotationXBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.rotationXBy(value); + } + return this; + } + + @Override + public ViewPropertyAnimator rotationY(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.rotationY(value); + } + return this; + } + + @Override + public ViewPropertyAnimator rotationYBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.rotationYBy(value); + } + return this; + } + + @Override + public ViewPropertyAnimator translationX(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.translationX(value); + } + return this; + } + + @Override + public ViewPropertyAnimator translationXBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.translationXBy(value); + } + return this; + } + + @Override + public ViewPropertyAnimator translationY(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.translationY(value); + } + return this; + } + + @Override + public ViewPropertyAnimator translationYBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.translationYBy(value); + } + return this; + } + + @Override + public ViewPropertyAnimator scaleX(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.scaleX(value); + } + return this; + } + + @Override + public ViewPropertyAnimator scaleXBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.scaleXBy(value); + } + return this; + } + + @Override + public ViewPropertyAnimator scaleY(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.scaleY(value); + } + return this; + } + + @Override + public ViewPropertyAnimator scaleYBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.scaleYBy(value); + } + return this; + } + + @Override + public ViewPropertyAnimator alpha(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.alpha(value); + } + return this; + } + + @Override + public ViewPropertyAnimator alphaBy(float value) { + android.view.ViewPropertyAnimator n = mNative.get(); + if (n != null) { + n.alphaBy(value); + } + return this; + } +} diff --git a/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorPreHC.java b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorPreHC.java new file mode 100644 index 000000000..c170f6d21 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/view/ViewPropertyAnimatorPreHC.java @@ -0,0 +1,724 @@ +/* + * 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 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 mPendingAnimations = new ArrayList(); + + /** + * 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 mNameValuesHolder; + + PropertyBundle(int propertyMask, ArrayList 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 mAnimatorMap = + new HashMap(); + + /** + * 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); + 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 mAnimatorMapCopy = + (HashMap)mAnimatorMap.clone(); + Set 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 nameValueList = + (ArrayList) 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 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 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*/); + } + } + } +} diff --git a/app/src/main/java/com/nineoldandroids/view/animation/AnimatorProxy.java b/app/src/main/java/com/nineoldandroids/view/animation/AnimatorProxy.java new file mode 100644 index 000000000..ef3dda267 --- /dev/null +++ b/app/src/main/java/com/nineoldandroids/view/animation/AnimatorProxy.java @@ -0,0 +1,322 @@ +package com.nineoldandroids.view.animation; + +import android.graphics.Camera; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.os.Build; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.Transformation; + +import java.lang.ref.WeakReference; +import java.util.WeakHashMap; + +/** + * A proxy class to allow for modifying post-3.0 view properties on all pre-3.0 + * platforms. DO NOT wrap your views with this class if you + * are using {@code ObjectAnimator} as it will handle that itself. + */ +public final class AnimatorProxy extends Animation { + /** Whether or not the current running platform needs to be proxied. */ + public static final boolean NEEDS_PROXY = Integer.valueOf(Build.VERSION.SDK).intValue() < Build.VERSION_CODES.HONEYCOMB; + + private static final WeakHashMap PROXIES = + new WeakHashMap(); + + /** + * Create a proxy to allow for modifying post-3.0 view properties on all + * pre-3.0 platforms. DO NOT wrap your views if you are + * using {@code ObjectAnimator} as it will handle that itself. + * + * @param view View to wrap. + * @return Proxy to post-3.0 properties. + */ + public static AnimatorProxy wrap(View view) { + AnimatorProxy proxy = PROXIES.get(view); + // This checks if the proxy already exists and whether it still is the animation of the given view + if (proxy == null || proxy != view.getAnimation()) { + proxy = new AnimatorProxy(view); + PROXIES.put(view, proxy); + } + return proxy; + } + + private final WeakReference mView; + private final Camera mCamera = new Camera(); + private boolean mHasPivot; + + private float mAlpha = 1; + private float mPivotX; + private float mPivotY; + private float mRotationX; + private float mRotationY; + private float mRotationZ; + private float mScaleX = 1; + private float mScaleY = 1; + private float mTranslationX; + private float mTranslationY; + + private final RectF mBefore = new RectF(); + private final RectF mAfter = new RectF(); + private final Matrix mTempMatrix = new Matrix(); + + private AnimatorProxy(View view) { + setDuration(0); //perform transformation immediately + setFillAfter(true); //persist transformation beyond duration + view.setAnimation(this); + mView = new WeakReference(view); + } + + public float getAlpha() { + return mAlpha; + } + public void setAlpha(float alpha) { + if (mAlpha != alpha) { + mAlpha = alpha; + View view = mView.get(); + if (view != null) { + view.invalidate(); + } + } + } + public float getPivotX() { + return mPivotX; + } + public void setPivotX(float pivotX) { + if (!mHasPivot || mPivotX != pivotX) { + prepareForUpdate(); + mHasPivot = true; + mPivotX = pivotX; + invalidateAfterUpdate(); + } + } + public float getPivotY() { + return mPivotY; + } + public void setPivotY(float pivotY) { + if (!mHasPivot || mPivotY != pivotY) { + prepareForUpdate(); + mHasPivot = true; + mPivotY = pivotY; + invalidateAfterUpdate(); + } + } + public float getRotation() { + return mRotationZ; + } + public void setRotation(float rotation) { + if (mRotationZ != rotation) { + prepareForUpdate(); + mRotationZ = rotation; + invalidateAfterUpdate(); + } + } + public float getRotationX() { + return mRotationX; + } + public void setRotationX(float rotationX) { + if (mRotationX != rotationX) { + prepareForUpdate(); + mRotationX = rotationX; + invalidateAfterUpdate(); + } + } + public float getRotationY() { + return mRotationY; + } + + public void setRotationY(float rotationY) { + if (mRotationY != rotationY) { + prepareForUpdate(); + mRotationY = rotationY; + invalidateAfterUpdate(); + } + } + public float getScaleX() { + return mScaleX; + } + public void setScaleX(float scaleX) { + if (mScaleX != scaleX) { + prepareForUpdate(); + mScaleX = scaleX; + invalidateAfterUpdate(); + } + } + public float getScaleY() { + return mScaleY; + } + public void setScaleY(float scaleY) { + if (mScaleY != scaleY) { + prepareForUpdate(); + mScaleY = scaleY; + invalidateAfterUpdate(); + } + } + public int getScrollX() { + View view = mView.get(); + if (view == null) { + return 0; + } + return view.getScrollX(); + } + public void setScrollX(int value) { + View view = mView.get(); + if (view != null) { + view.scrollTo(value, view.getScrollY()); + } + } + public int getScrollY() { + View view = mView.get(); + if (view == null) { + return 0; + } + return view.getScrollY(); + } + public void setScrollY(int value) { + View view = mView.get(); + if (view != null) { + view.scrollTo(view.getScrollX(), value); + } + } + + public float getTranslationX() { + return mTranslationX; + } + public void setTranslationX(float translationX) { + if (mTranslationX != translationX) { + prepareForUpdate(); + mTranslationX = translationX; + invalidateAfterUpdate(); + } + } + public float getTranslationY() { + return mTranslationY; + } + public void setTranslationY(float translationY) { + if (mTranslationY != translationY) { + prepareForUpdate(); + mTranslationY = translationY; + invalidateAfterUpdate(); + } + } + public float getX() { + View view = mView.get(); + if (view == null) { + return 0; + } + return view.getLeft() + mTranslationX; + } + public void setX(float x) { + View view = mView.get(); + if (view != null) { + setTranslationX(x - view.getLeft()); + } + } + public float getY() { + View view = mView.get(); + if (view == null) { + return 0; + } + return view.getTop() + mTranslationY; + } + public void setY(float y) { + View view = mView.get(); + if (view != null) { + setTranslationY(y - view.getTop()); + } + } + + private void prepareForUpdate() { + View view = mView.get(); + if (view != null) { + computeRect(mBefore, view); + } + } + private void invalidateAfterUpdate() { + View view = mView.get(); + if (view == null || view.getParent() == null) { + return; + } + + final RectF after = mAfter; + computeRect(after, view); + after.union(mBefore); + + ((View)view.getParent()).invalidate( + (int) Math.floor(after.left), + (int) Math.floor(after.top), + (int) Math.ceil(after.right), + (int) Math.ceil(after.bottom)); + } + + private void computeRect(final RectF r, View view) { + // compute current rectangle according to matrix transformation + final float w = view.getWidth(); + final float h = view.getHeight(); + + // use a rectangle at 0,0 to make sure we don't run into issues with scaling + r.set(0, 0, w, h); + + final Matrix m = mTempMatrix; + m.reset(); + transformMatrix(m, view); + mTempMatrix.mapRect(r); + + r.offset(view.getLeft(), view.getTop()); + + // Straighten coords if rotations flipped them + if (r.right < r.left) { + final float f = r.right; + r.right = r.left; + r.left = f; + } + if (r.bottom < r.top) { + final float f = r.top; + r.top = r.bottom; + r.bottom = f; + } + } + + private void transformMatrix(Matrix m, View view) { + final float w = view.getWidth(); + final float h = view.getHeight(); + final boolean hasPivot = mHasPivot; + final float pX = hasPivot ? mPivotX : w / 2f; + final float pY = hasPivot ? mPivotY : h / 2f; + + final float rX = mRotationX; + final float rY = mRotationY; + final float rZ = mRotationZ; + if ((rX != 0) || (rY != 0) || (rZ != 0)) { + final Camera camera = mCamera; + camera.save(); + camera.rotateX(rX); + camera.rotateY(rY); + camera.rotateZ(-rZ); + camera.getMatrix(m); + camera.restore(); + m.preTranslate(-pX, -pY); + m.postTranslate(pX, pY); + } + + final float sX = mScaleX; + final float sY = mScaleY; + if ((sX != 1.0f) || (sY != 1.0f)) { + m.postScale(sX, sY); + final float sPX = -(pX / w) * ((sX * w) - w); + final float sPY = -(pY / h) * ((sY * h) - h); + m.postTranslate(sPX, sPY); + } + + m.postTranslate(mTranslationX, mTranslationY); + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + View view = mView.get(); + if (view != null) { + t.setAlpha(mAlpha); + transformMatrix(t.getMatrix(), view); + } + } +} diff --git a/app/src/main/res/layout/widget_float_label_edit_text.xml b/app/src/main/res/layout/widget_float_label_edit_text.xml new file mode 100644 index 000000000..180374aea --- /dev/null +++ b/app/src/main/res/layout/widget_float_label_edit_text.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 4bfd46471..78ddc8f47 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -28,4 +28,13 @@ + + + + + + + + +