Android 学习笔记 —— ListView 的使用
ListView 的使用
ListView 用于显示可垂直滚动的视图集合,其中每个视图都位于列表中前一个视图的正下方。要使用更现代、更灵活、更高效的方法来显示列表,可以且推荐使用功能更强大的 RecyclerView 。
要显示列表,可在 XML 布局文件中添加 ListView
控件:
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
ListView 间接继承至 android.widget.AdapterView
,是一个适配器视图。它不知道子视图的类型和内容,需要通过适配器请求视图。为了显示 ListView 中的子项,需要调用 setAdapter(android.widget.ListAdapter)
方法将适配器与 ListView 关联,传入参数为 ListAdapter 接口的实现类,常用的适配器有 ArrayAdapter、SimpleAdapter 和 BaseAdapter。
- ArrayAdapter:继承BaseAdapter,实现了四个抽象方法,多用于只有文本的列表。
- SimpleAdapter:继承BaseAdapter,实现了四个抽象方法,可以简单的实现列表样式(无法轮播)。
- BaseAdapter:顾名思义,最基础的适配器,有四个抽象方法,可以方便的根据需求做出更改。
使用 ArrayAdapter 作为适配器
特点:使用简单、用于将 List 或数组形式的数据绑定到列表中作为数据源,支持泛型操作。
缺点:只能设置 Item 项中的一个 TextView。一般仅用来展示单行文本的列表。
使用步骤:
-
在 XML 布局文件中添加
ListView
控件。 -
在 Activity 中定义数据源 data(数组或 List)。
-
构造 ArrayAdapter 对象,设置适配器。
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(this, android.R.layaout.simple_list_item_1, data); // 第一个参数是上下文 Context // 第二个参数是 Item 的布局(这里使用内置的布局,里面只有一个 TextView) // 第三个参数是数据源,数据可以使用 List 也可以使用数组。 // 也可以使用自定的 Item 布局,但创建 ArrayAdapter 的构造方法需要指定该布局中的一个 TextView,如下 ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(this, R.layaout.list_item_diy, R.id.text_view, data);
注意: 如果 List 里面存放的是一个普通对象而不是 String 的话,则显示在 Item 中的数据为这个对象调用 toString 后的结果。
-
使用
setAdapter()
方法将 ListView 与 ArrayAdapter 绑定。ListView listView = (ListView) findViewById(R.id.list_view); listView.setAdapter(arrayAdapter);
-
设置行为响应事件(以点击事件为例)。
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // parent 指的就是当前的 ListView 对象 // view 指当前点击的子项视图 ItemView // position 指当前子项在适配器中的位置 // id 指当前点击子项的行ID,可理解为第几行(从0开始) } });
使用 SimpleAdapter 作为适配器
特点:可以将数据源的数据一一绑定到 Item 中的 view。
使用步骤:
-
在 XML 布局文件中添加
ListView
控件。 -
根据实际需求自定义一个 Item 布局(以联系人列表为例)。
<LinearLayout ... > <ImageView android:id="@+id/iv_contact_item_avatar" ... /> <TextView android:id="@+id/tv_contact_item_name" ... /> </LinearLayout>
-
在 Activity 中定义数据源 data(固定格式为
List<? extends Map<String, ? extends Object>>
,一般使用List<HashMap<String, Object>>
)。HashMap<String, Object> map = new HashMap<>(); map.put("avatar", R.mipmap.ic_launcher); map.put("name", "阿咩Amie"); // Item 中需要设置的数据都要保存在 HashMap 中,图片只能使用 res 资源中的本地图片 // 数据源一般从网络中获取,且图片数据大多采用地址字符串,网络加载图片暂不深入 List<HashMap<String,Object>> data = new ArrayList<>(); data.add(map);
-
构造 SimpleAdapter 对象,设置适配器。
// 将 HashMap 的 key 组成一个 String 数组 String[] form = new String[]{"avatar", "name"}; // 将 Item 布局中控件的 id 组成一个 int 数组,要和 form 对应 int[] to = {R.id.iv_contact_item_avatar, R.id.tv_contact_item_name}; SimpleAdapter simpleAdapter = new SimpleAdapter(this, data, R.layout.list_item_contact, form, to); // 第一个参数是上下文 Context // 第二个参数是数据源,List<HashMap<String,Object>> data // 第三个参数是 Item 的布局 id // 第四个参数是 HashMap 中与每个 Item 相关联的 key 所组成的 String 数组 // 第五个参数是 Item 布局中控件的 id 所组成的 int 数组,位置须与第四个参数对应,形成映射关系
-
使用
setAdapter()
方法将 ListView 与 SimpleAdapter 绑定(与 ArrayAdapter 方式相同)。 -
设置行为响应事件(与 ArrayAdapter 方式相同)。
使用 BaseAdapter 自定义适配器
特点:可最大程度定制 Item。
使用步骤:
-
在 XML 布局文件中添加
ListView
控件。 -
根据实际需求自定义一个 Item 布局(同上,以联系人列表为例)。
-
定义数据源实体类 ContactEntity,并设置 getter 和 setter 方法。
public class ContactEntity { private Integer avatar; // 依旧使用本地资源文件 private String name; // ... }
-
创建自定义适配器类 ContactAdapter,继承 BaseAdapter 并实现其方法。
public class ContactAdapter extends BaseAdapter { @Override public int getCount() { return 0; // 返回多少,ListView 就显示多少条 } @Override public Object getItem(int position) { return null; // 返回数据源中指定位置上的数据项 } @Override public long getItemId(int position) { return 0; // 返回指定 Item 数据项在适配器中的 ID,一般直接使用 position 作为 ID } @Override public View getView(int position, View convertView, ViewGroup parent) { return null; // 获取一个对应数据源中指定位置上的数据的视图 View } }
-
在 ContactAdapter 类中添加一些属性来完善适配器,如用于存放数据源的对象、ListView 所在的上下文 Context。还要记得添加对应的构造函数。
public class ContactAdapter extends BaseAdapter { private final Context mContext; // 上下文对象 private final List<ContactEntity> mData; // 对应数据源 List public ContactAdapter(Context context, List<ContactEntity> mData) { this.mContext = context; this.mData = mData; } // ... }
-
编写 BaseAdapter 中要求实现的
getCount()
,getItem()
,getItemId()
和getView()
方法.@Override public int getCount() { return mData.size(); // 返回的数量就是 ListView 显示的 Item 数量,返回数据源的长度即可 } @Override public Object getItem(int position) { return mData.get(position); // 返回数据源中指定位置上的数据项 } @Override public long getItemId(int position) { return position; // 返回指定 Item 数据项在适配器中的位置 }
前面三个方法比较简单,最后一个
getView()
方法需要做的就要多一点了。/** * 获取一个对应数据源中指定位置上的数据的视图 View * @param position 当前 Item 使用数据源中对应的数据的位置 * @param convertView 可重复使用的旧视图 * @param parent 新视图绑定到适配器后将被添加到的视图组,这里指的其实就是 ListView * @return 与指定位置的数据对应的 View */ @Override public View getView(int position, View convertView, ViewGroup parent) { // 通过 Context 获取布局加载器 LayoutInflater inflater = LayoutInflater.from(mContext); // 使用布局加载器的 inflate 方法加载 Item 项的布局文件并转化为 View 对象 // 第一个参数为 Item 布局文件的资源 ID // 第二个参数可以给 Item 项指定一个父视图,使用参数中的 parent 即可 // 第三个参数用来确认是否将 View 添加到父视图中,false 代表只让父视图中的 layout 属性生效,但不会将 View 添加到父视图中 // 这里不能设置为 true,因为 Item 视图有父视图那将不能再添加到 ListView 中,该页面也会打不开 View view = inflater.inflate(R.layout.contact_item, parent, false); // 通过 View 对象获取 Item 布局文件中对应的控件 ImageView avatar = (ImageView) view.findViewById(R.id.iv_contact_item_avatar); TextView name = (TextView) view.findViewById(R.id.tv_contact_item_name); // 获取数据源中对应位置上的数据项 ContactEntity entity = mData.get(position); // 将数据设置到对应控件中 avatar.setImageResource(entity.getAvatar()); name.setText(entity.getName()); return view; // 返回设置好数据后的 View }
-
在 Avtivity 中构造自定义的适配器对象,使用
setAdapter()
方法将 ListView 与自定义适配器绑定。// 自定义一个数据源,假装是从后台数据中获取的 List<ContactEntity> data = new ArrayList<>(); ContactEntity entity = null; for (int i = 0; i < 30; i++) { entity = new ContactEntity(); if (i % 2 == 0) { entity.setAvatar(R.mipmap.ic_launcher_round); entity.setName("ic_launcher_round" + i); } else { entity.setAvatar(R.mipmap.ic_launcher); entity.setName("ic_launcher" + i); } data.add(entity); } ContactAdapter adapter = new ContactAdapter(this, data);
-
设置行为响应事件(以子项的点击事件为例)。
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // TODO } });
ListView 优化:
上面就是使用 BaseAdapter 的最简单的方式,但这也是没有进行优化的处理方式,每次调用 getView 时都是通过 inflate 加载 XML 布局文件然后创建一个新的 View 对象,再从该 View 中通过 findViewById 找到对应的控件,这会对资源造成了极大的浪费,同时效率也低。
其实像 ListView、GridView 等数据展示控件都有缓存机制,在前面的 getView()
方法中有一个参数 convertView,它就是系统提供的可复用的 View 缓存对象。另外每次也都要通过 Context 重新获取 LayoutInflater 布局加载器,而这又是 Context 在这里的唯一作用,不如直接让 LayoutInflater 作为属性。那么现在来优化一下这个自定义适配器。
public class ContactAdapter extends BaseAdapter {
private final List<ContactEntity> mData; // 对应数据源 List
private final LayoutInflater mInflater; // 对应 Context 的布局加载器
public ContactAdapter(Context context, List<ContactEntity> mData) {
this.mData = mData;
this.mInflater = LayoutInflater.from(context); // 通过 Context 直接获取布局加载器
}
// ...
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
if (convertView == null) {
// 其实参数中的 parent 指的就是当前的 ListView
// 所以即便不传入 Context ,也能在此方法中通过 parent.getContext() 方法获取对应 Context
// 也就是说可以通过 LayoutInflater.from(parent.getContext()) 获取布局加载器
// LayoutInflater inflater = LayoutInflater.from(parent.getContext());
view = inflater.inflate(R.layout.contact_item, parent, false);
} else {
view = convertView;
}
ImageView avatar = (ImageView) view.findViewById(R.id.iv_contact_item_avatar);
TextView name = (TextView) view.findViewById(R.id.tv_contact_item_name);
ContactEntity entity = mData.get(position);
avatar.setImageResource(entity.getAvatar());
name.setText(entity.getName());
return view;
}
}
这样只要有可复用的 convertView,就不用每次都去创建新的 View 对象了。但每次还是要调用 View 的 findViewById()
方法来获取控件的实例。findViewById()
方法是一个遍历树的过程是很消耗时间的,可以借助 ViewHolder 进一步优化。
public class ContactAdapter extends BaseAdapter {
// ...
/** 定义静态内部类 ViewHolder,用于缓存控件,属性对应 Item 布局文件中的控件 */
static class ViewHolder {
private ImageView avatar;
private TextView name;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = inflater.inflate(R.layout.contact_item, parent, false);
// 创建 ViewHolder 对象
viewHolder = new ViewHolder();
// 将 Item 中的控件存储到 ViewHolder 对应的属性中
viewHolder.avatar = (ImageView) view.findViewById(R.id.iv_contact_item_avatar);
viewHolder.name = (TextView) view.findViewById(R.id.tv_contact_item_name);
// 利用 View 的 setTag() 方法将 ViewHolder 对象保存在当前的 View 对象中
view.setTag(viewHolder);
} else {
view = convertView;
// 从复用的 View 对象中取出 ViewHolder
viewHolder = (ViewHolder) view.getTag();
}
// 获取数据源中对应位置上的数据
ContactEntity entity = data.get(position);
// 将数据设置到 ViewHolder 对应的控件属性中
viewHolder.avatar.setImageResource(entity.getAvatar());
viewHolder.name.setText(entity.getName());
return view; // 返回设置后的 View
}
}
由于 ViewHolder 保存了当前 View 的控件实例,又通过 setTag()
方法将 ViewHolder 保存在当前 View 上,相当于当前 View 自己存储了另外的 View 控件实例,而这些 View 控件又是当前 View 的一部分。那么就不需要再通过 findViewById()
去查找某一个 View 控件了,直接使用自己内部已经存储了的 View 控件实例即可。
为什么给 ViewHolder 加上 static 修饰?
静态内部类主要作用就是,内部类是否需要隔离“外部类的 this 对象(指针)”。内部类是有 this 指针的,可以“直接”访问外部类的成员变量和成员方法(包括私有的成员)。而静态内部类,没有这个 this 指针,所以无法“直接”调用。
ViewHolder 的构造和复用,与是不是静态无关。核心是 ListView(AdapterView),通过 getView(int position, View convertView, ViewGroup parent)
的 convertView 会为开发者传入一个可以复用的对象。开发者需要利用该对象,减少应用内存的消耗。
从减少内存消耗的角度来看,ViewHolder 还是应该修饰成 static 比较好。这样 ViewHolder 中可以减少 Activity 的 this 指针,由于减少了一个 this 指针的引用,也会对 Activity 的引用计数大大减少。Activity 的 this 指针继承于 Android 的 Context 上下文,对于 Context 的回收遗漏,是 Android 内存管理中很大的问题。减少了对 Context 的引用,可以更容易减少 Context 引用计数出现问题。
这个解释来自于《Effective Java》第 22 条 优先考虑静态成员类。其中有条建议:
如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,是它成为静态成员类,而不是非静态成员类。
因为非静态成员类的实例会包含一个额外的指向外围对象的引用,保存这份引用要消耗时间和空间,并且导致外围类实例符合垃圾回收时仍然被保留。如果没有外围实例的情况下,也需要分配实例,就不能使用非静态成员类,因为非静态成员类的实例必须要有一个外围实例。
内存泄漏的本质
长生命周期的对象引用短生命周期的对象,长生命周期总是持有段生命周期的引用造成了对短生命周期对象的不能回收。
Listview 卡顿的优化思路
- 使用 Adapter 提供的 convertView 进行复用 ItemView。
- 使用 ViewHolder 减少 findviewbyid 调用次数。
- Listview 被多层嵌套,多次的 onMessure 导致卡顿,需要减少嵌套的层数。
- 如果多层嵌套无法避免,建议把 Listview 的高和宽设置为 match_parent。
- 使用分页,减少每次 ListView 加载的数据。
- 如果显示图片,可以对图片进行缓存,减少加载的。
- 减少不必要的视图更新。