20 September 2014

Android listview with differing rows


One of the Android questions I regularly see on stack overflow is how to have a listview with different rows. Different images, different highlights or whatever the UI calls for. I've answered the question more than once myself but as it comes up so often, I thought I'd write a article on it. Plus it lets me highlight a few extra points around adapters and caching that I often see missed.

So for this tutorial I'm going to create a super simple example of a few listview rows with differing color backgrounds. Hopefully from this super simple example you can see how you might add an imageview or various text elements, or whatever your heart my fancy.

First lets say each element in our listview is going to be an international rugby player. Now these big chaps all come from different countries so we'll identify them as such. First we need a rugby player object:


package example.com.multiitemlistview;

public class rugbyPlayer {

    public static final int COUNTRY_ENGLAND = 1;
    public static final int COUNTRY_NZ = 2;
    public static final int COUNTRY_AUS = 3;
    public static final int COUNTRY_SA = 4;

    private String playerName;
    private int countryId;

    public rugbyPlayer() {}

    public rugbyPlayer(String playerName, int countryId) {
        this.playerName = playerName;
        this.countryId = countryId;
    }

    public String getPlayerName(){
        return this.playerName;
    }

    public void setPlayerName(String playerName){
        this.playerName = playerName;
    }

    public int getCountryId(){
        return this.countryId;
    }

    public void setCountryId(int countryId){
        this.countryId = countryId;
    }
}

Note that I've added some public magic numbers to help easily identify where the rugbyPlayer is from.

Now our 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">

    <ListView
        android:id="@+id/activity_main_listview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</RelativeLayout>

and ActivityMain.java, nothing here should be rocket science, but take a minute to note that we create an ArrayList of players which we then pass in as the third argument to the adapter.

package example.com.multiitemlistview;

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

import java.util.ArrayList;


public class ActivityMain extends Activity {

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

        rugbyPlayer player;

        //Create list of rugby players
        ArrayList<rugbyPlayer> players = new ArrayList<rugbyPlayer>();
        player = new rugbyPlayer("Jonny Wilkinson", rugbyPlayer.COUNTRY_ENGLAND);
        players.add(player);
        player = new rugbyPlayer("Richie McCaw", rugbyPlayer.COUNTRY_NZ);
        players.add(player);
        player = new rugbyPlayer("Martin Johnson", rugbyPlayer.COUNTRY_ENGLAND);
        players.add(player);
        player = new rugbyPlayer("Brian Habana", rugbyPlayer.COUNTRY_SA);
        players.add(player);

        //Create Adapter
        AdapterPlayers adapter = new AdapterPlayers(this, R.layout.item_player, players);

        //Set Listview adapter
        ((ListView) findViewById(R.id.activity_main_listview)).setAdapter(adapter);
    }

}



So far so good, now the important part is the Adapter which, as they say, is where the magic happens.

package example.com.multiitemlistview;


import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.ArrayList;


public class AdapterPlayers extends ArrayAdapter {

    ArrayList<rugbyPlayer> mPlayers;
    LayoutInflater mInflater;
    Context mContext;

    public AdapterPlayers(Context context, int resource, ArrayList<rugbyPlayer> items) {
        super(context, resource);
        mInflater = LayoutInflater.from(context);
        mContext = context;
        mPlayers = items;
    }

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

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder viewHolder = new ViewHolder();
        rugbyPlayer currentPlayer = mPlayers.get(position);

        if(convertView == null){
            //If convertView is null we must re-infalte view
            convertView = mInflater.inflate(R.layout.item_player, parent, false);
            viewHolder.playerName = (TextView) convertView.findViewById(R.id.item_players_name);
        }else{
            //Else object has been cached
            viewHolder = (ViewHolder) convertView.getTag();
        }

        //Now we just set the name
        viewHolder.playerName.setText(currentPlayer.getPlayerName());

        //Now we set the background color
        switch(currentPlayer.getCountryId()){
            case rugbyPlayer.COUNTRY_ENGLAND:
                viewHolder.playerName.setBackgroundColor(mContext.getResources().getColor(R.color.color_white));
                viewHolder.playerName.setTextColor(mContext.getResources().getColor(R.color.color_black));
                break;
            case rugbyPlayer.COUNTRY_NZ:
                viewHolder.playerName.setBackgroundColor(mContext.getResources().getColor(R.color.color_black));
                viewHolder.playerName.setTextColor(mContext.getResources().getColor(R.color.color_white));
                break;
            case rugbyPlayer.COUNTRY_SA:
                viewHolder.playerName.setBackgroundColor(mContext.getResources().getColor(R.color.color_sa));
                viewHolder.playerName.setTextColor(mContext.getResources().getColor(R.color.color_black));
                break;
            case rugbyPlayer.COUNTRY_AUS:
                viewHolder.playerName.setBackgroundColor(mContext.getResources().getColor(R.color.color_aus));
                viewHolder.playerName.setTextColor(mContext.getResources().getColor(R.color.color_black));
                break;
        }

        convertView.setTag(viewHolder);
        return convertView;

    }

    static class ViewHolder {
        public TextView playerName;
    }

}

The interesting thing with regard to caching starts at the bottom. ViewHolder is a class we use to keep the whole listitem contained within. This represents every view in the layout file. It could be one imageview, it could be twenty. In my case it's one simple textview. So when we come to getView() we first get the current item from the adapter's arraylist (the adapter is maintaining the data, not the activity).
So first, if the convertView is null, it means the listview has never used this layout before, so lets inflate it. Then apply the elements in the layout are set to the viewHolder.
If convertview is not null, we can just read in the viewHolder which we are storing in the tag.

Now comes the bit where we can customize the list element for its data. Look how simple it is to set the background color based on the data. We just switch on the countryId.

One very important thing to remember in all of this is that the listview will cache views itself and re-use them. So if you set something, you must always set the opposite in the alternate case. For example if I just set the COUNTRY_NZ case to have white text, when that view is re-used it will STILL have white text, so we could have white text and white background! So just remember always re-set anything you change for the else condition :)

That's it. Hope it helps.



No comments: