NC

Building Custom Android ListViews

The documentation for Android’s ListView’s is a little sparse. The examples around on the web are also not too great. This article intends to be the one I was searching for in trying to understand how to display some more advanced data, and deal with events.

Introduction

ListView’s are the solution to most data problems on Android (much like UITableView is used extensively in iOS). However, they are a little undocumented. The rest of this article should allow you to go from having a basic ListView, to one much more complex and useful.

The Project

This is implemented using Android 2.2, but it will work with more recent versions. These are the project settings used:

Project name: HelloListView
Build target: Android 2.2
Application name: CustomListViews
Package name: org.example.hellolistview
Create Activity: HelloListView
Min SDK Version: 8

Prerequisites

Firstly, I assume you have tried building a basic ListView (This tutorial is good for that). Secondly, I assume your data source is an ArrayList, containing objects for each element of data.

Once you have followed the basics of this guide, you will find that you can use the latter sections as you wish.

At 2010’s Google IO, there was an hour long session which talked about ListView’s extensively, you may wish to watch that first. You’ll find it here.

Building the Foundations

This starts with building a basic view which is backed onto an ArrayList. You can expand on the complexity from here.

Extending ListActivity

The first step is to subclass ListActivity instead of Activity. This provides us with some functionality specific to lists. Of note, if we have no data we can easily provide an alternative.

public class HelloListView extends ListActivity {
    // ....
}

The View XML (main.xml, list_item.xml)

main.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout 
	xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    
    <ListView 
    	android:id="@android:id/list"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    
    <TextView
    	android:id="@android:id/empty"
    	android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/empty" />
</LinearLayout>

This defines the main view. It contains a layout which fills the screen, which in turn contains two subviews. The ListView defines the view, and the TextView is the backup which is called by ListActivity when there is no data to display.

list_item.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout 
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="wrap_content" 
	android:layout_height="wrap_content" 
	android:background="#000000">
	<TextView 
		android:layout_width="wrap_content"
		android:layout_height="wrap_content" 
		android:id="@+id/text">
	</TextView>
</LinearLayout>

This defines a very basic view. A screenshot is shown below. You do need to provide android:layout_width and android:layout_height declarations for each, otherwise it will not render.

ListView Basics
ListView Basics

Data Source and Adapter

To display what is shown in the screenshot above, the following code is used. It’s not necessarily the most concise, but you should find it simple to follow.

HelloListView.java

public class HelloListView extends ListActivity {
	// define the data source
	private ArrayList<String> data;
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // setup the data source
        this.data = new ArrayList<String>();
        
        // add some objects into the array list
        this.data.add("List Item 1");
        this.data.add("List Item 2");
        this.data.add("List Item 3");
        
        // use main.xml for the layout
        setContentView(R.layout.main);
        
        // setup the data adaptor
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.list_item, R.id.text, this.data);
        
        // specify the list adaptor
        setListAdapter(adapter);
    }
}

Here, we are creating the ArrayList holding our data set. Then we are adding the three elements we wish to display.

After this, we are setting up the ArrayAdapter to bridge the ListView to the dataset, and the specific item in the list.

The ArrayAdaptor translates the objects given to it (the last parameter) using a .toString(). This places the value inside the element with id @+id/text inside the list_item.xml file.

Displaying Custom Objects

For the rest of this article, the ListItem class is going to be used to display inside the ListView. This contains two members, title and subtitle. This is enough to show off using a custom adaptor.

ListItem.java

/*
* Defines a simple object to be displayed in a list view.
*/
package org.example.HelloListView;

public class ListItem {
	public String title;
	public String subTitle;
	
	// default constructor
	public ListItem() {
		this("Title", "Subtitle");
	}
	
	// main constructor
	public ListItem(String title, String subTitle) {
		super();
		this.title = title;
		this.subTitle = subTitle;
	}
	
	// String representation
	public String toString() {
		return this.title + " : " + this.subTitle;
	}
}

There are also a few changes to be made to the rest of the project to get this to work.

HelloListView.java

// setup the data source
this.data = new ArrayList<ListItem>();

// add some objects into the array list
ListItem item = new ListItem("Hello", "Nick");
this.data.add(item);

