14.2 列表控件

Android中的列表控件非常灵活,可以自定义每一个列表项。实际上,每一个列表项就是一个View。本节将介绍Android SDK中的3个列表控件:ListView、ExpandableListView和Spinner。其中Spinner就是在Windows中经常看到的下拉列表框。

14.2.1 ListView(普通列表控件)

源代码目录:src/ch14/Listview

ListView控件用于以列表的形式显示数据。ListView控件采用MVC模式将前端显示与后端数据进行分离。也就是说,ListView控件在装载数据时并不是直接使用ListView.add或类似的方法(根本就没这些方法)添加数据,而是需要指定一个Adapter对象,该对象相当于MVC模式中的C(控制器,Controller)。ListView相当于MVC模式中的V(视图,View),用于显示数据。为ListView提供数据的List、数组或数据库相当于MVC模式中的M(模型,Model)。

在ListView控件中通过Adapter对象获得需要显示的数据。在创建Adapter对象时需要指定要显示的数据(List或数组对象),因此,要显示的数据与ListView之间通过Adapter对象进行连接,同时又互相独立。也就是说,ListView只知道显示的数据来自Adapter,并不知道这些数据是来自List、数组或是数据库。对于数据来说,只知道将这些数据添加到Adapter对象中,并不知道这些数据会被用于ListView控件或其他控件。

在操作ListView控件之前,先来定义一个ListView控件,代码如下:

源代码文件:src/ch14/Listview/res/layout/main.xml

<ListView android:id="@+id/lvCommonListView"

  android:layout_width="fill_parent" android:layout_height="wrap_content"/>

向ListView控件装载数据之前需要创建一个Adapter对象(通常在onCreate方法中完成),代码如下:

源代码文件:src/ch14/Listview/src/mobile/android/listview/Main.java

ArrayAdapter<String> aaData = new ArrayAdapter<String>(this,android.R.layout.simple_ list_item_1, data);

在上面的代码中创建了一个android.widget.ArrayAdapter对象。ArrayAdapter类的构造方法需要一个android.content.Context对象,因此,在本例中使用当前窗口的对象实例(this)作为ArrayAdapter类的构造方法的第1个参数值。除此之外,ArrayAdapter还需要完成如下两件事:

指定列表项的布局文件的资源ID;

指定在列表项中显示的数据。

其中布局文件的资源ID通过ArrayAdapter类的构造方法的第2个参数传入ArrayAdapter对象,列表项中显示的数据(List对象或数组)通过第3个参数传入ArrayAdapter对象。在本例中使用了Android SDK提供的XML布局文件(simple_list_item_1.xml),该布局文件对应的资源ID是android.R.layout.simple_list_item_1。这个布局文件可以在<Android SDK安装目录>/platforms/ android-17/data/res/layout目录中找到,代码如下:

<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"

  android:id="@android:id/text1"

  android:layout_width="fill_parent"

  android:layout_height="wrap_content"

  android:textAppearance="?android:attr/textAppearanceLarge"

  android:gravity="center_vertical"

  android:paddingLeft="6dip"

  android:minHeight="?android:attr/listPreferredItemHeight"

/>

从上面的代码可以看出,在simple_list_item_1.xml文件中只定义了一个<TextView>标签,因此,使用这个布局文件相当于在ListView中只显示简单的文本列表项。

ArrayAdapter类的构造方法的第3个参数值(data)是一个String[]对象,该数组中定义了ListView的数据源。

除了可以使用String[]对象作为Adapter的数据源外,还可以使用List对象作为Adapter的数据源。因此,可以使用List对象来代替上面代码中的data变量。

在创建完ArrayAdapter对象后,需要使用ListView.setAdapter方法将ArrayAdapter对象与ListView控件绑定,代码如下:

ListView lvCommonListView = (ListView) findViewById(R.id.lvCommonListView);

lvCommonListView.setAdapter(aaData);

当调用setAdapter方法后,ListView控件的每一个列表项都会使用simple_list_item_1.xml文件定义的模板来显示,并将data数组中的每一个元素赋值给每一个列表项(列表项就是在simple_list_item_1.xml中定义的TextView控件)。

在默认情况下,ListView控件选中的是第1项。如果想一开始就选中指定的列表项,需要使用ListView.setSelection方法进行设置,代码如下:

lvCommonListView.setSelection(6); // 选中第7个列表项

与列表项相关的有如下两个事件:

ItemSelected(列表项被选中时触发);

ItemClick(单击列表项时触发)。

为了截获这两个事件,需要分别实现OnItemSelectedListener和OnItemClickListener接口。在本例中分别在这两个接口的事件方法中输出了相应的日志信息,读者可以在LogCat视图中查看这些事件的调用顺序。

本例的完整代码如下:

源代码文件:src/ch14/Listview/src/mobile/android/listview/Main.java

public class Main extends Activity implements OnItemSelectedListener,

    OnItemClickListener

{  

  private static String[] data = new String[]

  {

      "天地逃生",

      "保持通话",

      "乱世佳人(飘)",

      "怪侠一枝梅",

      "第五空间",

      "孔雀翎",

      "变形金刚3(真人版)",

      "星际传奇"};

  // 单击列表项调用该方法

  @Override

  public void onItemClick(AdapterView<?> parent, View view, int position,

      long id)

  {

    Log.d("itemclick", "click " + position + " item");

  }

  // 选择列表项调用该方法

  @Override

  public void onItemSelected(AdapterView<?> parent, View view, int position,

      long id)

  {

    Log.d("itemselected", "select " + position + " item");

  }

  @Override

  public void onNothingSelected(AdapterView<?> parent)

  {

    Log.d("nothingselected", "nothing selected");

  }

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.main);

    ListView lvCommonListView = (ListView) findViewById(R.id.lvCommonListView);

    // 创建ArrayAdapter对象

    ArrayAdapter<String> aaData = new ArrayAdapter<String>(this,

        android.R.layout.simple_list_item_1, data);

    // 将ArrayAdapter与ListView绑定,Listview会从ArrayAdapter获取数据

    lvCommonListView.setAdapter(aaData);

    lvCommonListView.setOnItemClickListener(this);

    lvCommonListView.setOnItemSelectedListener(this);

  }

}

运行本例后,将显示如图14-6所示的效果。

 

▲图14-6 ListView 控件

多学一招:如何添加快速滚动滑杆

快速滚动滑杆就是当ListView的列表项过多时,快速滚动列表后,右侧可以出现标识当前滚动位置的View(其实就是一个图像),效果如图14-7所示。

实现这个功能只需要将<ListView>标签的android:fastScrollEnabled属性值设为true即可,代码如下:

<ListView android:id="@+id/lvCommonListView"

  android:layout_width="fill_parent" android:layout_height="wrap_content"

  android:fastScrollEnabled="true"

  />

并不是ListView可以滚动就会出现如图14-17所示的块速滚动滑杆,而必须至少有4个滚动页时才会出现快速滚动滑杆。也就是说,如果每页可以显示8个列表项,至少要有32个列表项,滚动时才会显示快速滚动滑杆。读者可以在ListView工程的AndroidManifest.xml文件中将ListViewActivity设为主窗口(将Main设为非主窗口),并运行程序观察块速滚动滑杆的效果。

 

▲图14-7 块速滚动滑杆

14.2.2 为ListView列表项添加复选框和选项按钮

源代码目录:src/ch14/ChoiceListview

如果想选择多个列表项,就需要在每个列表项上添加RadioButton、CheckBox等控件。当然,向列表项添加控件的方法很多,但ListView提供了一种非常简单的方式向列表项添加多选按钮(RadioButton)。这种方法只需要使用simple_list_item_multiple_choice.xml布局文件即可,该布局文件对应的资源ID如下:

android.R.layout.simple_list_item_multiple_choice

除此之外,可以向列表项添加CheckBox和CheckedTextView(用对号作为被选择的标志)控件。添加这两个控件分别需要使用simple_list_item_single_choice.xml和simple_list_item_checked. xml布局文件,这两个布局文件分别对应如下资源ID:

android.R.layout.simple_list_item_single_choice

android.R.layout.simple_list_item_checked

虽然从表面上看,使用上述3个布局文件添加的是RadioButton、CheckBox和CheckedTextView控件,但实际上,在这3个布局文件中只使用了CheckedTextView控件。之所以会显示不同的风格,是因为设置了<CheckedTextView>标签的android:checkMark属性,例如, simple_list_item_multiple_ choice.xml文件的内容如下:

<?xml version="1.0" encoding="utf-8"?>

<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"

  android:id="@android:id/text1"

  android:layout_width="fill_parent"

  android:layout_height="?android:attr/listPreferredItemHeight"

  android:textAppearance="?android:attr/textAppearanceLarge"

  android:gravity="center_vertical"

  android:checkMark="?android:attr/listChoiceIndicatorSingle"

  android:paddingLeft="6dip"

  android:paddingRight="6dip"

/>

上面的代码用一个风格属性值设置了android:checkMark属性,从而可以使CheckedTextView变成拥有不同风格的选择控件。

本例在垂直方向显示了3个ListView控件,分别用来演示上述3个布局文件的效果。设置这3个ListView的代码如下:

源代码文件:src/ch14/ChoiceListview/src/mobile/android/choice/listview/Main.java

String[] data = new String[]{ "Android", "Meego" };

// CheckedTextView

ArrayAdapter<String> aaCheckedTextViewAdapter =

  new ArrayAdapter<String>(this, android.R.layout.simple_list_item_checked, data);

lvCheckedTextView.setAdapter(aaCheckedTextViewAdapter);

// 设置成单选模式

lvCheckedTextView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);

// RadioButton

ArrayAdapter<String> aaRadioButtonAdapter =

  new ArrayAdapter<String>(this, android.R.layout.simple_list_item_single_choice, data);

lvRadioButton.setAdapter(aaRadioButtonAdapter);

// 设置成单选模式

lvRadioButton.setChoiceMode(ListView.CHOICE_MODE_SINGLE);

// CheckBox

ArrayAdapter<String> aaCheckBoxAdapter =

  new ArrayAdapter<String>(this, android.R.layout.simple_list_item_multiple_choice, data);

lvCheckBox.setAdapter(aaCheckBoxAdapter);

// 设置成多选模式

lvCheckBox.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);

如果只设置列表项的布局,在单击列表项时,相应的选项控件并不会被选中。因此,在设置列表项的布局后,还需要使用ListView.setChoiceMode方法设置选择的模式(单选或多选)。

运行本例后,单击相应的列表项,将显示如图14-8所示的效果。

 

▲图14-8 可单选和多选的ListView 控件

14.2.3 对列表项进行增、删、改操作

源代码目录:src/ch14/DynamicListview

对ListView控件的动态操作(添加、删除、修改列表项)往往是程序中必不可少的功能。本例中通过一个ViewAdapter类实现了动态向ListView中添加文本和图像列表项,并可以删除和修改某个被选中的列表项,以及清空所有的列表项。

编写一个ViewAdapter类一般需要从android.widget.BaseAdapter类继承。在BaseAdapter类中有两个非常重要的方法:getView和getCount。 其中ListView在显示某一个列表项时会调用getView方法来返回当前显示列表项的View对象。getCount方法返回当前ListView控件中列表项的总数。在添加或删除列表项后,getCount方法返回的值要进行调整,否则ListView可能会出现异常情况。

在本例中要向ListView添加两类列表项:文本列表项和图像列表项。因此,getView方法要根据当前列表项返回TextView或ImageView对象。在添加文本列表项时直接使用String类型的值,添加图像列表项时使用图像资源ID。因此,需要在ViewAdapter类中编写两个方法(addText和addImage)用于添加文本和图像列表项。ViewAdapter类(Main类的内嵌类)的完整代码如下:

源代码文件:src/ch14/DynamicListview/src/mobile/android/dynamic/listview/Main.java

private class ViewAdapter extends BaseAdapter

{

  private Context context;

  private List textIdList = new ArrayList();

  // 每显示一个列表项时都会调用getView方法获取列表项的View对象

  @Override

  public View getView(int position, View convertView, ViewGroup parent)

  {

    String inflater = Context.LAYOUT_INFLATER_SERVICE;

    LayoutInflater layoutInflater = (LayoutInflater) context

        .getSystemService(inflater);

    LinearLayout linearLayout = null;

    // 处理字符串类型的列表项

    if (textIdList.get(position) instanceof String)

    {

      // 装载用于字符串类型列表项的布局

      linearLayout = (LinearLayout) layoutInflater.inflate(

          R.layout.text, null);

      TextView textView = ((TextView) linearLayout

          .findViewById(R.id.textview));

      // 设置列表项的值(一个字符串)

      textView.setText(String.valueOf(textIdList.get(position)));

    }

    // 处理图像类型的列表项

    else if (textIdList.get(position) instanceof Integer)

    {

      // 装载用于图像类型列表项的布局

      linearLayout = (LinearLayout) layoutInflater.inflate(

          R.layout.image, null);

      ImageView imageView = (ImageView) linearLayout

          .findViewById(R.id.imageview);

      //设置列表项的值(一个图像)

      imageView.setImageResource(Integer.parseInt(String

          .valueOf(textIdList.get(position))));

    }

    // 返回列表项要使用的View对象

    return linearLayout;

  }

  // 返回列表项的总数,在对列表进行增、删、加操作后,该方法必须返回最后的列表项数量

  @Override

  public int getCount()

  {

    return textIdList.size();

  }

 

  public ViewAdapter(Context context)

  {

    this.context = context;

  }

  // 获取列表项ID,该方法可以是空实现,如果不使用该方法,返回任意值即可

  @Override

  public long getItemId(int position)

  {

    return position;

  }

  // 获取与列表项相关的对象,该方法可以是空实现,如果不使用该方法,返回任意值即可

  @Override

  public Object getItem(int position)

  {

    return textIdList.get(position);

  }

  // 向列表数据源添加文本类型的列表项

  public void addText(String text)

  {

    textIdList.add(text);

    // 对列表的数据源进行增、删、改操作后,必须调用notifyDataSetChanged方法使系统

    // 重新调用getView方法更新当前显示的列表项,该方法的作用就是当数据变化后,通过

    // 重新调用getView方法更新列表项。

    notifyDataSetChanged();

  }

  // 向列表数据源添加图像类型的列表项

  public void addImage(int resId)

  {

    textIdList.add(resId);

    notifyDataSetChanged();

  }

  // 从列表数据源删除指定索引的列表项

  public void remove(int index)

  {

    if (index < 0)

      return;

    textIdList.remove(index);

    notifyDataSetChanged();

  }

  // 编辑指定索引的列表项,该列表项必须是字符串类型

  public void modify(int index, String text)

  {

    if (index < 0)

      return;

    if (textIdList.get(index) instanceof String)

    {

      textIdList.set(index, text);

      notifyDataSetChanged();

    }

  }

  // 清空所有的列表项

  public void removeAll()

  {

    textIdList.clear();

    notifyDataSetChanged();

  }

}

在编写ViewAdapter类时应注意如下几点。

由于BaseAdapter类并不像窗口类有getLayoutInflater()方法可以获得LayoutInflater对象,因此,需要使用Context.getSystemService方法来获得LayoutInflater对象。

在本例中使用了两个XML布局文件(text.xml和image.xml)分别作为文本列表项和图像列表项的模板,这两个布局文件分别包含一个<TextView>和<ImageView>标签。

特别要注意的是getView方法的调用。ListView会根据当前可视的列表项决定什么时候调用getView方法,调用几次getView方法。例如,ListView中有10000个列表项,但getView方法并不会立刻调用10000次,而是根据当前屏幕上可见或即将显示的列表项调用getView方法,并通过position参数将当前列表项的位置(从0开始)传入getView方法。开发人员一般不需要关心ListView是在什么时候调用getView方法的,而只需要关注于当前要返回的列表项(View对象)即可。

由于文本列表项和图像列表项的数据是从List对象(textIdList变量)中获得的,因此,要注意边界问题。也就是说,getCount方法要返回正确的列表项个数,也就是List对象的元素个数,也可以认为getView方法的position参数值就是List对象中某个元素的索引。如果这时getCount方法返回了不正确的列表项个数(返回值比List对象中的元素总数还大),position的值可能会超过List对象的边界,系统就会抛出异常。

在创建完ViewAdapter类后,需要将ViewAdapter对象绑定到ListView上,代码如下:

源代码文件:src/ch14/DynamicListview/src/mobile/android/dynamic/listview/Main.java

lvDynamic = (ListView) findViewById(R.id.lvDynamic);

ViewAdapter viewAdapter = new ViewAdapter(this);

lvDynamic.setAdapter(viewAdapter);

本例在屏幕的正上方显示了5个按钮,分别用来添加文本列表项、添加图像列表项、删除当前列表项、随机修改指定列表项和删除所有的列表项。这5个按钮共用同一个单击事件方法,代码如下:

源代码文件:src/ch14/DynamicListview/src/mobile/android/dynamic/listview/Main.java

public void onClick(View view)

{

  switch (view.getId())

  {

    // 添加文本列表项

    case R.id.btnAddText:

      int randomNum = new Random().nextInt(data.length);

      viewAdapter.addText(data[randomNum]);

      break;

    // 添加图像列表项

    case R.id.btnAddImage:

      viewAdapter.addImage(getImageResourceId());

      break;

    // 删除当前列表项

    case R.id.btnRemove:        

      viewAdapter.remove(selectedIndex);

      selectedIndex = -1;

      break;

    // 修改当前列表项

    case R.id.btnModify:

      viewAdapter.modify(selectedIndex,data[new Random().nextInt(data.length)]);

      selectedIndex = -1;  

    // 删除所有的列表项

    case R.id.btnRemoveAll:

      viewAdapter.removeAll();

      break;

    }

}

其中data变量为一个String[]对象,定义了在列表项中显示的文本集合。getImageResourceId方法从5个图像资源中随机选择一个图像资源ID作为当前添加的列表项的图像资源ID,代码如下:

源代码文件:src/ch14/DynamicListview/src/mobile/android/dynamic/listview/Main.java

