Sunday 25 March 2012

Custom ListView in Android

Ever wondered how to implement a ListView that displays content that YOU want??  If  the answer is yes,then READ ON!!! :)

Here,we'll be building a ListView that displays an ImageView and two TextViews.Something like this:

Each row will show an image,the name of a player and his team.

First step: Create the layout file that will display a row in the ListView.Mine is players.xml and here it is:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <ImageView 
        android:id="@+id/photo"
        android:layout_width="55dp"
        android:layout_height="70dp"
        android:layout_alignParentLeft="true"
        android:scaleType="fitXY"/>
    
    <TextView 
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/photo"
        android:textSize="18sp"/>
    
    <TextView 
        android:id="@+id/team"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/photo"
        android:layout_below="@id/name"/>
    
</RelativeLayout>
Now that you have the layout,lets get on to the coding part.
Create your Acivity by extending ListActivity
public class CustomListActivity extends ListActivity {
 //some code goes here
}
The important thing to note here is that ListActivity HAS AN IMPLICIT LAYOUT associated
with it.This default layout contains a ListView that fills the screen.You are not required to define any other layout for the ListActivity,although its perfectly fine to do so.:).

I've used the onCreate method of my ListActivity to define the data that will be displayed in the ListView and to set the adapter.
public class CustomListViewActivity extends ListActivity {
 
 //the ArrayList that will hold the data to be displayed in the ListView 
 ArrayList<HashMap<String, Object>> players;
 LayoutInflater inflater;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        //not necessary as ListActivity has an 
        //implicitly defined Layout(with a ListView of course)
        //setContentView(R.layout.main);   
        
        //get the LayoutInflater for inflating the customomView
        inflater=(LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        
        //these arrays are just the data that 
        //I'll be using to populate the ArrayList
        //You can use our own methods to get the data
        String names[]={"Ronaldo","Messi","Torres","Iniesta",
          "Drogba","Gerrard","Rooney","Xavi",};
       
        String teams[]={"Real Madrid","Barcelona","Chelsea","Barcelona","Chelsea","Liverpool","ManU","Barcelona"};
        Integer[] photos={R.drawable.cr7,R.drawable.messi,R.drawable.torres,R.drawable.iniesta,
          R.drawable.drogba,R.drawable.gerrard,
          R.drawable.rooney,R.drawable.xavi};
        
       players=new ArrayList<HashMap<String,Object>>();
       
       //HashMap for storing a single row
       HashMap<String , Object> temp;
       
       //total number of rows in the ListView
       int noOfPlayers=names.length;
       
       //now populate the ArrayList players
       for(int i=0;i<noOfPlayers;i++)
       {
     temp=new HashMap<String, Object>();
     
     temp.put("name", names[i]);
     temp.put("team", teams[i]);    
     temp.put("photo", photos[i]);
     
     //add the row to the ArrayList
     players.add(temp);        
       }
       
       //create the adapter
       //first param-the context
       //second param-the id of the layout file 
       //you will be using to fill a row
       //third param-the set of values that
       //             will populate the ListView
       CustomAdapter adapter=new CustomAdapter(this, R.layout.players_layout,players); 
   
       //finally,set the adapter to the default ListView
       setListAdapter(adapter);
    }
This is whats been done here:
1. Set the names,teams and the drawables for each row as arrays.
2. Add all of these to an ArrayList players.
   Hence,now we have all the data to be displayed in the ArrayList players sound and safe.

Now comes the important part: Creating your CustomAdapter
The adapter is the heart of the ListView.It provides the ListView with the data and the Views that will be displayed in the list.
The default implementation of the ArrayAdapter provides only  a simple TextView as a list item.However,its fairly simple to create your own adapter to replace them with custom layouts.
All you have to do is define a class which extends the ArrayAdapter class and Override its getView() method. The ListView will then set the View returned by getView() as a list item.Note that getView() will be called every time a list item is displayed.
This is how its done:


//define your custom adapter
 private class CustomAdapter extends ArrayAdapter<HashMap<String, Object>>
 {
     
   public CustomAdapter(Context context, int textViewResourceId,
    ArrayList<HashMap<String, Object>> Strings) {
      
     //let android do the initializing :)
 super(context, textViewResourceId, Strings);
 }

     
       //class for caching the views in a row  
  private class ViewHolder
  {
   ImageView photo;
   TextView name,team;
   
  }
  
  ViewHolder viewHolder;

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
  
   if(convertView==null)
   {        
                                //inflate the custom layout                        
    convertView=inflater.inflate(R.layout.players_layout, null);
    viewHolder=new ViewHolder();
    
    //cache the views
    viewHolder.photo=(ImageView) convertView.findViewById(R.id.photo);
    viewHolder.name=(TextView) convertView.findViewById(R.id.name);
    viewHolder.team=(TextView) convertView.findViewById(R.id.team);
    
    //link the cached views to the convertview
    convertView.setTag(viewHolder);
    
   }
   else
    viewHolder=(ViewHolder) convertView.getTag();
   
  
  int photoId=(Integer) players.get(position).get("photo");
   
      //set the data to be displayed
      viewHolder.photo.setImageDrawable(getResources().getDrawable(photoId));
   viewHolder.name.setText(players.get(position).get("name").toString());
   viewHolder.team.setText(players.get(position).get("team").toString());
  
   //return the view to be displayed
  return convertView;
  }
      
    }

I'll explain what just happened here.
The getView() method is called everytime android needs to display a list item.
We may inflate the layout and return it everytime;but android provides a brilliant yet simple mechanism for re-using the inflated Views.
The convertView parameter passed to getView() is the View recycling mechanism of android.
Android only creates the Views that are visible to the user.Sometimes,maybe one or two extra.
As the user scrolls,the View of the list item that leaves the screen can be re-used to display the list item that enters the screen.
And this recycled View is given to the getView() method as a parameter.
Thus,we require to inflate only when convertView is null.

That being said,how do you effectively re-use the convertView?? That's where the ViewHolder class comes into play.
Every time  convertView is null,the layout is inflated,and an instance of the ViewHolder class is created to hold the inflated views.This instance is then tagged to the convertView.
If  getView()  gives a non- null convertView,then simply get the already tagged ViewHolder from it and use it.

Thus effectively,in a list with a 1000 items,we can bring down the layout inflation from 1000 times to maybe 5 or 6 times!!! Imagine the performance enhancement this technique will give,given that inflating and parsing the View hierarchy is a costly affair.

2 comments: