Introduction
Are you trying to figure out how to place multiple ListView
controls into a single layout, but get them to behave properly when displaying
multiple list items? Are you having problems with them scrolling properly? This example will show you how to combine the separate ListView
controls
into one single ListView
, and split it into subsections, each using its own ListAdapter
. I should clarify, we won't actually have nested
ListView
controls, we will be using subsections within one ListView
, and populating each list item dynamically.
Background
This example assumes that you are already familiar with Android, and Mono C# coding.
I based this approach off of an example in a Wrox book,
Professional Android Programming with Mono for Android and .NET/C#. This example is modified a bit from the book example. This approach will allow the scrolling behavior
to work properly. The best practice is not to have more than one ListView
in a single layout. Doing so causes each
ListView
to default to displaying one list item each,
forcing separate scrolling for each ListView
. This is very annoying behavior, and the desired behavior would be for each ListView
to display all of its list items, and allow the parent layout to handle the scrolling. This method will allow you to achieve this behavior. I've also expanded the book example to show you how to handle the ListView.ItemClicked
event to properly handle the right item type, since our example will combine multiple list item types, each sprouting from their own adapter.
Using the code
This example will use a data model concerning types of food. Our layout will show a ListView
, sectioned for each different type of food we define. First, lets define our data model:
public class MeatType
{
private double _pricePerPound;
public MeatType(String name, String description, double pricePerPound)
{
_name = name;
_description = description;
_pricePerPound = pricePerPound;
}
public String Name
{
get { return _name; } set { _name = value; }
}
public String Description
{
get { return _description; } set { _description = value; }
}
public double PricePerPound
{
get { return _pricePerPound; } set { _pricePerPound = value; }
}
}
For brevity sake, we also have a VegetableType
and FruitType
, each with the same structure as the MeatType
,
but I won't list them here since their structure is the same.
Next, we need a template to describe the layout for our food type list items. Although we will be coding a separate ListAdapter
for each food type, the adapters can all use the same list item template in this example, therefore, we only need one, FoodTypeListItem.xml. This template will be a LinearLayout
,
with horizontal orientation, and three TextView
controls to hold
our three property values.
="1.0"="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"
android:id="@+id/rootLayout">
<TextView
android:id="@+id/nameLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10px"
android:width="100px"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/descriptionLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:width="150px"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/priceLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:width="50px"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
This template will be used to inflate our list items. Each adapter can call this template, and populate it differently, but the look will be the same, allowing consistency in the interface presentation.
Next we need to code our list adapters, which will each extend BaseAdapter<T>
. Again, for brevity, I will only show the MeatTypeListAdapter
, the VegetableTypeListAdapter
and FruitTypeListAdapter
will both be nearly identical except for the <Type>
casts.
using Android.Widget;
using Android.App;
public class MeatTypeListAdapter : BaseAdapter<MeatType>
{
private Activity _context;
private List<MeatTypes> _items;
private int _templateResourceId;
public MeatTypeListAdapter(Activity context,
int templateResourceId, List<MeatType> items) : base()
{
_context = context;
_templateResourceId = templateResourceId;
_items = items;
}
public override int Count { get { return _items.Count; } }
public override MeatType this[int index] { get { return _items[index]; } }
public override long GetItemId(int position) { return position; }
public override View GetView(int position, View convertView, ViewGroup parent)
{
MeatType item = this[position];
View view = convertView;
if(view == null || !(view is LinearLayout))
{
view = _context.LayoutInflater.Inflate(_templateResourceId, parent, false);
}
TextView nameLabel = view.FindViewById<TextView>(Resource.Id.nameLabel);
nameLabel.Text = item.Name;
TextView descriptionLabel = view.FindViewById<TextView>(Resource.Id.descriptionLabel);
descriptionLabel.Text = item.Description;
TextView priceLabel = view.FindViewById<TextView>(Resource.Id.priceLabel);
priceLabel.Text = item.PricePerPound.ToString("F2");
return view;
}
}
The bones of the adapter is the
GetView
method. It takes the item at the given position, and creates a
View
of the type defined
by the template, and populates its controls with the items property values. You have to code this method to properly handle the template you plan to use, and the item type your data is coming from. In our case, our
View
type is a
LinearLayout
, but if you use a different root layout type, or even just a base control type, then your code should reflect this type by changing the
!(view is LinearLayout)
to whatever your control type is, so that you provide the proper
View
type. This if clause allows for recycling of code at runtime, so if a
LinearLayout
is already passed, i.e. the same
View
type for consecutive calls to
GetView
, then inflation is not necessary.
Next, we need to create a SectionedListAdapter
to handle the multiple lists we will want to contain within our ListView
control. Before we can code this adapter though, we need a
ListSection
class to describe the separate list subsections. The
ListSection
class will hold the text caption for the section, the column header names for the section, and the
ListAdapter
that goes with the section.
using Android.Widget;
public class ListSection
{
private String _caption, _columnHeader1, _columnHeader2, _columnHeader3;
private BaseAdapter _adapter;
public ListSection(String caption, String columnHeader1, String columnHeader2,
String columnHeader3, BaseAdapter adapter)
{
_caption = caption;
_columnHeader1 = columnHeader1;
_columnHeader2 = columnHeader2;
_columnHeader3 = columnHeader3;
_adapter = adapter;
}
public String Caption { get { return _caption; } set { _caption = value; } }
public String ColumnHeader1 { get { return _columnHeader1; } set { _columnHeader1 = value; } }
public String ColumnHeader2 { get { return _columnHeader2; } set { _columnHeader2 = value; } }
public String ColumnHeader3 { get { return _columnHeader3; } set { _columnHeader3 = value; } }
public BaseAdapter Adapter { get { return _adapter; } set { _adapter = value; } }
}
Also, before we create our sectioned adapter, we need an xml template to describe our section header, or separator.
You could simply use a TextView
to achieve this. Setting the separator View
style
tag to "?android:attr/listSeparatorTextViewStyle"
will place a separator border line on the bottom border of the separator View
. For this example, I want the separator to also include the column headers, so the template will be a bit more complex than a simple TextView
.
The ListSeparator.xml looks like:
="1.0"="utf-8"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/caption"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10px"
android:textAppearance="?android:attr/textAppearanceSmall" />
<LinearLayout
android:id="@+id/columnHeaderLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
style="?android:attr/listSeparatorTextViewStyle">
<TextView
android:id="@+id/columnHeader1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:marginLeft="10px"
android:width="100px"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/columnHeader2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:width="150px"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/columnHeader3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:width="50px"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
</LinearLayout>
You can see, we've set the inner LinearLayout
, which holds our column headers, as the listSeperatorTextViewStyle
, so that the entire LinearLayout
gets the border line beneath it. Now we are ready to create our sectioned adapter, the SectionedListAdapter
using Android.App;
using Android.Widget;
using Android.Views;
public class SectionedListAdapter
{
private const int TYPE_SECTION_HEADER = 0;
private Context _context;
private LayoutInflater _inflater;
private List<ListSection> _sections;
public SectionedListAdapter(Context context)
{
_context = context;
_inflater = LayoutInflater.From(_context);
_sections = new List<ListSection>();
}
public List<ListSection> Sections { get { return _sections; } set { _sections = value; } }
public override int Count
{
get
{
int count = 0;
foreach (ListSection s in _sections) count += s.Adapter.Count + 1;
return count;
}
}
public override int ViewTypeCount
{
get
{
int viewTypeCount = 1;
foreach (ListSection s in _sections) viewTypeCount += s.Adapter.ViewTypeCount;
return viewTypeCount;
}
}
public override ListSection this[int index] { get { return _sections[index]; } }
public override bool AreAllItemsEnabled() { return false; }
public override int GetItemViewType(int position)
{
int typeOffset = TYPE_SECTION_HEADER + 1;
foreach (ListSection s in _sections)
{
if (position == 0) return TYPE_SECTION_HEADER;
int size = s.Adapter.Count + 1;
if (position < size) return (typeOffset + s.Adapter.GetItemViewType(position - 1));
position -+ size;
typeOffset += s.Adapter.ViewTypeCount;
}
return -1;
}
public override long GetItemId(int position) { return position; }
public void AddSection(String caption, String columnHeader1, String columnHeader2,
String columnHeader3, BaseAdapter adapter)
{
_sections.Add(new ListSection(caption, columnHeader1, columnHeader2, columnHeader3, adapter));
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
View view = convertView;
foreach (ListSection s in _sections)
{
if (position == 0)
{
if (view == null || !(view is LinearLayout))
{
view = _inflater.Inflate(Resource.Layout.ListSeparator, parent, false);
}
TextView caption = view.FindViewById<TextView>(Resource.Id.caption);
caption.Text = s.Caption;
TextView columnHeader1 = view.FindViewById<TextView>(Resource.Id.columnHeader1);
columnHeader1.Text = s.ColumnHeader1;
TextView columnHeader2 = view.FindViewById<TextView>(Resource.Id.columnHeader2);
columnHeader2.Text = s.ColumnHeader2;
TextView columnHeader3 = view.FindViewById<TextView>(Resource.Id.columnHeader3);
columnHeader3.Text = s.ColumnHeader3;
}
int size = s.Adapter.Count + 1;
if (position < size) return s.Adapter.GetView(position - 1, convertView, parent);
position -= size;
}
return null;
}
public override Java.Lang.Object GetItem(int position)
{
foreach (ListSection s in _sections)
{
if (position == 0) return null;
int size = s.Adapter.Count + 1;
if (position < size) return s.Adapter.GetItem(position);
position -= size;
}
return null;
}
}
As you see, we needed to override GetItem
in this case. Normally you would not need to, as by default it returns
this[int]
. However, for our sectioned adapter, this[int]
returns a ListSection
object, which does no good when trying to retrieve a list item from the ListView
. Overriding this method as above forces the method to dig into the proper sublist and return the appropriate object.
Now all that
is needed is to populate the ListView
that needs to house all this information. See the snippet below from within the OnCreate
method of our App.
List<MeatType> meats = new List<MeatType>();
meats.Add(new MeatType("Hamburger", "Ground chuck beef", 2.76));
meats.Add(new MeatType("Sirloin", "Sliced sirloin steaks", 4.56));
List<VegetableType> veggies = new List<VegetableType)();
veggies.Add(new VegetableType("Brocolli", "Cut brocolli floretes", 1.76));
veggies.Add(new VegetableType("Carrots", "Cut peeled baby carrots", 2.18));
List<FruitType> fruits = new List<FruitType>();
fruits.Add(new FruitType("Apple", "Granny smith apples", 0.87));
fruits.Add(new FruitType("Peach", "South Carolina peaches", 1.12));
MeatTypeListAdapter madptr = new MeatTypeListAdapter(this, Resource.Layout.FoodTypeListItem, meats);
VegetableTypeListAdapter vadptr = new VegetableTypeListAdapter(this, Resource.Layout.FoodTypeListItem, veggies);
FruitTypeListAdapter fadptr = new FruitTypeListAdapter(this, Resource.Layout.FoodTypeListItem, fruits);
SectionedListAdapter sadptr = new SectionedListAdapter(this);
sadptr.AddSection("Available Meats", "Name", "Description", "Price (lb.)", madptr);
sadptr.AddSection("Available Vegetables", "Name", "Description", "Price (lb.)", vadptr);
sadptr.AddSection("Available Fruits", "Name", "Description", "Price (lb.)", fadptr);
ListView foodList = FindViewById<ListView>(Resource.Id.foodList);
foodList.SetAdapter(sadptr);
foodList.ItemClick += new EventHandler<AdapterView.ItemClickEventHandler>(foodList_ItemClick);
Now for the final step, properly handling the ItemClick
event. We have to be sure that the proper sub list is queried when the ItemClick
event fires. In this example, we are showing a new layout with the details of the item clicked. Here is the guts of the ItemClick
event handler. There may be a better way to do this, and I am certainly open to any suggestions, but I don't see a clear way to cast a Java.Lang.Object
into a .NET Object
. For this reason, I've compared the output of both ToString
methods. If by chance your data object has a custom ToString
method, you may need to tweak this a bit.
private void foodList_ItemClick(object sender, AdapterView.ListItemClickEventArgs e)
{
SectionedListAdapter adptr = (sender as ListView).Adapter as SectionedListAdapter;
if (adptr.GetItem(e.Position != null)
{
if (adptr.GetItem(e.Position).ToString() == typeof(MeatType).ToString())
{
}
else if (adptr.GetItem(e.Position).ToString() == typeof(VegetableType).ToString())
{
}
else if (adptr.GetItem(e.Position).ToString() == typeof(FruitType).ToString())
{
}
}
}
Update - I realized this method is really only useful if you just want to react a certain generic way to the click. If you need to actually handle the object clicked, then things get a bit more tricky, not too bad though. To do this, you first need to create a wrapper class to handle passage of a Java.Lang.Object
instance from the Adapter, which is passed when you override the GetItem(int)
method of the Adapter. First, the wrapper class, JavaObjectHandler
using Java.Lang;
using System;
public class JavaObjectHandler : Java.Lang.Object
{
private System.Object _instance;
public JavaObjectHandler(System.Object instance) { _instance = instance; }
public System.Object Instance { get { return _instance; } }
}
Now, when we override the GetItem(int)
method in the adapter, we will pass a JavaObjectHandler
instance rather than the default Java.Lang.Object
instance, and our JavaObjectHandler
will contain the data object lying hidden in the Adapter. Go back to any of your BaseAdapter<T>
classes and add the following method
public override Java.Lang.Object GetItem(int position)
{
if (position < _items.Count)
{
return new JavaObjectHandler(_items[position]);
}
return null;
}
Since we have already overridden this method in our SectionedListAdapter
, it will now kindly either pass null if a separator is clicked, or the underlying JavaObjectHandler
containing our data object. Now you can modify your ItemClick
event to use this new JavaObjectHandler
instance, for example
private void foodList_ItemClick(object sender, AdapterView.ItemClickEventArgs e)
{
SectionedListAdapter adapter = (sender as ListView).Adapter as SectionedListAdapter;
JavaObjectHandler item = adapter.GetItem(e.Position);
if (item != null)
{
if (item.Instance is MeatType)
{
Toast.MakeText(this, "You clicked a meat: " +
(item.Instance as MeatType).Name, ToastLength.Short).Show();
}
else if (item.Instance is VegetableType)
{
Toast.MakeText(this, "You clicked a vegetable: " +
(item.Instance as VegetableType).Name, ToastLength.Short).Show();
}
else if (item.Instance is FruitType)
{
Toast.MakeText(this, "You clicked a fruit: " +
(item.Instance as FruitType).Name, ToastLenght.Short).Show();
}
}
}
This sectioned adapter can be used for ListView
or for ListActivity
alike.
History
- 1.0 - 12/7/2012 - General How-To posted. Source and example project to follow shortly.
- 1.1 - 12/17/2012 - Updated to reflect
JavaObjectHandler
usage for more advanced
ItemClick
event handling. Added sample project to article. Sample is built for Android 4.0.3.