private int getImageResourceId()

{

  int[] resourceIds = new int[]

  { R.drawable.item1, R.drawable.item2, R.drawable.item3,sR.drawable.item4, R.drawable.

  item5 };

  return resourceIds[new Random().nextInt(resourceIds.length)];

}

运行本例后,添加一些文本和图像列表项,将显示如图14-9所示的效果。

 

▲图14-9 动态添加、删除、修改列表项

扩展学习:重用列表项的View对象

如果ListView中的所有列表项的View对象都使用同一个布局,那么就可以利用getView方法的convertView参数重用列表项的View对象。例如,ListView控件一页只能显示8个列表项,而列表项总数有20个。那么在显示第9个列表项时,第1个列表项就会隐藏起来。系统会将隐藏的列表项对应的View对象通过convertView参数传入getView方法。所以convertView参数值不为空,则说明系统已将一个View对象传入了getView方法,因此,标准的做法是在getView方法中先判断convertView参数值是否为空,如果不为null,直接返回convertView参数值即可,但在返回之前需要更新当前列表项要显示的内容,否则还是会显示被隐藏列表项的内容。如果convertView参数值为null(说明ListView还没有翻过页,当前显示的是第1页),就只能创建新的View对象了。

public View getView(int position, View convertView, ViewGroup parent)

{

  if(convertView == null)

  {

    convertView = layoutInflater.inflate(

        R.layout.text, null);

  }

……

  // 利用convertView获取相应的控件对象,并更新当前列表项的内容

  return convertView;

}

通过重用列表项View对象的方法显示列表项,不管ListView要显示的列表项总数是多少,最多只需要创建在一页中显示的View对象。例如,ListView每页只能显示8个列表项,则最多只需要创建8个View对象即可。因此大大降低了内存的消耗,尤其对移动设备而言至关重要。

要注意的是,重用列表项的View对象需要所有列表项的View对象都使用了同一个布局,否则系统不一定会返回哪个布局的View对象,这样一来显示的列表项就会混乱。由于本节的例子使用了两个布局文件(处理文本列表项和图像列表项),因此没有考虑convertView参数。如果ListView有多个列表项使用了不同的布局(有限个布局),还想重用列表项的View对象,可以将这些布局都放到一个布局文件中,然后在getView方法中通过隐藏不需要的视图的方式来达到重用列表项View对象的目的。

14.2.4 改变列表项的背景色

源代码目录:src/ch14/ColorListview

前面章节中使用的ListView列表项在选中状态时背景色都是黄色的。实际上,可以将选中状态的背景色改成任意颜色,甚至是绚丽的图像。

改变列表项选中状态的背景色可以使用<ListView>标签的android:listSelector属性,也可以使用ListView.setSelector方法。例如,将背景色设为绿色的方式是将一个绿色的png图(green.png)复制到res/drawable目录中,然后在<ListView>标签中设置android:listSelector属性的值为"@drawable/green",或使用如下代码设置。

ListView listView = (ListView) findViewById(R.id.listview);

listView.setSelector(R.drawable.green);

在本例中有3个RadioButton控件,分别将列表项选中状态的背景颜色设置成默认颜色、绿色和光谱颜色。这3个RadioButton控件共享一个单击事件方法,代码如下:

源代码文件:src/ch14/ColorListview/src/mobile/android/color/listview/Main.java

public void onClick(View view)

{

  switch (view.getId())

  {

    case R.id.rbdefault:

      // 设置成默认背景颜色

      listView.setSelector(defaultSelector);

      break;

    case R.id.rbGreen:

      // 设置绿色背景

      listView.setSelector(R.drawable.green);

      break;      

    case R.id.rbSpectrum:

      // 设置光谱背景

      listView.setSelector(R.drawable.spectrum);

      break;

  }

}

在上面代码中的defaultSelector是Drawable类型变量,该变量表示列表项被选中状态默认的背景颜色,通过ListView.getSelector方法可获得该值。

运行本例后,分别单击“绿色”和“光谱”选项按钮,将显示如图14-10和图14-11所示的效果。

14.2.5 ListActivity(封装ListView的Activity)

ListActivity实际上是ListView和Activity的结合体,也就是说,一个ListActivity就是内嵌一个ListView控件的窗口。在ListActivity类的内部通过代码来创建ListView对象,因此,使用ListActivity并不需要使用布局文件来定义ListView控件。

 

▲图14-10 设置绿色背景

 

▲图14-11 设置光谱背景

如果在某些窗口中只包含一个ListView,使用ListActivity是非常方便的,可以通过ListActivity.setListAdapter方法来设置Adapter对象。该方法相当于调用了ListView.setAdapter方法。

也可以通过ListActivity.getListView方法获得当前ListActivity的ListView对象,并像操作普通的ListView对象一样操作ListActivity中的ListView对象。

14.2.6 ExpandableListView(可扩展的列表控件)

源代码目录:src/ch14/ExpandableListview

Android SDK提供了一个可以展开的ExpandableListView列表控件。与菜单和子菜单类似,ExpandableListView的列表项分为组列表项和子列表项,单击组列表项(包含子列表项的列表项)后,会显示当前列表项下的所有子列表项。

ExpandableListView是ListView的直接子类,因此,ExpandableListView拥有ListView的一切特性。当然,与ListView一样,ExpandableListView类也有一个与之对应的ExpandableListActivity类,该类包含一个ExpandableListView控件,如果窗口上只有一个ExpandableListView控件,建议直接使用ExpandableListActivity类来代替Activity类。

本节将使用ExpandableListActivity类来创建ExpandableListView对象,并添加几个列表项和相应的子列表项。ExpandableListView的用法与ExpandableListActivity非常相似,读者可参考本例提供的代码使用ExpandableListView控件。

与ListActivity一样,ExpandableListActivity类也需要一个Adapter对象。在本例中使用了一个继承自BaseExpandableListAdapter类的MyExpandableListAdapter类处理列表数据。在MyExpandableListAdapter类中有两个核心方法:getGroupView和getChildView。这两个方法分别用来返回列表项和子列表项的View对象。MyExpandableListAdapter类(Main类的内嵌类)的完整代码如下:

源代码文件:src/ch14/ExpandableListview/src/mobile/android/expandable/listview/Main.java

public class MyExpandableListAdapter extends BaseExpandableListAdapter

{

  private String[] provinces =

  { "辽宁", "山东", "江西", "四川" };

  private String[][] cities =

  {

  { "沈阳", "大连", "鞍山", "抚顺" },

  { "济南", "青岛", "淄博", "枣庄" },

  { "南昌", "景德镇" },

  { "成都", "自贡", "攀枝花" } };

  // 根据组索引和组列表项索引获取每一个列表项的值,就是cities数组的元素值

  public Object getChild(int groupPosition, int childPosition)

  {

    return cities[groupPosition][childPosition];

  }

  // 返回列表项的ID

  public long getChildId(int groupPosition, int childPosition)

  {

    return childPosition;

  }

  // 返回每一个列表项组中列表项的总数

  public int getChildrenCount(int groupPosition)

  {

    return cities[groupPosition].length;

  }

  // 返回用于显示列表项内容的TextView对象

  public TextView getGenericView()

  {

    AbsListView.LayoutParams lp = new AbsListView.LayoutParams(

        ViewGroup.LayoutParams.FILL_PARENT, 64);

    TextView textView = new TextView(Main.this);

    textView.setLayoutParams(lp);

    textView.setGravity(Gravity.CENTER_VERTICAL Gravity.LEFT);

    textView.setPadding(36, 0, 0, 0);

    textView.setTextSize(20);

    return textView;

  }

  // 返回列表项使用的View对象

  public View getChildView(int groupPosition, int childPosition,

      boolean isLastChild, View convertView, ViewGroup parent)

  {

    if(convertView == null)

      convertView = getGenericView();

    // convertView本身就是TextView对象

    TextView textView = (TextView) convertView;

    textView.setText(getChild(groupPosition, childPosition).toString());

    return textView;

  }

  // 返回列表项组的值(provinces数组元素值)

  public Object getGroup(int groupPosition)

  {

    return provinces[groupPosition];

  }

  // 返回列表项组的总数

  public int getGroupCount()

  {

    return provinces.length;

  }

 

  public long getGroupId(int groupPosition)

  {

    return groupPosition;

  }

  // 返回列表项组使用的View对象

  public View getGroupView(int groupPosition, boolean isExpanded,

      View convertView, ViewGroup parent)

  {

    if(convertView == null)

      convertView = getGenericView();

    // convertView本身就是TextView对象

    TextView textView = (TextView) convertView;

    textView.setText(getGroup(groupPosition).toString());

    return textView;

  }

  public boolean isChildSelectable(int groupPosition, int childPosition)

  {

    return true;

  }

  public boolean hasStableIds()

  {

    return true;

  }

}

ExpandableListActivity类也需要使用setListAdapter方法指定Adapter对象,代码如下:

ExpandableListAdapter adapter = new MyExpandableListAdapter();

setListAdapter(adapter);

当单击子列表项时会弹出一个菜单,因此,需要在onCreate方法中使用下面的代码将上下文菜单注册到ExpandableListView上。

