Android Logo MathCS.org - Android

Advanced Lists

java -> android -> advanced lists ...

We have previously created lists, both with a fixed number of elements and dynamically changing lists. They required three elements:

In fact, there is a fourth element needed: a template for displaying individual elements in the list. We have used an element defined in XML for that when we initialized the adapter.

Now we want to define lists with more sophisticated looks, appropriate for structured data. We need several ingredients:

And of course we need a list to display the collection, scroll through it, and register events.

Example:

Say we want to display a list of addresses. Each address, say, consists of a name, a phone number, and an email address. Each name should be displayed bold and large, phone number in a different, non-proportional font, and the email in italics.

Later you could add event-handling capabilities where a long-tap brings up a context menu to dial the phone number, send an email, delete the entry, sort the data, add a new address, or edit an existing entry.

First, create a new Android project as usual, perhaps named AddressBook. Then create the storage class to contain an "address", of course in a new source code file:

public class Address
{
	String first = null;
	String last = null;
	String phone = null;
	String email = null;
	
	public Address()
	{
		this("", "", "", "");
	}
	
	public Address(String first, String last, String phone, String email)
	{
		this.first = first;
		this.last = last;
		this.phone = phone;
		this.email = email;
	}
	
	public String toString()
	{
		return last + ", " + first;
	}
}

Next we create a class to serve a dual purpose: (a) to store a collection of Address items, and (b) to act as adapter between the data and the list. To accomplish (a) we add a field of type ArrayList to the class, as well as methods to add an address, retrieve an address, remove one address, and remove all addresses. As for (b), the class will extend BaseAdapter, which implies that we need to implement the methods:

Most of these methods are self-explanatory, but getView requires more discussion. First, though, here is the complete class:

public class AddressAdapter extends BaseAdapter
{
	private Context context;
	private ArrayList<Address> data = null;
	
	public AddressAdapter(Context context)
	{
		super();
		this.context = context;
		this.data = new ArrayList<Address>();
	}
	
	public void addAddress(Address address)
	{
		data.add(address);
	}
	
	public Address getAddress(int pos)
	{
		return data.get(pos);
	}
	
	public void remove(int pos)
	{
		data.remove(pos);
	}
	
	public void clear()
	{
		data.clear();
	}
	
	public int getCount()
	{
		return data.size();
	}
	
	public Object getItem(int pos)
	{
		return data.get(pos);
	}
	
	public long getItemId(int pos)
	{
		return 0;
	}
	
	public View getView(int pos, View convertView, ViewGroup parent)
	{
		TextView view = null;
		
		if (convertView != null)
			view = (TextView)convertView;
		else
			view = new TextView(context);
		
		view.setText(data.get(pos).toString());
		
		return view;
	}	
}

The getView method is the one that is called to render the item at position pos. It first creates a reference variable view of type TextView. Then it checks it it can recycle the passed view convertView. If so, recycle it, otherwise create a new TextView instance. Then, in either case, set the text of the view to the data at position pos and return it. Note that only TextViews are instantiated when necessary, so that if convertView is not null, it must be a TextView and can safely be typecast as such.

Of course this will not give us a fancy list: each address is rendered by a text view, which we could have accomplished easier (just as in a previous section). However, we can now change the look easily and adapt it to our needs, but first let's turn this into a working activity by implementing the AddressBook class. For simplicity, we will use a ListActivity instead of an Activity, which is easy to use if your activity consists only of one list:

public class AddressBook extends ListActivity 
{
	ListView list = null;
	AddressAdapter adapter = null;
	
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        
        list = getListView();
        adapter = new AddressAdapter(this);
        
        list.setAdapter(adapter);
        
        adapter.addAddress(new Address("John", "Doe", "555-1212", "jdoe@gmail.com"));
        adapter.addAddress(new Address("Jane", "Dear", "555-4567", "jdear@gmail.com"));
    }
}

If we execute this activity, we will see the following, not very exciting, app:

advanced_list_1

Now let's jazz up the look of an item: we will add a "View" class that defines how to render an address and switch to that class in the getView method of our AddressAdapter class. For simplicity and flexibility, we will create the look of an address in code only, but you could certainly use an XML layout file if you prefer. Here is our first iteration of our rendering class:

package org.mathcs.addressbook;

import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.util.TypedValue;
import android.widget.LinearLayout;
import android.widget.TextView;

public class AddressView extends LinearLayout
{
	public static final int BG_COLOR = Color.rgb(0, 0, 100);
	public static final int TXT_COLOR = Color.WHITE;
	public static final int TXT_SIZE_LARGE = 20;
	public static final int TXT_SIZE_SMALL = 14;
	
	private TextView name = null;
	private TextView phone = null;
	private TextView email = null;
	
	public AddressView(Context context)
	{
		super(context);
	
		this.setOrientation(LinearLayout.VERTICAL);
		this.setBackgroundColor(BG_COLOR);
		
		name = new TextView(context);
		name.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD);
		name.setTextColor(TXT_COLOR);
		name.setTextSize(TypedValue.COMPLEX_UNIT_SP, TXT_SIZE_LARGE);
		name.setPadding(2, 5, 2, 5);
		
		phone = new TextView(context);
		phone.setTypeface(Typeface.MONOSPACE, Typeface.NORMAL);
		phone.setTextColor(TXT_COLOR);
		phone.setTextSize(TypedValue.COMPLEX_UNIT_SP, TXT_SIZE_SMALL);
		phone.setPadding(10, 2, 2, 2);
		
		email = new TextView(context);
		email.setTypeface(Typeface.SERIF, Typeface.ITALIC);
		email.setTextColor(TXT_COLOR);
		email.setTextSize(TypedValue.COMPLEX_UNIT_SP, TXT_SIZE_SMALL);
		email.setPadding(10, 2, 2, 2);
		
		LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
		this.addView(name, params);
		this.addView(phone, params);
		this.addView(email, params);
	}

	public void setData(Address address)
	{
		name.setText(address.last + ", " + address.first);
		if (!address.phone.equals(""))
			phone.setText("Phone: " + address.phone);
		if (!address.email.equals(""))
			email.setText("Email: " + address.email);
	}
}

Note that when we define the font size we do so in "scalable" pixels, which will automatically scale the size to accommodate different screen resolutions. However, the padding is in pixels, which is no good since 10 pixels on a screen 100 pixels wide would occupy 10% of the width, but on a screen, say, 1000 pixels wide only takes up 1% of the width. We will, in the final discussion, address that problem, but now let's use our new view.

To use the new view on our data we only have to modify the getView method of the AddressAdapter class as follows:

	public View getView(int pos, View convertView, ViewGroup parent)
	{
		AddressView view = null;

		if (convertView != null)
			view = (AddressView)convertView;
		else
			view = new AddressView(context);
		
		view.setData(data.get(pos));
		
		return view;
	}

This will produce the following, almost perfect, look:

advanced_list2

A few things are not right: The list background should match the item background, the scaling of the padding needs to accommodate for screen resolution, and - important - clicking/tapping on a list item no longer highlights the item as it did before (try it out) to give user feedback. To remedy the problems, we will:

Here are, for easy reference, all three classes in their final form (the Address storage class does not change):

AddressBook.java

public class AddressBook extends ListActivity 
{
	public static final int BG_COLOR = Color.rgb(0, 0, 100);
	public static final int TXT_COLOR = Color.WHITE;
	public static final int TXT_SIZE_LARGE = 20;
	public static final int TXT_SIZE_SMALL = 14;
	public static final PaintDrawable LIST_SELECTOR = 
			new PaintDrawable(Color.argb(150, 100, 100, 200));
	
	ListView list = null;
	AddressAdapter adapter = null;
	
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        
        list = getListView();
        adapter = new AddressAdapter(this);
        
        list.setAdapter(adapter);
        list.setBackgroundColor(BG_COLOR);
        list.setSelector(LIST_SELECTOR);
        list.setDrawSelectorOnTop(true);

        adapter.addAddress(new Address("John", "Doe", "555-1212", "jdoe@gmail.com"));
        adapter.addAddress(new Address("Jane", "Dear", "555-4567", "jdear@gmail.com"));
    }
}

AddressView.java

public class AddressView extends LinearLayout
{
	private TextView name = null;
	private TextView phone = null;
	private TextView email = null;
	
	public AddressView(Context context, float scale)
	{
		super(context);
	
		this.setOrientation(LinearLayout.VERTICAL);
		this.setBackgroundColor(AddressBook.BG_COLOR);
		
		name = new TextView(context);
		name.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD);
		name.setTextColor(AddressBook.TXT_COLOR);
		name.setTextSize(TypedValue.COMPLEX_UNIT_SP, AddressBook.TXT_SIZE_LARGE);
		name.setPadding(2, 5, 2, 5);
		
		phone = new TextView(context);
		phone.setTypeface(Typeface.MONOSPACE, Typeface.NORMAL);
		phone.setTextColor(AddressBook.TXT_COLOR);
		phone.setTextSize(TypedValue.COMPLEX_UNIT_SP, AddressBook.TXT_SIZE_SMALL);
		phone.setPadding((int)(10*scale), 2, 2, 2);
		
		email = new TextView(context);
		email.setTypeface(Typeface.SERIF, Typeface.ITALIC);
		email.setTextColor(AddressBook.TXT_COLOR);
		email.setTextSize(TypedValue.COMPLEX_UNIT_SP, AddressBook.TXT_SIZE_SMALL);
		email.setPadding((int)(10*scale), 2, 2, 2);
		
		LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
		this.addView(name, params);
		this.addView(phone, params);
		this.addView(email, params);
	}

	public void setData(Address address)
	{
		name.setText(address.last + ", " + address.first);
		if (!address.phone.equals(""))
			phone.setText("Phone: " + address.phone);
		if (!address.email.equals(""))
			email.setText("Email: " + address.email);
	}
}

AddressAdapter.java

public class AddressAdapter extends BaseAdapter
{
	private Context context;
	private ArrayList<Address> data = null;
	private float scale = 1.0f;
	
	public AddressAdapter(Context context)
	{
		super();
		this.context = context;
		this.data = new ArrayList<Address>();
        this.scale = context.getResources().getDisplayMetrics().density;
	}
	
	public void addAddress(Address address)
	{
		data.add(address);
	}
	
	public Address getAddress(int pos)
	{
		return data.get(pos);
	}
	
	public void remove(int pos)
	{
		data.remove(pos);
	}
	
	public void clear()
	{
		data.clear();
	}
	
	public int getCount()
	{
		return data.size();
	}

	public Object getItem(int pos)
	{
		return data.get(pos);
	}

	public long getItemId(int pos)
	{
		return 0;
	}

	public View getView(int pos, View convertView, ViewGroup parent)
	{
		AddressView view = null;

		if (convertView != null)
			view = (AddressView)convertView;
		else
			view = new AddressView(context, scale);
		
		view.setData(data.get(pos));
		
		return view;
	}
}

This will produce a neat layout where an address when clicked changes to light blue:

advanced_list3

Optional: Sorting the List

It is exceedingly easy to add a sort method to our adapter so the list can be sorted if desired:

public class Address implements Comparable<Address> 
{
	// no changes except for adding the interface and implementing:
	public int compareTo(Address other )
	{
		int compLastNames = last.compareTo(other.last);
		if (compLastNames == 0)
			return first.compareTo(other.first);
		else
			return compLastNames;
	}
}

public class AddressAdapter extends BaseAdapter
{
	// no change except for:
	
	public void sortAddresses()
	{
		Collections.sort(data);
	}
}

Note:

Whenever you call a method that changes the address data and you want to see your changes, do not forget to call on the adapter's notifyDataSetChanged() method, otherwise the list will not refresh!

Questions: