1 ListView
ListView
是Android
中显示数据常用的控件之一,主要用于显示一个垂直滚动的数据集合,随着Android手机对性能要求越来越高,一个更现代,更灵活,显示列表性能更优异的RecyclerView
将会逐渐取代ListView
的数据显示方式,但是目前为止,ListView
在开发中还是十分常见的,并未被弃用。
继承关系
java.lang.Object
↳android.view.View
↳android.view.ViewGroup
↳android.widget.AdapterView<android.widget.ListAdapter>
↳android.widget.AbsListView
↳android.widget.ListView
1.1 工作原理
ListView
仅作为容器(列表),用于装载 & 显示数据(即列表项Item
)- 而容器内的具体数据(列表项
Item
)则是由适配器(Adapter
)提供 ListView
负责以列表的形式向我们展示Adapter
提供的内容
适配器(Adapter): 作为View
和数据之间的桥梁 & 中介,将数据映射到要展示的View
中,当需要显示数据时,ListView
会向Adapter
取出数据,从而加载显示
1.2 缓存原理
试想一个场景:若把所有数据集合的信息都加载到ListView
上显示,若 ListView
要为每个数据都创建一个视图,那么会占用非常多的内存
-
为了节省空间和时间,
ListView
不会为每一个数据创建一个视图,而是采用了Recycler组件,用于回收 & 复用View
-
当屏幕需显示
n
个Item
时,那么ListView
会创建n+1
个视图;当第1个Item
离开屏幕时,此Item
的View
被回收至缓存,入屏的Item
的View
会优先从该缓存中获取 -
只有
Item
完全离开屏幕后才可复用,这也是为什么ListView
要创建比屏幕需显示视图多1个的原因:缓冲显示视图。即:第1个Item
离开屏幕是有过程的,会有第1个Item
的下半部分 & 第n+1个Item
上半部分同时在屏幕中显示的状态,此时仍无法使用缓存的View
,只能继续用新创建的视图View
-
实例演示。假设:屏幕只能显示5个
Item
,那么ListView
只会创建(5+1)个Item
的视图;当第1个Item
完全离开屏幕后才会回收至缓存从而复用(用于显示第7个Item
)
2 具体使用
- 引入
ListView
和普通的View
一样,直接在布局中添加ListView
控件即可。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.ListViewActivity">
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
- 根据实际需求定制列表项:实现
ListView
每行的xml布局(即item
布局)
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="@+id/img_ico"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/person1"
android:layout_gravity="center"/>
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="猫九"
android:layout_gravity="center"
android:layout_marginLeft="50dp"/>
</LinearLayout>
- 定义一个
HashMap
构成的列表以键值对的方式存放数据
private final String[] names = {"深圳", "上海", "西安", "杭州", "深圳", "上海", "西安", "杭州", "深圳", "上海", "西安", "杭州"};
private final int[] imgs = {R.drawable.person1,R.drawable.person2,R.drawable.person1,R.drawable.person2,R.drawable.person1, R.drawable.person2,
R.drawable.person1,R.drawable.person2,R.drawable.person1,R.drawable.person2,R.drawable.person1,R.drawable.person2};
//定义一个HashMap构成的列表以键值对的方式存放数据
ArrayList<HashMap<String, Object>> mListItems = new ArrayList<HashMap<String, Object>>();
//循环填充数据
for (int i = 0; i < names.length; i++) {
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("name", names[i]);
hashMap.put("imgs", imgs[i]);
mListItems.add(hashMap);
}
- 构造SimpleAdapter对象,设置适配器
SimpleAdapter mSimpleAdapter = new SimpleAdapter(this, mListItems, R.layout.my_list_view,
new String[]{"img", "name"}, new int[]{R.id.img_ico, R.id.text_name});
ListView listView = findViewById(R.id.my_list_view);
- 将
LsitView
绑定到SimpleAdapter
上
listView.setAdapter(mSimpleAdapter);
结果显示
3 常用属性和相关方法
3.1 AbsListView的常用属性和相关方法
标题 | 说明 | 备注 |
---|---|---|
android:choiceMode | 列表的选择行为 默认:none 没有选择行为 | none:不显示任何选中项目 singleChoice:允许单选 multipleChoiceModel:允许多选 配合getCheckedItemPosition 、getCheckedItemCount等使用 |
android:drawSelectorOnTop | --- | 如果该属性设置为true,选中的列表项的选中颜色会成为前景颜色 |
android:transcriptMode | 指定列表添加新的选项的时候, 是否自动滑动到底部,显示新的选项 | 如果该属性设置为true,选中的列表项的选中颜色会成为前景颜色 |
android:fastScrollEnabled | 是否允许快速滚动 | 如果该属性设置为true,将会显示滚动图标, 并允许用户拖动该滚动图标进行快速滚动 |
3.2 Listview提供的XML属性
XML属性 | 说明 | 备注 |
---|---|---|
android:divider | 设置List列表项的分隔条 可用颜色分割,也可用图片(Drawable)分割 | 不设置列表之间的分割线, 可设置属性为@null |
android:dividerHeight | 用于设置分隔条的高度 | |
android:background | 设置列表的背景 | |
android:entries | 指定一个数组资源, Android将根据该数组资源来生成ListView | |
android:footerDividerEnabled | 如果设置成false, 则不在footer View之前绘制分隔条 | |
andorid:headerDividerEnabled | 如果设置成false, 则不再header View之前绘制分隔条 |
3. Adapter简介
Adapter本身是一个接口,Adapter接口及其子类的继承关系如下图:
-
Adapter接口派生了ListAdapter和SpinnerAdapter两个子接口
其中ListAdapter为AbsAdapter提供列表项,而SpinnerAdapter为AbsSpinner提供列表项 -
ArrayAdapter、SimpleAdapter都是Android API给我们提供好的适配器,直接使用即可,不过模式都已经写死了
- ArrayAdapter:简单、易用的Adapter,用于将数组绑定为列表项的数据源,支持泛型操作
- SimpleAdapter:功能强大的Adapter,可以将数据源的数据绑定到item中的view中
- SimpleCursorAdapter:与SimpleAdapter类似,用于绑定游标(直接从数据数取出数据)作为列表项的数据源,不常用
- BaseAdapter:常用,我们需要继承BaseAdapter来自定义我们自己的适配器
3.1 ArrayAdapter
使用简单、用于将数组、List 形式的数据绑定到列表中作为数据源,支持泛型操作
步骤:
- 在xml文件布局上实现ListView
- 在Activity中定义数据源(列表或者数组)
List<String> listData = new ArrayList<>();
for(int i=0;i<20;i++){
listData.add("item数据"+i);
}
- 构造ArrayAdapter对象,设置适配器
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(this, android.R.layaout.simple_list_item_1, listData);
第一参数都是Context
第二个参数就是要添加的item的布局id
第三个参数是数据,数据可以使用数组也可以使用List
备注:如果 List 里面存放的是一个普通对象而不是String的话,则显示在item中的数据为这个对象调用toString后的结果
- 将ListView绑定到ArrayAdapter上
listView.setAdapter(arrayAdapter);
3.2 SimpleAdapter
- 定义:功能强大的Adapter,用于将XML中控件绑定作为列表项的数据源
- 特点:可对每个列表项进行定制(自定义布局),能满足大多数开发的需求场景,灵活性较大
使用步骤:
1 在xml中添加ListView
2 实现item布局(根据实际UI需求)
3 创建数据源(数据源形式有要求List<?extends Map<String, ?>>)
4 创建SimpleAdapter适配器
5 将SimpleAdapter适配器绑定到ListView中
上面已经实现,这里不做介绍
3.3 BaseAdapter
使用步骤:
- 定义主xml布局
- 根据需要定义ListView每行所实现的xml布局(这里直接使用SimpleAdapter中的item布局)
- 创建数据源
public class ItemData {
private String name;
private int img;
get and set 方法
@Override
public String toString() {
return "ItemData{" +
"name='" + name + ''' +
", img=" + img +
'}';
}
}
private final String[] names = {"深圳", "上海", "西安", "杭州", "深圳", "上海", "西安", "杭州", "深圳", "上海", "西安", "杭州"};
private final int[] imgs = {R.drawable.person1,R.drawable.person2,R.drawable.person1,R.drawable.person2,R.drawable.person1, R.drawable.person2,
R.drawable.person1,R.drawable.person2,R.drawable.person1,R.drawable.person2,R.drawable.person1,R.drawable.person2};
private void initItemData() {
mItemDataList = new ArrayList<ItemData>();
for (int i = 0; i < names.length; i++) {
ItemData itemData = new ItemData();
itemData.setName(names[i]);
itemData.setImg(imgs[i]);
mItemDataList.add(itemData);
}
}
- 定义一个Adapter类继承BaseAdapter,重写里面的方法
public class MyAdapter extends BaseAdapter {
private Context mContext;
private LayoutInflater mInflater;
private List<ItemData> mDataItemLists;
public MyAdapter(Context context, List<ItemData> list) {
mContext = context;
mDataItemLists = list;
mInflater =LayoutInflater.from(context);
}
/**
* 适配器中数据集的数据个数,即:ListView的长度(item的个数)
* @return
*/
@Override
public int getCount() {
return mDataItemLists.size();
}
/**
* 获取数据集中与索引对应的数据项
* @param position
* @return
*/
@Override
public Object getItem(int position) {
return mDataItemLists.get(position);
}
/**
* 获取指定行对应的ID
* @param position
* @return
*/
@Override
public long getItemId(int position) {
return position;
}
/**
* 获取每一行 Item 的显示内容
* @param position
* @param convertView
* @param parent
* @return
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = mInflater.inflate(R.layout.my_list_view, null);
ImageView imageView = view.findViewById(R.id.img_ico);
TextView textView = view.findViewById(R.id.text_name);
imageView.setImageResource(mDataItemLists.get(position).getImg());
textView.setText(mDataItemLists.get(position).getName());
return view;
}
}
- 构造Adapter对象,设置适配器
mMyAdapter = new MyAdapter(this, mItemDataList);
- 将LsitView绑定到Adapter上
mListView.setAdapter(mMyAdapter);
3.3.1 getView()方法的几种实现方式
实现方式1:直接返回索引对应的数据的视图
这是最直接的一种方式,目标很明确,就是返回对应的视图。同样缺点也很明确,没有利用ListView
对item
的复用机制,假如有1000个item
就要绘制1000个view
。然后再进行findViewById
会十分消耗资源。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View itemView = mInflater.inflate(R.layout.my_list_view, null);
ImageView imageView = itemView.findViewById(R.id.img_ico);
TextView textView = itemView.findViewById(R.id.text_name);
imageView.setImageResource(mDataItemLists.get(position).getImg());
textView.setText(mDataItemLists.get(position).getName());
return itemView;
}
实现方式2:使用convertView
作为View
缓存,将convertView
作为getView()
的输入参数,返回参数 借助ListView
的缓存机制,实现view
的复用
优点:减少了重绘View
的次数
缺点:但是每次都要通过findViewById()
寻找View
组件
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.my_list_view, null);
}
ImageView imageView = convertView.findViewById(R.id.img_ico);
TextView textView = convertView.findViewById(R.id.text_name);
imageView.setImageResource(mDataItemLists.get(position).getImg());
textView.setText(mDataItemLists.get(position).getName());
return convertView;
}
实现方式三:在方式二的基础上,进行优化,引入ViewHolder
减少findViewById()
具体原理:
- a. 将
convertView
作为getView()
的输入参数 & 返回参数,从而形成反馈 - b. 形成了
Adapter
的itemView
重用机制,减少了重绘View的次数
优点:重用View
时就不用通过findViewById()
重新寻找View
组件,同时也减少了重绘View
的次数,是ListView
使用的最优化方案
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
Log.d(TAG, "getView: ");
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.my_list_view, null);
viewHolder.imageView = (ImageView)convertView.findViewById(R.id.img_ico);
viewHolder.textView = (TextView)convertView.findViewById(R.id.text_name);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder)convertView.getTag();
}
Log.d(TAG, "getView: position ="+position);
viewHolder.imageView.setImageResource(mDataItemLists.get(position).getImg());
viewHolder.textView.setText(mDataItemLists.get(position).getName());
return convertView;
}
public static class ViewHolder {
ImageView imageView;
TextView textView;
}
ListView的优化总结:
4 进阶使用:添加头部 & 尾部View
在日常使用中,我们常常会需要在ListView
头部/尾部添加视图
- 添加头部/尾部视图。header_view.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="header"
android:textSize="20sp"
android:gravity="center"/>
</LinearLayout>
- 添加到ListView中
View headerView = LayoutInflater.from(this).inflate(R.layout.header_view, null);
View footerView = LayoutInflater.from(this).inflate(R.layout.footer_view, null);
mListView.addHeaderView(headerView);
mListView.addFooterView(footerView);