registerForContextMenu(getExpandableListView());

在本例中,与上下文菜单相关的事件方法是onCreateContextMenu和onContextItemSelected,当单击子列表项时系统会调用onCreateContextMenu方法创建弹出菜单,单击菜单项时系统会调用onContextItemSelected方法。这两个方法的实现代码如下:

源代码文件:src/ch14/ExpandableListview/src/mobile/android/expandable/listview/Main.java

// 创建上下文菜单

@Override

public void onCreateContextMenu(ContextMenu menu, View view,

    ContextMenuInfo menuInfo)

{

  ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo;

  // 获得当前列表项的类型

  int type = ExpandableListView.getPackedPositionType(info.packedPosition);

  // 获得当前列表项的文本

  String title = ((TextView) info.targetView).getText().toString();

  // 单击子菜单项时,弹出上下文菜单

  if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD)

  {

    menu.setHeaderTitle("弹出菜单");

    menu.add(0, 0, 0, title);

  }

}

// 响应菜单项单击事件

@Override

public boolean onContextItemSelected(MenuItem item)

{

  ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) item

      .getMenuInfo();

  String title = ((TextView) info.targetView).getText().toString();

  Toast.makeText(this, title, Toast.LENGTH_SHORT).show();

  return true;

}

运行本例后,长按第1个列表项“辽宁”的第1个子列表项“沈阳”,将显示如图14-12所示的效果。

 

▲图14-12 可展开的ListView

14.2.7 Spinner(下拉列表控件)

源代码目录:src/ch14/Spinner

Spinner控件用于显示一个下拉列表。该控件的用法与ListView控件类似,在装载数据时也需要创建一个Adapter对象,并在创建Adapter对象的过程中指定要装载的数据(数组或List对象)。例如,下面的代码分别使用ArrayAdapter和SimpleAdapter对象向两个Spinner控件添加数据。

源代码文件:src/ch14/Spinner/src/mobile/android/spinner/Main.java

public void onCreate(Bundle savedInstanceState)

{

  super.onCreate(savedInstanceState);

  setContentView(R.layout.main);

  // 开始创建第一个Spinner对象

  Spinner spinner1 = (Spinner) findViewById(R.id.spinner1);

  String[] mobileOS = new String[]

  { "Android", "IPhone", "Symbian", "Meego", "Window Phone7" };

  ArrayAdapter<String> aaAdapter = new ArrayAdapter<String>(this,

      android.R.layout.simple_spinner_item, mobileOS);

  // 为第1个Spinner设置Adapter对象

  spinner1.setAdapter(aaAdapter);

  //  开始创建第2个Spinner对象

  Spinner spinner2 = (Spinner) findViewById(R.id.spinner2);

  // 第2个Spinner中的数据是一个Map对象的集合

  final List<Map<String, Object>> items = new ArrayList<Map<String, Object>>();

  Map<String, Object> item1 = new HashMap<String, Object>();

  item1.put("ivLogo", R.drawable.calendar);

  item1.put("tvApplicationName", "多功能日历");

  Map<String, Object> item2 = new HashMap<String, Object>();

  item2.put("ivLogo", R.drawable.eoemarket);

  item2.put("tvApplicationName", "eoeMarket客户端");

  items.add(item1);

  items.add(item2);

  SimpleAdapter simpleAdapter = new SimpleAdapter(this, items,

      R.layout.item, new String[]

      { "ivLogo", "tvApplicationName" }, new int[]

      { R.id.ivLogo, R.id.tvApplicationName });

  // 为第2个Spinner设置Adapter对象

  spinner2.setAdapter(simpleAdapter);

  // 设置第2个Spinner的列表项选择事件,当选中某个列表项时,会在窗口的标题栏显示当前

  // 列表项的文本内容

  spinner2.setOnItemSelectedListener(new OnItemSelectedListener()

  {

    @Override

    public void onItemSelected(AdapterView<?> parent, View view,

        int position, long id)

    {

      setTitle(items.get(position).get("tvApplicationName").toString());

    }

    @Override

    public void onNothingSelected(AdapterView<?> parent)

    {

    }

  });

}

在设置第2个Spinner的Adapter对象时使用了一个item.xml布局文件(资源ID为R.layout.item),该布局文件的内容如下:

源代码文件:src/ch14/Spinner/res/layout/item.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:orientation="horizontal" android:layout_width="fill_parent"

  android:layout_height="wrap_content">

  <ImageView android:id="@+id/ivLogo" android:layout_width="60dp"

    android:layout_height="60dp" android:src="@drawable/icon"

    android:paddingLeft="10dp" />

  <TextView android:id="@+id/tvApplicationName" android:textColor="#000"

    android:layout_width="wrap_content" android:layout_height="fill_parent"

    android:textSize="16dp" android:gravity="center_vertical"

    android:paddingLeft="10dp" />

</LinearLayout>

在item.xml文件中定义了两个控件:<ImageView>和<TextView>。ID分别为ivLogo和tvApplicationName。由于item.xml是用于列表项的布局,也就是说,每一个列表项由一个图像和一个文本组成。那么在为Spinner设置数据时(Adapter对象),就不能只设置一个文本了,需要同时指定每一个列表项要显示的图像资源ID和文本,所以在本例中将图像资源ID和文本放到Map对象中(每一个列表项的数据对应一个Map对象),而Map对象的key就是控件标签的ID值,这样系统就可以利用Map对象找到控件要显示的数据了。

运行本例后,单击第1个和第2个Spinner控件右侧的下拉按钮,将显示如图14-13和图14-14所示的效果。

 

▲图14-13 只显示文本的下拉列表框

 

▲图14-14 带文本和图像的下拉列表框

扩展学习:SimpleAdapter类

在本例中使用了一个SimpleAdapter类来处理Spinner显示的数据,这个类可以将任何自定义的XML布局文件作为列表项来使用。我们先来看看SimpleAdapter类构造方法的原型。

public SimpleAdapter(Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to)

其中第1个参数context不必多说了,这个参数在前面已经多次提到过了,一般在窗口类中使用this作为该参数的值。现在需要着重说的是后4个参数。

data是一个List类型的参数,而List对象的元素值是Map<String, ?>类型。我们可以回顾一下本例的列表项布局文件item.xml的内容。在该布局文件中定义了两个控件:ImageView和TextView,每一个列表项都要根据不同的情况设置ImageView的图像和TextView的文本。假设要添加两个列表项,就意味着需要设置4个值(每个列表项2个值),每个列表项显示内容可以封装到Map对象中。key表示相应控件的ID(在本例中是ivLogo和tvApplicationName,注意,不是ID值,而是R.id类中的变量名),value表示具体的值。在本例中,需要使用如下代码来设置这两个列表项中控件显示的内容:

Map<String, Object> item1 = new HashMap<String, Object>();

// 设置第1个列表项的数据

item1.put("ivLogo", R.drawable.calendar);

item1.put("ivApplicationName", "多功能日历");

Map<String, Object> item2 = new HashMap<String, Object>();

// 设置第2个列表项的数据

item2.put("ivLogo", R.drawable.eoemarket);

item2.put("ivApplicationName", "eoemarket客户端");

List<Map<String, Object>> data = new ArrayList<Map<String, Object>>();

// 将两个Map对象添加到List对象中,该对象就是SimpleAdapter构造方法的第2个参数值

data.add(item1);

data.add(item2);

从上面的代码可以很容易知道data参数表示所有列表项的数据,List对象的元素(Map对象)表示列表项的数据。

SimpleAdapter类构造方法的第3个参数resource表示列表项模板的资源ID,在本例中是R.layout.item。from和to参数分别表示布局文件(item.xml)中控件标签在R.id类中对应的变量名(由于从android:id属性中获得的只是一个int类型的值,而不是变量名,所以要使用from指定相应的变量名)及android:id属性值。在本例中使用如下代码设置这两个参数的值:

String[] from = new String[]{ "ivLogo", "tvApplicationName" };

int[] to = new int[]{ R.id.ivLogo, R.id.tvApplicationName };

from和to数组设置的控件顺序要一致,也就是说,from的第n个元素要对应于to的第n个元素。

在最后需要向List对象中添加SimpleAdapter所需的数据,并使用SimpleAdapter对象为列表控件提供数据,代码如下:

public void onCreate(Bundle savedInstanceState)

{

  super.onCreate(savedInstanceState);

  List<Map<String, Object>> appItems = new ArrayList<Map<String, Object>>();

  // 设置data参数的值,其中resIds和applicationNames保存列表项中相应组件的值

  for (int i = 0; i < applicationNames.length; i++)

  {

    Map<String, Object> appItem = new HashMap<String, Object>();

    appItem.put("ivLogo", resIds[i]);

    appItem.put("tvApplicationName", applicationNames[i]);

    appItems.add(appItem);

  }

  SimpleAdapter simpleAdapter = new SimpleAdapter(this, appItems,

      R.layout.main, new String[]{ "tvApplicationName", "ivLogo" },

      new int[]{ R.id.tvApplicationName, R.id.ivLogo});

  setListAdapter(simpleAdapter);

}