/* * Copyright (C) 2013 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.example.android.listviewitemanimations; import java.util.ArrayList; import java.util.HashMap; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewTreeObserver; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationSet; import android.view.animation.TranslateAnimation; import android.widget.ListView; /** * This example shows how to use a swipe effect to remove items from a ListView, * and how to use animations to complete the swipe as well as to animate the other * items in the list into their final places. This code works on runtimes back to Gingerbread * (Android 2.3), by using the android.view.animation classes on earlier releases. * * Watch the associated video for this demo on the DevBytes channel of developer.android.com * or on the DevBytes playlist in the androiddevelopers channel on YouTube at * https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_XOgcRukSoKKjewFJZrKV0. */ public class ListViewItemAnimations extends Activity { final ArrayList mCheckedViews = new ArrayList(); StableArrayAdapter mAdapter; ListView mListView; BackgroundContainer mBackgroundContainer; boolean mSwiping = false; boolean mItemPressed = false; HashMap mItemIdTopMap = new HashMap(); boolean mAnimating = false; float mCurrentX = 0; float mCurrentAlpha = 1; private static final int SWIPE_DURATION = 250; private static final int MOVE_DURATION = 150; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_view_item_animations); mBackgroundContainer = (BackgroundContainer) findViewById(R.id.listViewBackground); mListView = (ListView) findViewById(R.id.listview); final ArrayList cheeseList = new ArrayList(); for (int i = 0; i < Cheeses.sCheeseStrings.length; ++i) { cheeseList.add(Cheeses.sCheeseStrings[i]); } mAdapter = new StableArrayAdapter(this,R.layout.opaque_text_view, cheeseList, mTouchListener); mListView.setAdapter(mAdapter); } /** * Returns true if the current runtime is Honeycomb or later */ private boolean isRuntimePostGingerbread() { return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB; } private View.OnTouchListener mTouchListener = new View.OnTouchListener() { float mDownX; private int mSwipeSlop = -1; @SuppressLint("NewApi") @Override public boolean onTouch(final View v, MotionEvent event) { if (mSwipeSlop < 0) { mSwipeSlop = ViewConfiguration.get(ListViewItemAnimations.this). getScaledTouchSlop(); } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (mAnimating) { // Multi-item swipes not handled return true; } mItemPressed = true; mDownX = event.getX(); break; case MotionEvent.ACTION_CANCEL: setSwipePosition(v, 0); mItemPressed = false; break; case MotionEvent.ACTION_MOVE: { if (mAnimating) { return true; } float x = event.getX(); if (isRuntimePostGingerbread()) { x += v.getTranslationX(); } float deltaX = x - mDownX; float deltaXAbs = Math.abs(deltaX); if (!mSwiping) { if (deltaXAbs > mSwipeSlop) { mSwiping = true; mListView.requestDisallowInterceptTouchEvent(true); mBackgroundContainer.showBackground(v.getTop(), v.getHeight()); } } if (mSwiping) { setSwipePosition(v, deltaX); } } break; case MotionEvent.ACTION_UP: { if (mAnimating) { return true; } // User let go - figure out whether to animate the view out, or back into place if (mSwiping) { float x = event.getX(); if (isRuntimePostGingerbread()) { x += v.getTranslationX(); } float deltaX = x - mDownX; float deltaXAbs = Math.abs(deltaX); float fractionCovered; float endX; final boolean remove; if (deltaXAbs > v.getWidth() / 4) { // Greater than a quarter of the width - animate it out fractionCovered = deltaXAbs / v.getWidth(); endX = deltaX < 0 ? -v.getWidth() : v.getWidth(); remove = true; } else { // Not far enough - animate it back fractionCovered = 1 - (deltaXAbs / v.getWidth()); endX = 0; remove = false; } // Animate position and alpha long duration = (int) ((1 - fractionCovered) * SWIPE_DURATION); animateSwipe(v, endX, duration, remove); } else { mItemPressed = false; } } break; default: return false; } return true; } }; /** * Animates a swipe of the item either back into place or out of the listview container. * NOTE: This is a simplified version of swipe behavior, for the purposes of this demo * about animation. A real version should use velocity (via the VelocityTracker class) * to send the item off or back at an appropriate speed. */ @SuppressLint("NewApi") private void animateSwipe(final View view, float endX, long duration, final boolean remove) { mAnimating = true; mListView.setEnabled(false); if (isRuntimePostGingerbread()) { view.animate().setDuration(duration). alpha(remove ? 0 : 1).translationX(endX). setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // Restore animated values view.setAlpha(1); view.setTranslationX(0); if (remove) { animateOtherViews(mListView, view); } else { mBackgroundContainer.hideBackground(); mSwiping = false; mAnimating = false; mListView.setEnabled(true); } mItemPressed = false; } }); } else { TranslateAnimation swipeAnim = new TranslateAnimation(mCurrentX, endX, 0, 0); AlphaAnimation alphaAnim = new AlphaAnimation(mCurrentAlpha, remove ? 0 : 1); AnimationSet set = new AnimationSet(true); set.addAnimation(swipeAnim); set.addAnimation(alphaAnim); set.setDuration(duration); view.startAnimation(set); setAnimationEndAction(set, new Runnable() { @Override public void run() { if (remove) { animateOtherViews(mListView, view); } else { mBackgroundContainer.hideBackground(); mSwiping = false; mAnimating = false; mListView.setEnabled(true); } mItemPressed = false; } }); } } /** * Sets the horizontal position and translucency of the view being swiped. */ @SuppressLint("NewApi") private void setSwipePosition(View view, float deltaX) { float fraction = Math.abs(deltaX) / view.getWidth(); if (isRuntimePostGingerbread()) { view.setTranslationX(deltaX); view.setAlpha(1 - fraction); } else { // Hello, Gingerbread! TranslateAnimation swipeAnim = new TranslateAnimation(deltaX, deltaX, 0, 0); mCurrentX = deltaX; mCurrentAlpha = (1 - fraction); AlphaAnimation alphaAnim = new AlphaAnimation(mCurrentAlpha, mCurrentAlpha); AnimationSet set = new AnimationSet(true); set.addAnimation(swipeAnim); set.addAnimation(alphaAnim); set.setFillAfter(true); set.setFillEnabled(true); view.startAnimation(set); } } /** * This method animates all other views in the ListView container (not including ignoreView) * into their final positions. It is called after ignoreView has been removed from the * adapter, but before layout has been run. The approach here is to figure out where * everything is now, then allow layout to run, then figure out where everything is after * layout, and then to run animations between all of those start/end positions. */ private void animateOtherViews(final ListView listview, View viewToRemove) { int firstVisiblePosition = listview.getFirstVisiblePosition(); for (int i = 0; i < listview.getChildCount(); ++i) { View child = listview.getChildAt(i); int position = firstVisiblePosition + i; long itemId = mAdapter.getItemId(position); if (child != viewToRemove) { mItemIdTopMap.put(itemId, child.getTop()); } } // Delete the item from the adapter int position = mListView.getPositionForView(viewToRemove); mAdapter.remove(mAdapter.getItem(position)); // After layout runs, capture position of all itemIDs, compare to pre-layout // positions, and animate changes final ViewTreeObserver observer = listview.getViewTreeObserver(); observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { public boolean onPreDraw() { observer.removeOnPreDrawListener(this); boolean firstAnimation = true; int firstVisiblePosition = listview.getFirstVisiblePosition(); for (int i = 0; i < listview.getChildCount(); ++i) { final View child = listview.getChildAt(i); int position = firstVisiblePosition + i; long itemId = mAdapter.getItemId(position); Integer startTop = mItemIdTopMap.get(itemId); int top = child.getTop(); if (startTop == null) { // Animate new views along with the others. The catch is that they did not // exist in the start state, so we must calculate their starting position // based on whether they're coming in from the bottom (i > 0) or top. int childHeight = child.getHeight() + listview.getDividerHeight(); startTop = top + (i > 0 ? childHeight : -childHeight); } int delta = startTop - top; if (delta != 0) { Runnable endAction = firstAnimation ? new Runnable() { public void run() { mBackgroundContainer.hideBackground(); mSwiping = false; mAnimating = false; mListView.setEnabled(true); } } : null; firstAnimation = false; moveView(child, 0, 0, delta, 0, endAction); } } mItemIdTopMap.clear(); return true; } }); } /** * Animate a view between start and end X/Y locations, using either old (pre-3.0) or * new animation APIs. */ @SuppressLint("NewApi") private void moveView(View view, float startX, float endX, float startY, float endY, Runnable endAction) { final Runnable finalEndAction = endAction; if (isRuntimePostGingerbread()) { view.animate().setDuration(MOVE_DURATION); if (startX != endX) { ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, startX, endX); anim.setDuration(MOVE_DURATION); anim.start(); setAnimatorEndAction(anim, endAction); endAction = null; } if (startY != endY) { ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY); anim.setDuration(MOVE_DURATION); anim.start(); setAnimatorEndAction(anim, endAction); } } else { TranslateAnimation translator = new TranslateAnimation(startX, endX, startY, endY); translator.setDuration(MOVE_DURATION); view.startAnimation(translator); if (endAction != null) { view.getAnimation().setAnimationListener(new AnimationListenerAdapter() { @Override public void onAnimationEnd(Animation animation) { finalEndAction.run(); } }); } } } @SuppressLint("NewApi") private void setAnimatorEndAction(Animator animator, final Runnable endAction) { if (endAction != null) { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { endAction.run(); } }); } } private void setAnimationEndAction(Animation animation, final Runnable endAction) { if (endAction != null) { animation.setAnimationListener(new AnimationListenerAdapter() { @Override public void onAnimationEnd(Animation animation) { endAction.run(); } }); } } /** * Utility, to avoid having to implement every method in AnimationListener in * every implementation class */ static class AnimationListenerAdapter implements AnimationListener { @Override public void onAnimationEnd(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { } } }