21
Nov
10

Android ListView Custom ViewAdapter


This page describes the process of creating an Android application that displays a list of countries in a ListView component. The ListView will be fed data using a customized ViewAdapter. The custom adapter will display a different flag icon for each country. The list as well as the flags will be stored locally as part of the application install package. The application will later on be modified to add a please wait message while the application and data is loading.

Background

The ListView is one of the more popular layout managers on the Android OS. We will be using an ArrayAdapter to read an ArrayList of Items to display on the page.

Requirements

Create a New Project

  1. In eclipse Right click New -> project, choose “Android Project”
  2. Project Name: AndroidCountryFlagList
  3. Package name: com.test
  4. Create Activity: CountryFlagActivity
  5. Click Finish

Java Code

We start by defining the POJO class that will hold information about each country.

src/com/test/CountryItem.java

package com.test;

public class CountryItem {
	private String isoCode;
	private String name;

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getIsoCode() {
		return isoCode;
	}
	public void setIsoCode(String isoCode) {
		this.isoCode = isoCode;
	}
}

Country DataManager

Responsible for getting the list of countries. A Thread.sleep line was added to make slow on purpose.

src/com/test/CountryDataManager.java

package com.test;
 
import java.util.ArrayList;
import java.util.List;
 
import android.util.Log;
 
public class CountryDataManager {
 
    final String[][] COUNTRIES = new String[][] {
            {"af", "Afghanistan"}
            ,{"al", "Albania"}
            ,{"dz", "Algeria"}
            ,{"as", "American Samoa"}
            ,{"ad", "Andorra"}
            ,{"ao", "Angola"}
            ,{"ai", "Anguilla"}
            ,{"aq", "Antarctica"}
            ,{"ag", "Antigua and Barbuda"}
            ,{"ar", "Argentina"}
            ,{"am", "Armenia"}
            ,{"aw", "Aruba"}
            ,{"au", "Australia"}
            ,{"at", "Austria"}
            ,{"az", "Azerbaijan" }
    };
 
    public List<CountryItem> getCountries(){
        ArrayList<CountryItem> m_countries = null;
 
        try{
          m_countries = new ArrayList<CountryItem>();
 
          for(String[] country : COUNTRIES) {
              CountryItem o1 = new CountryItem();
              o1.setIsoCode(country[0]);
              o1.setName(country[1]);
              m_countries.add(o1);
          }
 
            Thread.sleep(5000); // make this data manager be slow on purpose
          } catch (Exception e) {
          }
          return m_countries;
      }
}

Properties

This file allows for internationalization of your mobile application.

res/values/string.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Country Flag List</string>
    <string name="main_no_items">No orders to display</string>
</resources>

Layout Files

The main screen will have a list of items followed by a place holder to indicate when there is no data to display.

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   >
	<ListView
	    android:id="@+id/android:list"
	    android:layout_width="fill_parent"
	    android:layout_height="fill_parent"
	    />
	<TextView
	    android:id="@+id/android:empty"
	    android:layout_width="fill_parent"
	    android:layout_height="fill_parent"
	    android:text="@string/main_no_items"/>
</LinearLayout>

The next file defines the layout at each row of the List view. We will have an icon to the left of the text.

res/layout/row.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="wrap_content"
    android:orientation="horizontal">
 
    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon"/>
 
    <TextView
        android:id="@+id/toptext"
        android:padding="5dp"
        android:textSize="10pt"
        android:singleLine="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
 
</LinearLayout>

Country Adapter

The following class is the heart of what makes this all work. The ListView will call this adapter on each row, the adapter’s responsibility is to return data for that row. The adapter has intimate knowledge of the components in the current row and is able to “inject” text or data into those components. For example in line 33 and 37 the adapter knows that there is a TextView and a ImageView component on the current row.

src/com/test/CountryAdapter.java

package com.test;
 
import java.util.List;
 
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
 
class CountryAdapter extends ArrayAdapter<CountryItem> {
 
    private List<CountryItem> items;
 
    public CountryAdapter(Context context, int textViewResourceId,
            List<CountryItem> items) {
        super(context, textViewResourceId, items);
        this.items = items;
    }
 
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View v = convertView;
        if (v == null) {
            LayoutInflater vi = (LayoutInflater) getContext().getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
            v = vi.inflate(R.layout.row, null);
        }
        CountryItem o = items.get(position);
        if (o != null) {
            TextView tt = (TextView) v.findViewById(R.id.toptext);
            if (tt != null) {
                tt.setText(o.getName());
            }
            ImageView im = (ImageView) v.findViewById(R.id.icon);
            if (im != null) {
                im.setImageDrawable(getContext().getResources().getDrawable(
                        getContext().getResources().getIdentifier(
                                "@drawable/" + o.getIsoCode(), "drawable",
                                getContext().getPackageName())));
            }
        }
        return v;
    }
}

List Activity

This is the main activity of the application.

src/com/test/CountryFlagActivity.java

package com.test;

import java.util.ArrayList;
import java.util.List;

import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;

public class CountryFlagActivity extends ListActivity {

    private CountryAdapter countryAdapter;
    private static final CountryDataManager countryDataManager 
    = new CountryDataManager();
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
                
        this.countryAdapter = new CountryAdapter(this, R.layout.row, 
        		new ArrayList<CountryItem>());
        setListAdapter(this.countryAdapter);
        
        List<CountryItem> countryList = countryDataManager.getCountries();
        
        for(CountryItem country : countryList) {
        	countryAdapter.notifyDataSetChanged();
        	countryAdapter.add(country);        	
        }
        countryAdapter.notifyDataSetChanged();
        
		getListView().setOnItemClickListener(new OnItemClickListener() {
			public void onItemClick(AdapterView<?> parent, View view,
					int position, long id) {
				Toast.makeText(getApplicationContext(),
					((TextView)((LinearLayout) view).getChildAt(1)).getText(), 
						Toast.LENGTH_SHORT).show();
			}
		});
		
    }

}

Icons

Right click on each one of these icons and “Save Link As”. Save these image files into “res/drawable-hdpi” and leave the name of the file the same.

Once all the files are saved into the res/drawable-hdpi then copy all the images into the two other folders “drawable-ldpi” and “drawable-mdpi”. Since I have an HTC Evo phone it always looks in the hdpi folder for me. Depending on the phone you have it might look into these other folders. You may need to resize the icons to be smaller… I will write more about this in a future blog but for now lets hope the copying worked for you.

Run The Application

At this point save all the files and right click on the project and select run as Android application. The phone should start the application and after a brief pause it should show you a list of countries with their associated flags.

Displaying a Please Wait Message

The data manager purposefully contains a line that makes it sleep for a few seconds to simulate a slow data connection. During this time the application is not responsive and does not display a message explaining what is happening. This is a bad user experience. To fix this we will display a message to the user letting them know what is going on and behind the scenes we will retrieve the data using a Thread.

In order to make this work we need to make a slight change to the CountryFlagActivity.java class. Make your class look like the one below. The text that is different is highlighted in gray.

package com.test;

import java.util.ArrayList;
import java.util.List;

import android.app.ListActivity;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;

public class CountryFlagActivity extends ListActivity {

    private CountryAdapter countryAdapter;
    private static final CountryDataManager countryDataManager
    = new CountryDataManager();

    private ProgressDialog m_ProgressDialog = null;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        this.countryAdapter = new CountryAdapter(this, R.layout.row,
                new ArrayList<CountryItem>());
        setListAdapter(this.countryAdapter);

        Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				final List<CountryItem> countryList = countryDataManager.getCountries();
				runOnUiThread(new Runnable() {
					@Override
					public void run() {
				        for(CountryItem country : countryList) {
				            countryAdapter.notifyDataSetChanged();
				            countryAdapter.add(country);
				        }
				        m_ProgressDialog.dismiss();
				        countryAdapter.notifyDataSetChanged();
					}
				});
			}
		});

        t.start();

        // shows a please wait while the data loads.
        m_ProgressDialog = ProgressDialog.show(CountryFlagActivity.this,
                "Please wait...", "Retrieving data ...", true);

        getListView().setOnItemClickListener(new OnItemClickListener() {
            public void onItemClick(AdapterView parent, View view,
                    int position, long id) {
                Toast.makeText(getApplicationContext(),
                    ((TextView)((LinearLayout) view).getChildAt(1)).getText(),
                        Toast.LENGTH_SHORT).show();
            }
        });
    }
}

Threading is necessary to allow stuff to happen in the background. It allows applications to be more responsive.

The call to “runOnUiThread()” on line 38 is necessary because the system does not allow non-UI threads to change the state of the countryAdapter object. The “runOnUiThread()” accepts an object that implements runnable. In this we just declare a anonymous class that does the work of adding data to and notifying the countryAdapter.

Java Code

After running the application again you should see a message like this for a few seconds before displaying the list of countries.

Up Next

Getting the list of countries and their flag icons from a remote site.

References

Advertisements

6 Responses to “Android ListView Custom ViewAdapter”


  1. 1 Radovan
    November 24, 2010 at 9:46 am

    This is just what i was looking for. Unfortunately i don’t know how to implement this code to appear when i click button. I don’t want it to appear across screen immediately, but when after clicking on my button like here: http://android-er.blogspot.com/2009/10/listview-and-listactivity.html.
    Do you have some suggestions?

  2. 3 david
    December 12, 2010 at 4:34 pm

    pls what is IsoCode attributte used for?


Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 75 other followers

November 2010
S M T W T F S
« Oct   Dec »
 123456
78910111213
14151617181920
21222324252627
282930  

Blog Stats

  • 813,810 hits

%d bloggers like this: