Thursday, March 29, 2012

ExpandableListView on Android


ExpandableListView on Android

ListView is pretty widely used. There are situations when you would like to group/categorize your list items. To achieve such a thing on Android, you would probably use theExpandableListView. The data to the ExpandableListView is supplied by a special kind of adapter called the SimpleExpandableListAdapter which extends theBaseExpandableListAdapter.

As with any other widget on Android, you are free to customize the widgets as per your needs. Here, I will show how to create such a custom list adapter for the ExpandableListView.

For this example, I want to show a list of vehicles with their names. Also, I want to group them according to their category.

I have 4 classes.
1. Vehicle : The parent class for the rest.
2. Car: Extends the Vehicle class.
3. Bus: Extends the Vehicle class.
4. Bike: Extends the Vehicle class.







public class Vehicle {
    private String name;

    private String group;

    public String getGroup() {
        return group;
    }

    public void setGroup(String group) {
        this.group = group;
    }

    public Vehicle(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class Bike extends Vehicle {

    public Bike(String name) {
        super(name);
        setGroup("Bikes");
    }

}
public class Bus extends Vehicle {

    public Bus(String name) {
        super(name);
        setGroup("Busses");
    }

}
public class Car extends Vehicle{

    public Car(String name) {
        super(name);
        setGroup("Cars");
    }
   
}



I have a method called getRandomVehicle(String name) which returns a random vehicle instance setting the name that I pass. The vehicle can be a Bus, Car or a Bike. Its completely random.

In the ExpandableListAdapter, (the custom adapter), there's a method called addItem(Vehicle vehicle), which manages the groups and their children.



import java.util.ArrayList;

import com.beanie.example.list.R;
import com.beanie.example.list.classes.Bike;
import com.beanie.example.list.classes.Bus;
import com.beanie.example.list.classes.Car;
import com.beanie.example.list.classes.Vehicle;

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

public class ExpandableListAdapter extends BaseExpandableListAdapter {

    @Override
    public boolean areAllItemsEnabled()
    {
        return true;
    }

    private Context context;

    private ArrayList<String> groups;

    private ArrayList<ArrayList<Vehicle>> children;

    public ExpandableListAdapter(Context context, ArrayList<String> groups,
            ArrayList<ArrayList<Vehicle>> children) {
        this.context = context;
        this.groups = groups;
        this.children = children;
    }

    /**
     * A general add method, that allows you to add a Vehicle to this list
     *
     * Depending on if the category opf the vehicle is present or not,
     * the corresponding item will either be added to an existing group if it
     * exists, else the group will be created and then the item will be added
     * @param vehicle
     */
    public void addItem(Vehicle vehicle) {
        if (!groups.contains(vehicle.getGroup())) {
            groups.add(vehicle.getGroup());
        }
        int index = groups.indexOf(vehicle.getGroup());
        if (children.size() < index + 1) {
            children.add(new ArrayList<Vehicle>());
        }
        children.get(index).add(vehicle);
    }

    @Override
    public Object getChild(int groupPosition, int childPosition) {
        return children.get(groupPosition).get(childPosition);
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }
   
    // Return a child view. You can load your custom layout here.
    @Override
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
            View convertView, ViewGroup parent) {
        Vehicle vehicle = (Vehicle) getChild(groupPosition, childPosition);
        if (convertView == null) {
            LayoutInflater infalInflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = infalInflater.inflate(R.layout.child_layout, null);
        }
        TextView tv = (TextView) convertView.findViewById(R.id.tvChild);
        tv.setText("   " + vehicle.getName());

        // Depending upon the child type, set the imageTextView01
        tv.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
        if (vehicle instanceof Car) {
            tv.setCompoundDrawablesWithIntrinsicBounds(R.drawable.car, 0, 0, 0);
        } else if (vehicle instanceof Bus) {
            tv.setCompoundDrawablesWithIntrinsicBounds(R.drawable.bus, 0, 0, 0);
        } else if (vehicle instanceof Bike) {
            tv.setCompoundDrawablesWithIntrinsicBounds(R.drawable.bike, 0, 0, 0);
        }
        return convertView;
    }

    @Override
    public int getChildrenCount(int groupPosition) {
        return children.get(groupPosition).size();
    }

    @Override
    public Object getGroup(int groupPosition) {
        return groups.get(groupPosition);
    }

    @Override
    public int getGroupCount() {
        return groups.size();
    }

    @Override
    public long getGroupId(int groupPosition) {
        return groupPosition;
    }

    // Return a group view. You can load your custom layout here.
    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
            ViewGroup parent) {
        String group = (String) getGroup(groupPosition);
        if (convertView == null) {
            LayoutInflater infalInflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = infalInflater.inflate(R.layout.group_layout, null);
        }
        TextView tv = (TextView) convertView.findViewById(R.id.tvGroup);
        tv.setText(group);
        return convertView;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

    @Override
    public boolean isChildSelectable(int arg0, int arg1) {
        return true;
    }

}

In the SampleActivity, I have initialized a blank ExpandableListAdapter and set it to the list view. Now, I start a thread, which gets a random vehicle after every 2 seconds, and adds it to the adapter, and calls the adapter to notify that the data has changed. 


import java.util.ArrayList;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ExpandableListView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ExpandableListView.OnGroupClickListener;

import com.beanie.example.list.adapter.ExpandableListAdapter;
import com.beanie.example.list.classes.Vehicle;
import com.beanie.example.list.data.MockDataProvider;

public class SampleActivity extends Activity implements Runnable
{
    private ExpandableListAdapter adapter;

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

