65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Mono C# 的 Android 分区 ListView

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (8投票s)

2012 年 12 月 13 日

CPOL

6分钟阅读

viewsIcon

41343

downloadIcon

1224

使用 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; }
   }
}  

为了简洁起见,我们还有一个 VegetableTypeFruitType,它们具有与 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>。同样,为了简洁起见,我将仅显示 MeatTypeListAdapterVegetableTypeListAdapterFruitTypeListAdapter 将非常相似,除了 <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();
      }
   }
}  

此分段适配器可用于 ListViewListActivity。 

历史 

  • 1.0 - 2012/12/7 - 发布通用操作指南。源代码和示例项目将稍后提供。
  • 1.1 - 2012/12/17 - 更新以反映 JavaObjectHandler 的用法,以实现更高级的 ItemClick 事件处理。向文章添加了示例项目。示例项目是为 Android 4.0.3 构建的。
© . All rights reserved.