- Android开发权威指南(第二版)
- 李宁编著
- 1647字
- 2024-12-21 14:46:44
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);
}