/* * Copyright 2014 Niek Haarman * * 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.nhaarman.listviewanimations.appearance; import android.annotation.SuppressLint; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.os.SystemClock; import android.util.SparseArray; import android.view.View; import android.widget.GridView; import com.nhaarman.listviewanimations.util.ListViewWrapper; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.AnimatorSet; import com.nineoldandroids.view.ViewHelper; import com.pan.simplepicture.annotations.NonNull; import com.pan.simplepicture.annotations.Nullable; /** * A class which decides whether given Views should be animated based on their position: each View should only be animated once. * It also calculates proper animation delays for the views. */ public class ViewAnimator { /* Saved instance state keys */ private static final String SAVEDINSTANCESTATE_FIRSTANIMATEDPOSITION = "savedinstancestate_firstanimatedposition"; private static final String SAVEDINSTANCESTATE_LASTANIMATEDPOSITION = "savedinstancestate_lastanimatedposition"; private static final String SAVEDINSTANCESTATE_SHOULDANIMATE = "savedinstancestate_shouldanimate"; /* Default values */ /** * The default delay in millis before the first animation starts. */ private static final int INITIAL_DELAY_MILLIS = 150; /** * The default delay in millis between view animations. */ private static final int DEFAULT_ANIMATION_DELAY_MILLIS = 100; /** * The default duration in millis of the animations. */ private static final int DEFAULT_ANIMATION_DURATION_MILLIS = 300; /* Fields */ /** * The ListViewWrapper containing the ListView implementation. */ @NonNull private final ListViewWrapper mListViewWrapper; /** * The active Animators. Keys are hashcodes of the Views that are animated. */ @NonNull private final SparseArray<Animator> mAnimators = new SparseArray<Animator>(); /** * The delay in millis before the first animation starts. */ private int mInitialDelayMillis = INITIAL_DELAY_MILLIS; /** * The delay in millis between view animations. */ private int mAnimationDelayMillis = DEFAULT_ANIMATION_DELAY_MILLIS; /** * The duration in millis of the animations. */ private int mAnimationDurationMillis = DEFAULT_ANIMATION_DURATION_MILLIS; /** * The start timestamp of the first animation, as returned by {@link android.os.SystemClock#uptimeMillis()}. */ private long mAnimationStartMillis; /** * The position of the item that is the first that was animated. */ private int mFirstAnimatedPosition; /** * The position of the last item that was animated. */ private int mLastAnimatedPosition; /** * Whether animation is enabled. When this is set to false, no animation is applied to the views. */ private boolean mShouldAnimate = true; /** * Creates a new ViewAnimator, using the given {@link com.nhaarman.listviewanimations.util.ListViewWrapper}. * * @param listViewWrapper the {@code ListViewWrapper} which wraps the implementation of the ListView used. */ public ViewAnimator(@NonNull final ListViewWrapper listViewWrapper) { mListViewWrapper = listViewWrapper; mAnimationStartMillis = -1; mFirstAnimatedPosition = -1; mLastAnimatedPosition = -1; } /** * Call this method to reset animation status on all views. */ public void reset() { for (int i = 0; i < mAnimators.size(); i++) { mAnimators.get(mAnimators.keyAt(i)).cancel(); } mAnimators.clear(); mFirstAnimatedPosition = -1; mLastAnimatedPosition = -1; mAnimationStartMillis = -1; mShouldAnimate = true; } /** * Set the starting position for which items should animate. Given position will animate as well. * Will also call {@link #enableAnimations()}. * * @param position the position. */ public void setShouldAnimateFromPosition(final int position) { enableAnimations(); mFirstAnimatedPosition = position - 1; mLastAnimatedPosition = position - 1; } /** * Set the starting position for which items should animate as the first position which isn't currently visible on screen. This call is also valid when the {@link View}s * haven't been drawn yet. Will also call {@link #enableAnimations()}. */ public void setShouldAnimateNotVisible() { enableAnimations(); mFirstAnimatedPosition = mListViewWrapper.getLastVisiblePosition(); mLastAnimatedPosition = mListViewWrapper.getLastVisiblePosition(); } /** * Sets the value of the last animated position. Views with positions smaller than or equal to given value will not be animated. */ void setLastAnimatedPosition(final int lastAnimatedPosition) { mLastAnimatedPosition = lastAnimatedPosition; } /** * Sets the delay in milliseconds before the first animation should start. Defaults to {@value #INITIAL_DELAY_MILLIS}. * * @param delayMillis the time in milliseconds. */ public void setInitialDelayMillis(final int delayMillis) { mInitialDelayMillis = delayMillis; } /** * Sets the delay in milliseconds before an animation of a view should start. Defaults to {@value #DEFAULT_ANIMATION_DELAY_MILLIS}. * * @param delayMillis the time in milliseconds. */ public void setAnimationDelayMillis(final int delayMillis) { mAnimationDelayMillis = delayMillis; } /** * Sets the duration of the animation in milliseconds. Defaults to {@value #DEFAULT_ANIMATION_DURATION_MILLIS}. * * @param durationMillis the time in milliseconds. */ public void setAnimationDurationMillis(final int durationMillis) { mAnimationDurationMillis = durationMillis; } /** * Enables animating the Views. This is the default. */ public void enableAnimations() { mShouldAnimate = true; } /** * Disables animating the Views. Enable them again using {@link #enableAnimations()}. */ public void disableAnimations() { mShouldAnimate = false; } /** * Cancels any existing animations for given View. */ void cancelExistingAnimation(@NonNull final View view) { int hashCode = view.hashCode(); Animator animator = mAnimators.get(hashCode); if (animator != null) { animator.end(); mAnimators.remove(hashCode); } } /** * Animates given View if necessary. * * @param position the position of the item the View represents. * @param view the View that should be animated. */ public void animateViewIfNecessary(final int position, @NonNull final View view, @NonNull final Animator[] animators) { if (mShouldAnimate && position > mLastAnimatedPosition) { if (mFirstAnimatedPosition == -1) { mFirstAnimatedPosition = position; } animateView(position, view, animators); mLastAnimatedPosition = position; } } /** * Animates given View. * * @param view the View that should be animated. */ private void animateView(final int position, @NonNull final View view, @NonNull final Animator[] animators) { if (mAnimationStartMillis == -1) { mAnimationStartMillis = SystemClock.uptimeMillis(); } ViewHelper.setAlpha(view, 0); AnimatorSet set = new AnimatorSet(); set.playTogether(animators); set.setStartDelay(calculateAnimationDelay(position)); set.setDuration(mAnimationDurationMillis); set.start(); mAnimators.put(view.hashCode(), set); } /** * Returns the delay in milliseconds after which animation for View with position mLastAnimatedPosition + 1 should start. */ @SuppressLint("NewApi") private int calculateAnimationDelay(final int position) { int delay; int lastVisiblePosition = mListViewWrapper.getLastVisiblePosition(); int firstVisiblePosition = mListViewWrapper.getFirstVisiblePosition(); int numberOfItemsOnScreen = lastVisiblePosition - firstVisiblePosition; int numberOfAnimatedItems = position - 1 - mFirstAnimatedPosition; if (numberOfItemsOnScreen + 1 < numberOfAnimatedItems) { delay = mAnimationDelayMillis; if (mListViewWrapper.getListView() instanceof GridView && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { int numColumns = ((GridView) mListViewWrapper.getListView()).getNumColumns(); delay += mAnimationDelayMillis * (position % numColumns); } } else { int delaySinceStart = (position - mFirstAnimatedPosition) * mAnimationDelayMillis; delay = Math.max(0, (int) (-SystemClock.uptimeMillis() + mAnimationStartMillis + mInitialDelayMillis + delaySinceStart)); } return delay; } /** * Returns a Parcelable object containing the AnimationAdapter's current dynamic state. */ @NonNull public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putInt(SAVEDINSTANCESTATE_FIRSTANIMATEDPOSITION, mFirstAnimatedPosition); bundle.putInt(SAVEDINSTANCESTATE_LASTANIMATEDPOSITION, mLastAnimatedPosition); bundle.putBoolean(SAVEDINSTANCESTATE_SHOULDANIMATE, mShouldAnimate); return bundle; } /** * Restores this AnimationAdapter's state. * * @param parcelable the Parcelable object previously returned by {@link #onSaveInstanceState()}. */ public void onRestoreInstanceState(@Nullable final Parcelable parcelable) { if (parcelable instanceof Bundle) { Bundle bundle = (Bundle) parcelable; mFirstAnimatedPosition = bundle.getInt(SAVEDINSTANCESTATE_FIRSTANIMATEDPOSITION); mLastAnimatedPosition = bundle.getInt(SAVEDINSTANCESTATE_LASTANIMATEDPOSITION); mShouldAnimate = bundle.getBoolean(SAVEDINSTANCESTATE_SHOULDANIMATE); } } }