        // Retrive the ExpandableListView from the layout
        ExpandableListView listView = (ExpandableListView) findViewById(R.id.listView);
       
        listView.setOnChildClickListener(new OnChildClickListener()
        {
           
            @Override
            public boolean onChildClick(ExpandableListView arg0, View arg1, int arg2, int arg3, long arg4)
            {
                Toast.makeText(getBaseContext(), "Child clicked", Toast.LENGTH_LONG).show();
                return false;
            }
        });
       
        listView.setOnGroupClickListener(new OnGroupClickListener()
        {
           
            @Override
            public boolean onGroupClick(ExpandableListView arg0, View arg1, int arg2, long arg3)
            {
                Toast.makeText(getBaseContext(), "Group clicked", Toast.LENGTH_LONG).show();
                return false;
            }
        });

        // Initialize the adapter with blank groups and children
        // We will be adding children on a thread, and then update the ListView
        adapter = new ExpandableListAdapter(this, new ArrayList<String>(),
                new ArrayList<ArrayList<Vehicle>>());

        // Set this blank adapter to the list view
        listView.setAdapter(adapter);

        // This thread randomly generates some vehicle types
        // At an interval of every 2 seconds
        Thread thread = new Thread(this);
        thread.start();
    }

    @Override
    public void run()
    {
        final int ITEMS = 15;
        int count = 0;
        while (count != ITEMS)
        {
            count++;
            adapter.addItem(MockDataProvider.getRandomVehicle("Vehicle no. " + count));

            // Notify the adapter
            handler.sendEmptyMessage(1);
            try
            {
                // Sleep for two seconds
                Thread.sleep(2000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    private Handler handler = new Handler()
    {

        @Override
        public void handleMessage(Message msg)
        {
            adapter.notifyDataSetChanged();
            super.handleMessage(msg);
        }

    };
}



public class MockDataProvider {
    // A utility method that generates random Vehicles
    public static Vehicle getRandomVehicle(String name) {
        Vehicle vehicle = null;
        Random random = new Random();
        int type = random.nextInt(3);
        switch (type) {
            case 0:
                vehicle = new Car(name);
                break;
            case 1:
                vehicle = new Bus(name);
                break;
            case 2:
                vehicle = new Bike(name);
                break;
        }
        return vehicle;
    }
}

There are a few methods, in the ExpandableListAdapter, which you should go through carefully. I have two layout files, group_layout.xml and child_layout.xml which are used as the layout for the group views and the child views of the ExpandableListView.




<LinearLayout android:id="@+id/LinearLayout01"
        android:layout_width="fill_parent" android:layout_height="45dip"
        xmlns:android="http://schemas.android.com/apk/res/android">
        <ImageView android:id="@+id/ImageView01" android:src="@drawable/icon"
                android:layout_height="40dip" android:layout_width="40dip" android:layout_marginLeft="40dip"></ImageView>
        <TextView android:layout_width="fill_parent"
                android:layout_height="45dip" android:paddingLeft="5dip"
                android:paddingRight="5dip" android:textStyle="bold" android:textSize="17dip"
                android:gravity="center_vertical" android:id="@+id/tvChild"
                android:text="Children" android:textColor="#ffCCCC22"></TextView>
</LinearLayout>
<LinearLayout android:id="@+id/LinearLayout01"
        android:layout_width="fill_parent" android:layout_height="45dip"
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:background="@drawable/title">
        <ImageView android:id="@+id/ImageView01" android:src="@drawable/icon"
                android:layout_height="40dip" android:layout_width="40dip" android:layout_marginLeft="40dip"></ImageView>
        <TextView android:id="@+id/tvGroup" android:layout_width="fill_parent"
                android:layout_height="45dip" android:text="Groups" android:gravity="center_vertical|right"
                android:paddingLeft="5dip" android:paddingRight="5dip"
                android:textColor="#ffffffff" android:textStyle="bold"
                android:textSize="17dip"></TextView>
</LinearLayout>
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">
        <ExpandableListView android:id="@+id/listView"
                android:layout_width="fill_parent" android:layout_height="fill_parent" android:scrollbars="none"></ExpandableListView>
</LinearLayout>
There you go, you have a custom ExpandableListView. 


There are some more methods that you might be interested in, like, how to change the "arrow icon" for the group views(official doc link), or how to expand or collapse a group at will, or how to handle specific events like group collapsed or group expanded. Read the docs onExpandableListView.

No comments: