使用数据填充 Xamarin.Android ListView

若要向 ListView 添加行,需要将其添加到布局,并实现 IListAdapter,使用 ListView 调用以填充自身的方法。 Android 包含内置的 ListActivityArrayAdapter 类,你可以使用它们,而无需定义任何自定义布局 XML 或代码。 ListActivity 类会自动创建一个 ListView 并公开一个 ListAdapter 属性,以提供通过适配器显示的行视图。

内置适配器采用视图资源 ID 作为用于每行的参数。 可以使用内置资源(例如 Android.Resource.Layout 中的),因此你无需编写自己的资源。

使用 ListActivity 和 ArrayAdapter<String>

示例 BasicTable/HomeScreen.cs 演示如何使用这些类仅通过几行代码显示 ListView

[Activity(Label = "BasicTable", MainLauncher = true, Icon = "@drawable/icon")]
public class HomeScreen : ListActivity {
   string[] items;
   protected override void OnCreate(Bundle bundle)
   {
       base.OnCreate(bundle);
       items = new string[] { "Vegetables","Fruits","Flower Buds","Legumes","Bulbs","Tubers" };
       ListAdapter = new ArrayAdapter<String>(this, Android.Resource.Layout.SimpleListItem1, items);
   }
}

处理行单击

通常,ListView 还让用户可以触摸某行来执行某些操作(例如播放歌曲、呼叫联系人或显示另一个屏幕)。 若要响应用户触摸,需要在 ListActivityOnListItemClick 中实现另一个方法,如下所示:

SimpleListItem 的屏幕截图

protected override void OnListItemClick(ListView l, View v, int position, long id)
{
   var t = items[position];
   Android.Widget.Toast.MakeText(this, t, Android.Widget.ToastLength.Short).Show();
}

现在,用户可以触摸一行,此时会显示 Toast 警报:

触摸行时显示的 Toast 的屏幕截图

实现 ListAdapter

ArrayAdapter<string> 是很好的,因为它简单,但它是非常受限的。 但是,通常,你有业务实体的集合,而不仅仅是要绑定的字符串。 例如,如果数据由 Employee 类的集合组成,则你可能希望列表只显示每个员工的姓名。 若要自定义 ListView 的行为以控制要显示的数据,则必须实现 BaseAdapter 的子类,替代以下四项:

  • Count – 告知控件数据中的行数。

  • GetView – 返回每行的视图,其中填充了数据。 此方法具有让 ListView 传入现有未使用的行以供重复使用的参数。

  • GetItemId – 返回行标识符(通常是行号,但也可以是你想要的任何长值)。

  • this[int] 索引器 – 返回与特定行号关联的数据。

BasicTableAdapter/HomeScreenAdapter.cs 中的示例代码演示了如何子类化 BaseAdapter

public class HomeScreenAdapter : BaseAdapter<string> {
   string[] items;
   Activity context;
   public HomeScreenAdapter(Activity context, string[] items) : base() {
       this.context = context;
       this.items = items;
   }
   public override long GetItemId(int position)
  {
       return position;
   }
   public override string this[int position] {  
       get { return items[position]; }
   }
   public override int Count {
       get { return items.Length; }
   }
   public override View GetView(int position, View convertView, ViewGroup parent)
   {
       View view = convertView; // re-use an existing view, if one is available
      if (view == null) // otherwise create a new one
           view = context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItem1, null);
       view.FindViewById<TextView>(Android.Resource.Id.Text1).Text = items[position];
       return view;
   }
}

使用自定义适配器

自定义适配器的使用类似于内置的 ArrayAdapter,传入 context 和要显示的值的 string[]

ListAdapter = new HomeScreenAdapter(this, items);

由于此示例使用相同的行布局 (SimpleListItem1),生成的应用程序将看起来与前面的示例相同。

行视图重用

在此示例中,只有六个项。 由于屏幕可以容纳 8 个,因此无需重用行。 但是,当显示数百或数千行时,如果屏幕一次只能容纳八个,那么创建数百或数千个 View 对象就会浪费内存。 为了避免这种情况,当行从屏幕中消失时,其视图会置于队列中以供重用。 当用户滚动时,ListView 会调用 GetView 以请求显示新视图,如果可以,它会在 convertView 参数中传递一个未使用的视图。 如果此值为 null,则你的代码应会创建新的视图实例,否则你可以重新设置该对象的属性并重用它。

GetView 方法应遵循此模式来重用行视图:

public override View GetView(int position, View convertView, ViewGroup parent)
{
   View view = convertView; // re-use an existing view, if one is supplied
   if (view == null) // otherwise create a new one
       view = context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItem1, null);
   // set view properties to reflect data for the given row
   view.FindViewById<TextView>(Android.Resource.Id.Text1).Text = items[position];
   // return the view, populated with data, for display
   return view;
}

在创建新视图之前,自定义适配器实现应始终重用 convertView 对象,以确保它们在显示长列表时不会耗尽内存。

某些适配器实现(例如 CursorAdapter)没有 GetView 方法,它们需要两种不同的方法 NewViewBindView,通过将 GetView 的责任分到两种方法中来强制重用行。 本文档后面提供了一个 CursorAdapter 示例。

启用快速滚动

快速滚动可帮助用户滚动长列表,方法是提供一个充当滚动条的额外“句柄”来直接访问列表的一部分。 此屏幕截图展示了快速滚动句柄:

使用滚动图柄快速滚动的屏幕截图

使快速滚动句柄出现非常简单,只需将 FastScrollEnabled 属性设置为 true

ListView.FastScrollEnabled = true;

添加分区索引

分区索引为用户在快速滚动长列表时提供其他反馈 - 它会显示他们滚动到的“分区”。 若要使分区索引出现,Adapter 子类必须实现 ISectionIndexer 接口,以便根据显示的行提供索引文本:

显示在以 H 开头的部分上方的 H 的截图

若要实现 ISectionIndexer,需要向适配器添加三个方法:

  • GetSections – 提供可显示的分区索引标题的完整列表。 此方法需要 Java 对象的数组,因此代码需要从 .NET 集合创建一个 Java.Lang.Object[]。 在我们的示例中,它将列表中的初始字符的列表返回为 Java.Lang.String

  • GetPositionForSection – 返回给定分区索引的第一行位置。

  • GetSectionForPosition – 返回要为给定行显示的分区索引。

示例 SectionIndex/HomeScreenAdapter.cs 文件实现了这些方法,并在构造函数中实现了一些其他代码。 构造函数通过循环遍历每一行并提取标题的第一个字符来生成分区索引(必须已对项进行排序才能正常工作)。

alphaIndex = new Dictionary<string, int>();
for (int i = 0; i < items.Length; i++) { // loop through items
   var key = items[i][0].ToString();
   if (!alphaIndex.ContainsKey(key))
       alphaIndex.Add(key, i); // add each 'new' letter to the index
}
sections = new string[alphaIndex.Keys.Count];
alphaIndex.Keys.CopyTo(sections, 0); // convert letters list to string[]

// Interface requires a Java.Lang.Object[], so we create one here
sectionsObjects = new Java.Lang.Object[sections.Length];
for (int i = 0; i < sections.Length; i++) {
   sectionsObjects[i] = new Java.Lang.String(sections[i]);
}

创建数据结构后,ISectionIndexer 方法就非常简单了:

public Java.Lang.Object[] GetSections()
{
   return sectionsObjects;
}
public int GetPositionForSection(int section)
{
   return alphaIndexer[sections[section]];
}
public int GetSectionForPosition(int position)
{   // this method isn't called in this example, but code is provided for completeness
    int prevSection = 0;
    for (int i = 0; i < sections.Length; i++)
    {
        if (GetPositionForSection(i) > position)
        {
            break;
        }
        prevSection = i;
    }
    return prevSection;
}

分区索引标题不需要 1:1 映射到你的实际分区。 这就是 GetPositionForSection 方法存在的原因。 GetPositionForSection 让你有机会将你的索引列表中的任何索引映射到列表视图中的任何分区。 例如,你在索引中可能有一个 "z",但你可能没有每个字母的表分区,因此,"z" 可能不映射到 26,而是映射到 25 或 24,或者任何分区索引 "z" 应映射到的位置。