使用 Mono C# 的 Android 分区 ListView
使用 Android Mono C# 开发一个偏好设置风格的分段列表适配器。
引言
您是否在尝试弄清楚如何将多个 ListView
控件放入单个布局中,但又希望它们在显示多个列表项时能正常工作?您是否在使用它们的滚动功能时遇到问题?本示例将向您展示如何将单独的 ListView
控件合并成一个单一的 ListView
,并将其分成多个子部分,每个子部分使用自己的 ListAdapter
。我应该澄清一下,我们实际上不会使用嵌套的 ListView
控件,而是在一个 ListView
中使用子部分,并动态填充每个列表项。
背景
本示例假定您已经熟悉 Android 和 Mono C# 编码。
我的这种方法是基于 Wrox 出版社的一本书《Professional Android Programming with Mono for Android and .NET/C#》中的一个示例。这个示例比书中的示例稍作修改。这种方法可以确保滚动行为正常工作。最佳实践是不要在单个布局中放置多个 ListView
。这样做会导致每个 ListView
默认只显示一个列表项,从而迫使每个 ListView
进行单独滚动。这是一种非常令人烦恼的行为,而期望的行为是每个 ListView
都显示其所有列表项,并由父布局处理滚动。此方法将使您能够实现此行为。我还扩展了书中的示例,展示了如何处理 ListView.ItemClicked
事件,以便正确处理正确的项目类型,因为我们的示例将结合多种列表项类型,每种类型都源自其自己的适配器。
使用代码
本示例将使用一个关于食物类型的数据模型。我们的布局将显示一个 ListView
,按我们定义的每种不同食物类型进行分段。首先,让我们定义我们的数据模型:
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; }
}
}
为了简洁起见,我们还有一个 VegetableType
和 FruitType
,它们具有与 MeatType
相同的结构,但我在此不列出它们,因为它们的结构是相同的。
接下来,我们需要一个模板来描述食物类型列表项的布局。虽然我们将为每种食物类型编写一个单独的 ListAdapter
,但在本示例中,所有适配器都可以使用相同的列表项模板,因此我们只需要一个,即FoodTypeListItem.xml。该模板将是一个具有水平方向的 LinearLayout
,包含三个 TextView
控件来容纳我们的三个属性值。
<?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"
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>
此模板将用于填充我们的列表项。每个适配器都可以调用此模板,并以不同的方式填充它,但外观将相同,从而在界面呈现上保持一致性。
接下来,我们需要编写我们的列表适配器,每个适配器都将继承 BaseAdapter<T>
。同样,为了简洁起见,我将仅显示 MeatTypeListAdapter
,VegetableTypeListAdapter
和 FruitTypeListAdapter
将非常相似,除了 <Type>
转换。
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;
}
}
适配器的核心是 GetView
方法。它获取给定位置的项,并创建一个由模板定义的 View
,然后用项的属性值填充其控件。您必须对该方法进行编码,以正确处理您计划使用的模板以及您的数据来源的项类型。在我们的例子中,我们的 View
类型是 LinearLayout
,但如果您使用不同的根布局类型,甚至只是一个基础控件类型,那么您的代码应该通过更改 !(view is LinearLayout)
来反映该类型,以提供正确的 View
类型。此 if 子句允许在运行时回收代码,因此如果传递的 LinearLayout
已经存在,即 GetView
的连续调用使用相同的 View
类型,那么就不需要进行填充。 接下来,我们需要创建一个 SectionedListAdapter
来处理我们希望包含在我们 ListView
控件中的多个列表。但在我们编写此适配器之前,我们需要一个 ListSection
类来描述单独的列表子部分。ListSection
类将包含该部分的文本标题、该部分的列标题名称以及与该部分对应的 ListAdapter
。
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; } }
}
同样,在我们创建分段适配器之前,我们需要一个 xml 模板来描述我们的分段标题或分隔符。您可以简单地使用一个 TextView
来实现此目的。将分隔符 View
style
标签设置为 "?android:attr/listSeparatorTextViewStyle"
将在分隔符 View
的底部边框上放置一条分隔线。在本例中,我希望分隔符也包含列标题,因此模板将比简单的 TextView
稍复杂一些。ListSeparator.xml 如下:
<?xml version="1.0" encoding="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>
您可以看到,我们将包含列标题的内部 LinearLayout
设置为 listSeperatorTextViewStyle
,以便整个 LinearLayout
获得其下方的边框线。 现在我们可以创建我们的分段适配器 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; } }
// Each section has x list items + 1 list item for the caption. This is the reason for the +1 in the tally
public override int Count
{
get
{
int count = 0;
foreach (ListSection s in _sections) count += s.Adapter.Count + 1;
return count;
}
}
// We know there will be at least 1 type, the seperator, plus each
// type for each section, that is why we start with 1
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]; } }
// Since we dont want the captions selectable or clickable returning a hard false here achieves this
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)
{
// postion == 0 means we have to inflate the section separator
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;
// postion < size means we are at an item, so we just pass through its View from its adapter
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; // this is a separator item, dont want it instantiated
int size = s.Adapter.Count + 1;
if (position < size) return s.Adapter.GetItem(position);
position -= size;
}
return null;
}
}
正如您所见,在这种情况下,我们需要重写 GetItem
。通常您不需要这样做,因为默认情况下它返回 this[int]
。但是,对于我们的分段适配器,this[int]
返回一个 ListSection
对象,当尝试从 ListView
中检索列表项时,这毫无用处。重写方法如下,迫使该方法深入到适当的子列表中并返回适当的对象。
现在,唯一需要做的就是填充需要容纳所有这些信息的 ListView
。请参阅我们应用程序 OnCreate
方法中的代码片段。
// Fist lets create and populate the List<> instances that hold our food items
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));
// Now we create our adapters for the item types
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);
// Now we create our sectioned adapter and add its sections
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);
// Now fetch the ListView and set its adapter to the sectioned adapter
ListView foodList = FindViewById<ListView>(Resource.Id.foodList);
foodList.SetAdapter(sadptr);
foodList.ItemClick += new EventHandler<AdapterView.ItemClickEventHandler>(foodList_ItemClick);
现在进入最后一步,正确处理 ItemClick
事件。我们必须确保在 ItemClick
事件触发时查询适当的子列表。在本示例中,我们显示了一个新布局,其中包含被点击项的详细信息。以下是 ItemClick
事件处理程序的内部实现。 可能有一种更好的方法来做到这一点,我当然欢迎任何建议,但我看不到将 Java.Lang.Object
转换为 .NET Object
的明确方法。因此,我比较了两个 ToString
方法的输出。如果碰巧您的数据对象有一个自定义的 ToString
方法,您可能需要对此进行一些调整。
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())
{
// Handle your code however you like here for when a meat is clicked
}
else if (adptr.GetItem(e.Position).ToString() == typeof(VegetableType).ToString())
{
// Handle your code however you like here for when a vegetable is clicked
}
else if (adptr.GetItem(e.Position).ToString() == typeof(FruitType).ToString())
{
// Handler your code however you like here for when a fruit is clicked
}
}
}
更新 - 我意识到此方法仅在您只想以某种通用方式响应点击时才真正有用。如果您需要实际处理被点击的对象,那么事情会变得棘手一些,但也不是太难。要做到这一点,您首先需要创建一个包装类来处理适配器传递的 Java.Lang.Object
实例,当您重写适配器的 GetItem(int)
方法时就会传递它。首先是包装类 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; } }
}
现在,当我们重写适配器中的 GetItem(int)
方法时,我们将传递一个 JavaObjectHandler
实例而不是默认的 Java.Lang.Object
实例,我们的 JavaObjectHandler
将包含隐藏在适配器中的数据对象。回到您的任何一个 BaseAdapter<T>
类,并添加以下方法:
public override Java.Lang.Object GetItem(int position)
{
if (position < _items.Count)
{
return new JavaObjectHandler(_items[position]);
}
return null;
}
由于我们已经在 SectionedListAdapter
中重写了此方法,因此它现在会很方便地在点击分隔符时传递 null,或者传递包含我们的数据对象的底层 JavaObjectHandler
。现在您可以修改您的 ItemClick
事件以使用此新的 JavaObjectHandler
实例,例如:
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();
}
}
}
此分段适配器可用于 ListView
或 ListActivity
。
历史
- 1.0 - 2012/12/7 - 发布通用操作指南。源代码和示例项目将稍后提供。
- 1.1 - 2012/12/17 - 更新以反映
JavaObjectHandler
的用法,以实现更高级的ItemClick
事件处理。向文章添加了示例项目。示例项目是为 Android 4.0.3 构建的。