[TUT] Animate Removing From a Custom Adapter (ListView)

Hi guys, this is a Tutorial Request to show how to control your own animations from a custom adapter.

The question was a bit vague so I’ve taken it and will show you how to animate a view when you are removing an item from a ListView. Of course ListView is backed by an Adapter, so here we have a custom adapter where we can control the animations.

I’ve created some dummy data for our ListView, it will show a list of fruits with the type, name and colour on each list item.

What we’re going to do:

  1. Create an Activity with our list (obv)
  2. Create some dummy domain data (our fruits)
  3. Create a custom adapter so we can control the view layout
  4. Listen for long presses on our list and delete items using a custom animation!

Ok first off I’m not coding to paste every class into the article so for the full source you will have to download the eclipse project or check out the GitHub repo. See end of post for the links. As usual all source code has been highly commented for explanation.

So pretend you’ve seen the code for the domain, it’s just a list of fruit items. Each item extends our abstract fruit:

Fruit.java

package com.blundell.tut.animatelistview.domain.fruit;

import com.blundell.tut.animatelistview.domain.Color;
import com.blundell.tut.animatelistview.domain.Type;

/**
 *
 * @author paul.blundell
 *
 */
public abstract class Fruit {

	private final String name;
	private final int id;

	/**
	 * Create a new fruit with the given name, each fruit is given a unique ID
	 * @param name
	 */
	public Fruit(String name) {
		id = getNewId();
		this.name = name;
	}

	public abstract Type type();
	public abstract Color color();

	public int id(){
		return id;
	}

	public String name(){
		return name;
	}

	// Unique ID is generated using a static count, you probably would
	// want something a bit more robust in production
	private static int i = 0;
	private static int getNewId(){
		return i++;
	}
}

We then create a custom ListView, this allows us to keep the long click and adapter communication all internal and the activity clean. In the initialization of our list we add a layout animation. This makes the list animate as it is populated, it is a nice fade in effect and affects each row as it is drawn. Each row looks like this:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="5dip" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Large Text"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dip"
        android:text="Small Text"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="right"
        android:text="Small Text"
        android:textAppearance="?android:attr/textAppearanceSmall" />

</LinearLayout>

Also this is where we add the long click listener, because we have a custom listview we have a reference to our adapter when the fruit data is set. This allows the long click listener to send the adapter the position of the specific row that has been clicked. Thus allowing us to animate then delete it from the list.

FruitListView.java

package com.blundell.tut.animatelistview.ui.widget;

import java.util.ArrayList;

import com.blundell.tut.animatelistview.domain.fruit.Fruit;
import com.blundell.tut.animatelistview.ui.adapter.FruitAdapter;
import com.blundell.tut.animatelistview.util.AnimationHelper;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.LayoutAnimationController;
import android.widget.AdapterView;
import android.widget.ListView;

/**
 * Our listview for showing fruit. You can call addFruit to populate the list.
 * We have also added an Animation so the list is populated nicely from top to bottom
 * @author paul.blundell
 *
 */
public class FruitListView extends ListView implements android.widget.AdapterView.OnItemLongClickListener {

	private FruitAdapter fruitAdapter;

	public FruitListView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init();
	}

	public FruitListView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	public FruitListView(Context context) {
		super(context);
		init();
	}

	// Populate our list view
	// we pass the layout inflater to the adapter for efficiency
	// also allows us to keep a reference to the adapter
	public void addFruit(ArrayList<Fruit> fruits) {
		fruitAdapter = new FruitAdapter(fruits, LayoutInflater.from(getContext()));
		setAdapter(fruitAdapter);
	}

	private void init() {
		setPopulationAnimation();
		setOnItemLongClickListener(this);
	}

	// Add a new animation for when the view is layed out
	// this will add our items to the list in a nice controlled fashion top to bottom
	private void setPopulationAnimation() {
		AnimationSet set = new AnimationSet(true);

		Animation animation = AnimationHelper.createFadeInAnimation();
		set.addAnimation(animation);

		LayoutAnimationController controller = new LayoutAnimationController(set, 0.5f);
		setLayoutAnimation(controller);
	}

	// Add a listener so we know when the user is long pressing on one of our rows
	// we then pass this interaction to the adapter
	@Override
	public boolean onItemLongClick(AdapterView<?> adapter, View view, int position, long id) {
		fruitAdapter.delete(position);
		return true;
	}
}

Straight onto the Adapter. It’s just standard list drawing and recycling until the end of the getView method. Here we make use of a second list of items. This list is what is to be deleted. We need to have a second list so we can allow the user to carry on using the list while items are being animated and deleted. When this second list is checked, if it matches the row item it will run the animation. The final trick is to set a listener to when the animation ends, at that point you delete the item from your original list.

FruitAdapter.java

package com.blundell.tut.animatelistview.ui.adapter;

import java.util.ArrayList;

import com.blundell.tut.animatelistview.R;
import com.blundell.tut.animatelistview.domain.fruit.Fruit;
import com.blundell.tut.animatelistview.util.AnimationHelper;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.widget.BaseAdapter;
import android.widget.TextView;

/**
 * This is a custom adapter for our Fruit, these are held in an Array. It uses a ViewHolder for recycling views.
 * It also uses a secondary array, this is a list of items that the user has pressed on to delete. A list is used
 * so that we can animate multiple user interactions. An animation is set on the row the user clicks on, when this animation
 * ends that is when the actual data is deleted.
 *
 * @author paul.blundell
 *
 */
public class FruitAdapter extends BaseAdapter {

	private final ArrayList<Fruit> fruits;
	private final LayoutInflater inflater;
	private final ArrayList<Fruit> deleteableItems;

	public FruitAdapter(ArrayList<Fruit> fruits, LayoutInflater inflater) {
		this.fruits = fruits;
		this.inflater = inflater;
		deleteableItems = new ArrayList<Fruit>();
	}

	@Override
	public int getCount() {
		return fruits.size();
	}

	@Override
	public Object getItem(int position) {
		return fruits.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	public void delete(int position) {
		deleteableItems.add(fruits.get(position));
		notifyDataSetChanged();
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view = convertView;
		// View holder for view recycling
		ViewHolder holder;
		if(view == null){
			view = inflater.inflate(R.layout.list_item_fruit, null);
			TextView type = (TextView) view.findViewById(R.id.textView1);
			TextView name = (TextView) view.findViewById(R.id.textView2);
			TextView color = (TextView) view.findViewById(R.id.textView3);

			holder = new ViewHolder();
			holder.type = type;
			holder.name = name;
			holder.color = color;

			view.setTag(holder);
		} else {
			holder = (ViewHolder) view.getTag();
		}
		// Get the fruit we want for this row
		Fruit item = fruits.get(position);
		// Populate the row
		holder.type.setText(item.type().toString());
		holder.name.setText(item.name());
		holder.color.setText(item.color().toString());
		// Now we check if this row has been 'long pressed' by the user
		// this is our queue to start the animation then delete the row
		checkIfItemHasBeenMarkedAsDeleted(view, item);

		return view;
	}

	// Loop around the list of deleted items
	private void checkIfItemHasBeenMarkedAsDeleted(View view, Fruit item) {
		for (Fruit deletable : deleteableItems) {
			deleteItemIfMarkedAsDeletable(view, item, deletable);
		}
	}

	// Check if this row item has been marked to be deleted
	// if it has we create a new animation
	// attach a listener for when that animation ends
	// then start the animation on our row
	private void deleteItemIfMarkedAsDeletable(View view, Fruit item, Fruit deletable) {
		if(itemIsDeletable(item, deletable)){
			Animation fadeout = AnimationHelper.createFadeoutAnimation();
			deleteOnAnimationComplete(fadeout, item);
			animate(view, fadeout);
		}
	}

	// The item is deletable if the ID for the fruit of this row matches
	// the id of any fruit in our list of deleteable fruits
	private static boolean itemIsDeletable(Fruit item, Fruit deletable) {
		return item.id() == deletable.id();
	}

	// This is our listener for when the delete animate completes
	// We then update our data set (removing the fruit)
	private void deleteOnAnimationComplete(Animation fadeout, final Fruit item) {
		fadeout.setAnimationListener(new AnimationListener() {
			@Override
			public void onAnimationStart(Animation animation) { }
			@Override
			public void onAnimationRepeat(Animation animation) { }

			@Override
			public void onAnimationEnd(Animation animation) {
				fruits.remove(item);
				deleteableItems.remove(item);
				notifyDataSetChanged();
			}
		});
	}

	// actually do the animate on our row
	private static void animate(View view, Animation animation) {
		view.startAnimation(animation);
	}

	private class ViewHolder {
		TextView type;
		TextView name;
		TextView color;
	}
}

Thats it really! It’s simpler than you think, you now just have to declare your new Fruit List in your activity and it should work like a dream. Just remember notifyDataSetChanged() is your friend.

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/textView_list_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/press_and_hold_an_item_to_delete" />

    <com.blundell.tut.animatelistview.ui.widget.FruitListView
        android:id="@+id/listView_fruits"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_below="@id/textView_list_info" />

</RelativeLayout>

MainActivity.java

package com.blundell.tut.animatelistview.ui;

import com.blundell.tut.animatelistview.R;
import com.blundell.tut.animatelistview.domain.Data;
import com.blundell.tut.animatelistview.ui.widget.FruitListView;

import android.app.Activity;
import android.os.Bundle;

/**
 *
 * @author paul.blundell
 *
 */
public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FruitListView list = (FruitListView) findViewById(R.id.listView_fruits);
        list.addFruit(Data.FRUITS);
    }

}

Here’s the Animate Custom Adapter / ListView Tut eclipse source.

And here’s the GitHub Repo: AnimateListViewTut

2 thoughts on “[TUT] Animate Removing From a Custom Adapter (ListView)

  1. Hi, nice tutorial! I just want to ask you: what about adding data in a listview? Is there already a tutorial?
    Thank you!

Leave a Reply

Your email address will not be published. Required fields are marked *