/* * Copyright 2013 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.haarman.listviewanimations.swinginadapters; import android.annotation.SuppressLint; import android.os.Build; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.GridView; import com.haarman.listviewanimations.BaseAdapterDecorator; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.AnimatorSet; import com.nineoldandroids.animation.ObjectAnimator; /** * A BaseAdapterDecorator class which applies multiple Animators at once to * views when they are first shown. The Animators applied include the animations * specified in getAnimators(ViewGroup, View), plus an alpha transition. */ public abstract class AnimationAdapter extends BaseAdapterDecorator { protected static final long DEFAULTANIMATIONDELAYMILLIS = 100; protected static final long DEFAULTANIMATIONDURATIONMILLIS = 300; private static final long INITIALDELAYMILLIS = 150; private SparseArray<AnimationInfo> mAnimators; private long mAnimationStartMillis; private int mFirstAnimatedPosition; private int mLastAnimatedPosition; private boolean mHasParentAnimationAdapter; private boolean mShouldAnimate = true; public AnimationAdapter(BaseAdapter baseAdapter) { super(baseAdapter); mAnimators = new SparseArray<AnimationInfo>(); mAnimationStartMillis = -1; mLastAnimatedPosition = -1; if (baseAdapter instanceof AnimationAdapter) { ((AnimationAdapter) baseAdapter).setHasParentAnimationAdapter(true); } } /** * Call this method to reset animation status on all views. The next time * notifyDataSetChanged() is called on the base adapter, all views will * animate again. Will also call setShouldAnimate(true). */ public void reset() { mAnimators.clear(); mFirstAnimatedPosition = 0; mLastAnimatedPosition = -1; mAnimationStartMillis = -1; mShouldAnimate = true; if (getDecoratedBaseAdapter() instanceof AnimationAdapter) { ((AnimationAdapter) getDecoratedBaseAdapter()).reset(); } } /** * Set whether to animate the {@link View}s or not. * @param shouldAnimate true if the Views should be animated. */ public void setShouldAnimate(boolean shouldAnimate) { mShouldAnimate = shouldAnimate; } /** * Set the starting position for which items should animate. Given position will animate as well. * Will also call setShouldAnimate(true). * @param position the position. */ public void setShouldAnimateFromPosition(int position) { mShouldAnimate = true; 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 setShouldAnimate(true). */ public void setShouldAnimateNotVisible() { if (getAbsListView() == null) { throw new IllegalStateException("Call setListView() on this AnimationAdapter before setShouldAnimateNotVisible()!"); } mShouldAnimate = true; mFirstAnimatedPosition = getAbsListView().getLastVisiblePosition(); mLastAnimatedPosition = getAbsListView().getLastVisiblePosition(); } @Override public final View getView(int position, View convertView, ViewGroup parent) { boolean alreadyStarted = false; if (!mHasParentAnimationAdapter) { if (getAbsListView() == null) { throw new IllegalStateException("Call setListView() on this AnimationAdapter before setAdapter()!"); } if (convertView != null) { alreadyStarted = cancelExistingAnimation(position, convertView); } } View itemView = super.getView(position, convertView, parent); if (!mHasParentAnimationAdapter && !alreadyStarted) { animateViewIfNecessary(position, itemView, parent); } return itemView; } private boolean cancelExistingAnimation(int position, View convertView) { boolean alreadyStarted = false; int hashCode = convertView.hashCode(); AnimationInfo animationInfo = mAnimators.get(hashCode); if (animationInfo != null) { if (animationInfo.position != position) { animationInfo.animator.end(); mAnimators.remove(hashCode); } else { alreadyStarted = true; } } return alreadyStarted; } private void animateViewIfNecessary(int position, View view, ViewGroup parent) { if (position > mLastAnimatedPosition && mShouldAnimate) { animateView(position, parent, view, false); mLastAnimatedPosition = position; } } private void animateView(int position, ViewGroup parent, View view, boolean isHeader) { if (mAnimationStartMillis == -1) { mAnimationStartMillis = System.currentTimeMillis(); } hideView(view); Animator[] childAnimators; if (mDecoratedBaseAdapter instanceof AnimationAdapter) { childAnimators = ((AnimationAdapter) mDecoratedBaseAdapter).getAnimators(parent, view); } else { childAnimators = new Animator[0]; } Animator[] animators = getAnimators(parent, view); Animator alphaAnimator = ObjectAnimator.ofFloat(view, "alpha", 0, 1); AnimatorSet set = new AnimatorSet(); set.playTogether(concatAnimators(childAnimators, animators, alphaAnimator)); set.setStartDelay(calculateAnimationDelay(isHeader)); set.setDuration(getAnimationDurationMillis()); set.start(); mAnimators.put(view.hashCode(), new AnimationInfo(position, set)); } private void hideView(View view) { ObjectAnimator animator = ObjectAnimator.ofFloat(view, "alpha", 0); AnimatorSet set = new AnimatorSet(); set.play(animator); set.setDuration(0); set.start(); } private Animator[] concatAnimators(Animator[] childAnimators, Animator[] animators, Animator alphaAnimator) { Animator[] allAnimators = new Animator[childAnimators.length + animators.length + 1]; int i; for (i = 0; i < animators.length; ++i) { allAnimators[i] = animators[i]; } for (int j = 0; j < childAnimators.length; ++j) { allAnimators[i] = childAnimators[j]; ++i; } allAnimators[allAnimators.length - 1] = alphaAnimator; return allAnimators; } @SuppressLint("NewApi") private long calculateAnimationDelay(boolean isHeader) { long delay; int numberOfItems = getAbsListView().getLastVisiblePosition() - getAbsListView().getFirstVisiblePosition(); if (numberOfItems + 1 < mLastAnimatedPosition) { delay = getAnimationDelayMillis(); if (getAbsListView() instanceof GridView && Build.VERSION.SDK_INT >= 11) { delay += getAnimationDelayMillis() * ((mLastAnimatedPosition + 1) % ((GridView) getAbsListView()).getNumColumns()); } } else { long delaySinceStart = (mLastAnimatedPosition - mFirstAnimatedPosition + 1) * getAnimationDelayMillis(); delay = mAnimationStartMillis + getInitialDelayMillis() + delaySinceStart - System.currentTimeMillis(); delay -= isHeader && mLastAnimatedPosition > 0 ? getAnimationDelayMillis() : 0; } // System.out.println(isHeader + ": " + delay); return Math.max(0, delay); } /** * Set whether this AnimationAdapter is encapsulated by another * AnimationAdapter. When this is set to true, this AnimationAdapter does * not apply any animations to the views. Should not be set explicitly, the * AnimationAdapter class manages this by itself. */ public void setHasParentAnimationAdapter(boolean hasParentAnimationAdapter) { mHasParentAnimationAdapter = hasParentAnimationAdapter; } /** * Get the delay in milliseconds before the first animation should start. Defaults to {@value #INITIALDELAYMILLIS}. */ protected long getInitialDelayMillis() { return INITIALDELAYMILLIS; } /** * Get the delay in milliseconds before an animation of a view should start. */ protected abstract long getAnimationDelayMillis(); /** * Get the duration of the animation in milliseconds. */ protected abstract long getAnimationDurationMillis(); /** * Get the Animators to apply to the views. In addition to the returned * Animators, an alpha transition will be applied to the view. * * @param parent * The parent of the view * @param view * The view that will be animated, as retrieved by getView() */ public abstract Animator[] getAnimators(ViewGroup parent, View view); private class AnimationInfo { public int position; public Animator animator; public AnimationInfo(int position, Animator animator) { this.position = position; this.animator = animator; } } }