blob: 68a765125accebfb049fd1c1b60a62737ce3f3a0 [file] [log] [blame]
/*
* 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<View> mCheckedViews = new ArrayList<View>();
StableArrayAdapter mAdapter;
ListView mListView;
BackgroundContainer mBackgroundContainer;
boolean mSwiping = false;
boolean mItemPressed = false;
HashMap<Long, Integer> mItemIdTopMap = new HashMap<Long, Integer>();
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<String> cheeseList = new ArrayList<String>();
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) {
}
}
}