You will also need to change any references to ArrayList<String> to ArrayList<ListItem>.

By default, this will still display in the current incarnation. This is because we’re simply outputting a string representation of the object. To display more complex information, the SimpleAdaptor class can render checkable objects (like a CheckBox), Strings and Images. Anything more complicated would require building a custom data adaptor.

ListView Custom Object
ListView Custom Object

Complex ListViews with SimpleAdapter

SimpleAdapter can be used for building more complex ListViews. Compared to the ArrayAdapter class, SimpleAdaptor takes a few more arguments to map more data to more views.

Unfortunately, SimpleAdapter requires a collection of Maps to define the data. There are two ways to put together the objects wanted, the first is to create all new objects, and the second is to build a simple wrapper around Map on our original ListItem class. The former is shown below:

Creating new Objects

HelloListView.java

public class HelloListView extends ListActivity {
	// define the data source
	private ArrayList<Map> data;
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // setup the data source
        this.data = new ArrayList<Map>();
        
        // add some objects into the array list
        Map m = new HashMap();
        m.put("title", "Hello");
        m.put("subtitle", "Nick");
        
        this.data.add(m);
        
        // use main.xml for the layout
        setContentView(R.layout.main);
        
        // setup the data adaptor
        String[] from = {"title", "subtitle"};
        int[] to = {R.id.title, R.id.subtitle};
        SimpleAdapter adapter = new SimpleAdapter(this, (List<? extends Map<String, ?>>) this.data, R.layout.list_item, from, to);
        
        // specify the list adaptor
        setListAdapter(adapter);
    }
}

list_item.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout 
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="wrap_content" 
	android:layout_height="wrap_content" 
	android:background="#000000">
	
	<TextView 
		android:layout_width="wrap_content"
		android:layout_height="wrap_content" 
		android:id="@+id/title">
	</TextView>
	
	<TextView 
		android:layout_width="wrap_content"
		android:layout_height="wrap_content" 
		android:id="@+id/subtitle">
	</TextView>
		
</LinearLayout>

Here, we are creating a new object (which is a Map), and placing this into our collection. This is then passed into SimpleAdapter.

Extending Map

By extending Map, we can adjust our ListItem class to appear to be a Map. Here, we will use the members of the class as the key, and their values, as the values.

ListItem.java

public class ListItem implements Map<String, String> {
	public String title;
	public String subTitle;
	
	// default constructor
	public ListItem() {
		this("Title", "Subtitle");
	}
	
	// main constructor
	public ListItem(String title, String subTitle) {
		super();
		this.title = title;
		this.subTitle = subTitle;
	}
	
	// String representation
	public String toString() {
		return this.title + " : " + this.subTitle;
	}
	
	// Map interface classes
	
	// return a count of our members
	public int size() {
		return 2;
	}
	
	// set the values of the object to null
	public void clear() {
		this.title = null;
		this.subTitle = null;
	}
	
	// return all of the values as a collection
	public ArrayList<String> values() {
		ArrayList<String> list = new ArrayList<String>();
		
		list.add(title);
		list.add(subTitle);
		
		return list;
	}
	
	// if the values of the members are null, return true
	public boolean isEmpty() {
		if ((this.title == null) && (this.subTitle == null)) {
			return true;
		} else {
			return false;
		}
	}
	
	// return a set of the members
	public Set<String> keySet() {
		Set<String> s = new HashSet<String>();
		
		s.add("title");
		s.add("subTitle");
		
		return s;
	}
	
	// return a set of the member values
	public Set entrySet() {
		Set<String> s = new HashSet<String>();
		
		s.add(this.title);
		s.add(this.subTitle);
		
		return s;
	}
	
	// return the value of the given key
	public String get(Object key) {
		if (key.equals("title")) {
			return this.title;
		}
		if (key.equals("subTitle")) {
			return this.subTitle;
		}
		// if we can't return a value, throw the exception
		throw new ClassCastException();
	}
	
	// set the value of a given key
	public String put(String key, String value) {
		if (key.equals("title")) {
			this.title = value;
		}
		if (key.equals("subTitle")) {
			this.subTitle = value;
		}
		return value;
	}
	
	// remove a key (nullify)
	public String remove(Object key) {
		String value = null;
		if (key.equals("title")) {
			value = this.title;
			this.title = null;
		}
		if (key.equals("subTitle")) {
			value = this.subTitle;
			this.subTitle = null;
		}
		return value;
	}
	
	// return boolean if we have a member
	public boolean containsKey(Object key) {
		if (key.equals("title")) {
			return true;
		}
		if (key.equals("subTitle")) {
			return true;
		}
		return false;
	}
	
	// return boolean if we have a member's value
	public boolean containsValue(Object value) {
		if (value.equals(this.title)) {
			return true;
		}
		if (value.equals(this.subTitle)) {
			return true;
		}
		return false;
	}

	// set the values of this map to that of another
	public void putAll(Map<? extends String, ? extends String> arg0) {
		// we only need the stub.
	}
	
}

This implements all of the methods required by the Map interface. You may not need all of them to support SimpleAdapter. I’d suggest subclassing the class above, and making this abstract. Then you can implement just what you need.

In HelloListView.java, the original object, then map declarations can then be replaced with:

// add some objects into the array list
ListItem item = new ListItem("Hello", "Nick");
    
this.data.add(item);

The view will then look like the previous screenshot.

Creating a Custom Data Adaptor

ArrayAdapter provides a simple way to add an array of strings to a ListView. SimpleAdapter provides a way to specify a more complex object (mostly containing strings) and place those into a ListView.

However, if you want to do anything more complicated you need to roll your own Adaptor. The Data Adaptor provides the link between the data and the View. It implements the methods of BaseAdapter to provide what is needed by the ListView. Here, we are assuming that you wish to stick with XML for layout (it’s the suggested way). If you wish to do it just in code, here’s an example.

For this section, we are starting again with the basic list view implemented earlier.

HelloListView.java

public class HelloListView extends ListActivity {
	private ArrayList<ListItem> data;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        // setup the data source
        this.data = new ArrayList<ListItem>();
        
        // create some objects
        ListItem item1 = new ListItem("Title", "Subtitle");
        
        // add them into the array list
        this.data.add(item1);
        
        // use main.xml for the layout
        setContentView(R.layout.main);
        
        // setup the data adaptor
        CustomAdapter adapter = new CustomAdapter(this, R.layout.list_item, this.data);
        
        // specify the list adaptor
        setListAdapter(adapter);
    }
}

CustomAdapter.java

public class CustomAdapter extends BaseAdapter {
	// store the context (as an inflated layout)
	private LayoutInflater inflater;
	// store the resource (typically list_item.xml)
	private int resource;
	// store (a reference to) the data
	private ArrayList<ListItem> data;
	
	/**
	 * Default constructor. Creates the new Adaptor object to
	 * provide a ListView with data.
	 * @param context
	 * @param resource
	 * @param data
	 */
	public CustomAdapter(Context context, int resource, ArrayList<ListItem> data) {
		this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
		this.resource = resource;
		this.data = data;
	}
	
	/**
	 * Return the size of the data set.
	 */
	public int getCount() {
		return this.data.size();
	}
	
	/**
	 * Return an object in the data set.
	 */
	public Object getItem(int position) {
		return this.data.get(position);
	}
	
	/**
	 * Return the position provided.
	 */
	public long getItemId(int position) {
		return position;
	}

	/**
	 * Return a generated view for a position.
	 */
	public View getView(int position, View convertView, ViewGroup parent) {
		// reuse a given view, or inflate a new one from the xml
		View view;
		 
		if (convertView == null) {
			view = this.inflater.inflate(resource, parent, false);
		} else {
			view = convertView;
		}
		
		// bind the data to the view object
		return this.bindData(view, position);
	}
	
	/**
	 * Bind the provided data to the view.
	 * This is the only method not required by base adapter.
	 */
	public View bindData(View view, int position) {
		// make sure it's worth drawing the view
		if (this.data.get(position) == null) {
			return view;
		}
		
		// pull out the object
		ListItem item = this.data.get(position);
		
		// extract the view object
		View viewElement = view.findViewById(R.id.title);
		// cast to the correct type
		TextView tv = (TextView)viewElement;
		// set the value
		tv.setText(item.title);
		
		viewElement = view.findViewById(R.id.subTitle);
		tv = (TextView)viewElement;
		tv.setText(item.subTitle);
		
		// return the final view object
		return view;
	}
}

list_item.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout 
   	xmlns:android="http://schemas.android.com/apk/res/android"
   	android:layout_width="wrap_content" 
   	android:layout_height="wrap_content" 
   	android:background="#000000">
   	<TextView 
   		android:layout_width="wrap_content"
   		android:layout_height="wrap_content" 
   		android:id="@+id/title">
   	</TextView>
   	<TextView 
   		android:layout_width="wrap_content"
   		android:layout_height="wrap_content" 
   		android:id="@+id/subTitle">
   	</TextView>
</LinearLayout>

The same source as above was used for the ListItem class.

The bindData method is where the most customisation will be required. This extracts the given view objects (the XML TextView, in this case), and binds the value of the members to it. Here, we have just used TextViews, but something similar would be used for other parts of the view.

Handling Events

Single Taps

The most obvious case for handling events is handling when a user taps (or clicks) a row. To do this, you define the onItemClick() method, and then inside this you can extract the original object back out to do stuff with it.

    ListView lv = getListView();
    lv.setTextFilterEnabled(true);

    lv.setOnItemClickListener(new OnItemClickListener() {
      public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    	  // When clicked, show a toast with the TextView text
    	  Toast.makeText(getApplicationContext(), parent.getItemAtPosition(position).toString(),
            Toast.LENGTH_SHORT).show();
      }
    });

This defines the on onItemClick() method for handling the event. When a user taps on the item, it prints out the string representation of the object. It uses “Toast”, Android’s discrete notifications class.

The important call is parent.getItemAtPosition(position). This extracts the selected object from the ListAdapter.

Long Taps

“long taps” are perceived to be the equivalent to right-clicks on the desktop. On a list item, you would take this to mean a desire for more information about an item.

Implementing LongClick is much the same as normal clicks (taps). The difference is merely in the method calls which are defined:

lv.setOnItemLongClickListener(new OnItemLongClickListener() {
    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
      	  // When clicked, show a toast with the TextView text
      	  Toast.makeText(getApplicationContext(), "You long clicked on: " + parent.getItemAtPosition(position).toString(),
              Toast.LENGTH_SHORT).show();
      	  
      	  return true;
    }
});

Adjusting Data in the View

This is a bit of a hack. The intention here is to show how to add and notify the adaptor of changes, rather than suggest a good way to go about doing it.

Adding New Data

The simplest way to demonstrate this is to make the list item duplicate itself on tap (or click). Add the following before the Toast declarations, and the data model will be updated:

// on press, duplicate the object
ListItem item = (ListItem)parent.getItemAtPosition(position);
ListItem newItem = new ListItem(item.title, item.subTitle);
data.add(newItem);

The next step is to notify the ListView that the data is invalid. This will reload the data from the data model, and the new object will appear.

// get the adaptor
SimpleAdapter adapter = (SimpleAdapter)parent.getAdapter();
adapter.notifyDataSetChanged();

The same goes for handling editing and deleting functions on the underlying data. You just need to make sure you keep adjust the data set, and keep the adapter informed - then the most recent data is both saved and displayed.

Code

For each of the sections in this article, I have put together a set of examples. They are Zip archives of the Eclipse projects which were created whilst I was writing this.

The projects are targeted at Android 2.2, and were used with Eclipse 3.5.2 (Helios). The code is licensed under the MIT license.

You can find the code here.

Further Reading

To move along from here, I would suggest reading Chapter 9 (Putting SQL to Work) of Hello, Android (3rd Edition) by Ed Burnette. This provides a basic introduction to ListViews, but more importantly talks about hooking up a ListView to SQLite.

As mentioned earlier, the basic ListView tutorial and the ListActivity Class Reference should also be of use to you.

Conclusion

Android’s ListView is pretty powerful, unfortunately, the documentation isn’t go great. Hopefully this will give people new to Android a kick-start in using the ListView.

Thanks to Paul Hallet for reviewing this before posting.