ListView
1. 初始ListView
ListView:
采用适配器设计模式,让数据和界面分离,更利于拓展和维护。
3个要素:
(1)ListView控件。
(2)适配器类。它是视图和数据直接的桥梁,负责提供对数据的访问,生成每一个列表项第一的View。常见的有ArrayAdapter、SimpleAdapter、BaseAdapter。
(3)数据源。
如图用一个字符串数组来测试:

1.listview是展示大量数据的,需要实现将数据准备好,这些数据可从网上下载,也可从数据库获取。
2.数组中的数据不能直接传递给ListView,需要借助适配器。最简单的就是ArrayAdapter,可通过泛型来指定要适配的数据类型,然后在构造方法中把要适配的数据传入即可。
由于提供的数据是字符串,因此反省指定为String,在ArrayAdapter的构造函数中依次传入当前的上下文、每个条目布局的id、要适配的数据。这里使用了Android内置的布局文件——android.R.layout.simple_list_item_1,这里面只有一个TextView,可用于简单显示一列文本。这样适配器对象就构建好了。
3.调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和适配器之间的关联就建立完成了。
2.定制ListView条目的界面
只能显示一段文本会有些单独,无法满足需求,可对其进行定制丰富内容。
e.g. 通过定制,显示旗帜+国家/地区。
-
字符串已经无法满足要求,可新建实体类Country。
其中有两个字段,name为国家和地区的名字,ImageId表示旗帜对应图片的id。
image.png
-
创建item_country.xml。1个ImageView(iv_flag)+1个TextView(tv_name)。
-
创建自定义适配器,继承自ArrayAdapter,泛型指定为Country类。
新建一个类CountryAdapter:
public class CountryAdapter extends ArrayAdapter<Country>{
int resourceId;
public CountryAdapter(@NonNull Context context, int resource, @NonNull List<Country> objects) {
super(context, resource, objects);
resourceId = resource; //记录布局资源Id
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
//position为当前项的位置,从0开始计数
Country country = getItem(position); //获取当前位置条目的数据实例
//把xml转换成view对象
View view = View.inflate(getContext(),resourceId,null);
ImageView imageView = (ImageView)view.findViewById(R.id.iv_flag);
TextView textView = (TextView)view.findViewById(R.id.tv_name);
//设置数据
imageView.setImageResource(country.getImageId());
textView.setText(country.getName());
return view;
}
}
CountryAdapter 重写了父类的构造函数,用于上下文、ListView子项目布局的id和数据传递过来。另外重写了getView()方法,这个方法在每个子项滚动出屏幕时被调用。
修改 ListViewActivity.xml:
public class ListViewActivity extends AppCompatActivity {
private List<Country> countryList = new ArrayList<>();
ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view);
listView = findViewById(R.id.list_view);
//初始化数据
initDatas();
//给ListView设置适配器,从而显示在界面上
CountryAdapter adapter = new CountryAdapter(this,R.layout.item_country,countryList);
listView.setAdapter(adapter);
}
private void initDatas() {
countryList.add(new Country("中国",R.drawable.flag));
countryList.add(new Country("日本",R.drawable.flag));
countryList.add(new Country("韩国",R.drawable.flag));
countryList.add(new Country("印度",R.drawable.flag));
countryList.add(new Country("俄罗斯",R.drawable.flag));
countryList.add(new Country("新加坡",R.drawable.flag));
countryList.add(new Country("马来西亚",R.drawable.flag));
countryList.add(new Country("越南",R.drawable.flag));
countryList.add(new Country("泰国",R.drawable.flag));
}
}
- 定制结果如图:

总结:
目前,CountryAdapter的getView()方法中每次都会把布局重新加载一遍,当ListView快速滑动时就会出现程序卡顿或崩溃。
3. 优化ListView
仔细观察,getView()方法还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便以后进行重用。修改CountryAdapter中getView()方法的代码,如下:

如图可看出,在getView方法中进行了判断,如果convertView为空,则通过inflater()方法加载布局;如果不为空,则直接对convertView进行重用。这样就大大提高了ListView的运行效率,在快速滚动时也可以表现出更好的性能。
但是还可以继续优化。无论是通过inflate()加载布局,还是复用convertView,都会调用findViewById()方法去初始化控件,这步操作也是可以复用的。
public class CountryAdapter extends ArrayAdapter<Country>{
int rescourId;
public CountryAdapter(@NonNull Context context, int resource, @NonNull List<Country> objects) {
super(context, resource, objects);
rescourId = resource;//记录布局资源Id
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
//position为当前项的位置,从0开始计数
Country country = getItem(position); //获取当前位置条目的数据实例
View view;
ViewHolder viewHolder;
//当convertView不为空,复用convertView
if (convertView == null) {
viewHolder = new ViewHolder(); //创建ViewHolder
//把xml转换成view对象
view = View.inflate(getContext(), rescourId, null);
viewHolder.imageView = view.findViewById(R.id.iv_flag);
viewHolder.textView = view.findViewById(R.id.tv_name);
view.setTag(viewHolder); //将ViewHolder保存到view对象中
}else {
view = convertView;
viewHolder = (ViewHolder)view.getTag();
}
//设置数据
viewHolder.imageView.setImageResource(country.getImageId());
viewHolder.textView.setText(country.getName());
return view;
}
private class ViewHolder {
ImageView imageView;
TextView textView;
}
}
当convertView为空时,创建一个ViewHolder对象,并将控件的实例都存放在ViewHolder中,然后调用View的setTag()方法,将ViewHolder的对象缓存到View中。当convertView不为空时,则调用View的getTag()方法,把ViewHolder重新取出。这样所有控件的实例都缓存在了ViewHolder中,就不会每次都通过findViewById()方法来获取实例了。
4.ListView的点击事件
使用setOnItemClickListener()来为ListView注册一个监听器,当用户点击任意一个条目时,就会回调onItemClick()方法,在这个方法中可以通过position参数判断出用户点击的是哪一个条目。然后获取到相应的国家,Toast显示国家/地区名称。
public class ListViewActivity extends AppCompatActivity {
private List<Country> countryList = new ArrayList<>();
ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view);
listView = findViewById(R.id.list_view);
//初始化数据
initDatas();
//给ListView设置适配器,从而显示在界面上
final CountryAdapter adapter = new CountryAdapter(this,R.layout.item_country,countryList);
listView.setAdapter(adapter);
//设置ListView条目的点击事件
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
/*
* 参数:1.parent:指ListView, 2.view:指被点击条目的View, 3.posiyion:条目的位置
*4.id:如果Adapter继承自ArrayAdapter,id和position一致
* */
Toast.makeText(getApplicationContext(),countryList.get(position).getName(),Toast.LENGTH_SHORT).show();
}
});
//长按事件
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
countryList.remove(position); //删除长按位置的数据
adapter.notifyDataSetChanged(); //刷新ListView界面
return true; //返回true时,处理完长按事件,不会再处理点击事件
//return false; //返回false时,处理完长按事件,还会处理点击事件
}
});
}
private void initDatas() {
countryList.add(new Country("中国",R.drawable.flag));
countryList.add(new Country("日本",R.drawable.flag));
countryList.add(new Country("韩国",R.drawable.flag));
countryList.add(new Country("印度",R.drawable.flag));
countryList.add(new Country("俄罗斯",R.drawable.flag));
countryList.add(new Country("新加坡",R.drawable.flag));
countryList.add(new Country("马来西亚",R.drawable.flag));
countryList.add(new Country("越南",R.drawable.flag));
countryList.add(new Country("泰国",R.drawable.flag));
}
}
其中还设置长按事件,需要注意的是,OnItemLongClick()有一个boolean的返回值,代表是否消费掉这个事件。
- 返回false时,处理完长按事件,还会处理点击事件
- 返回true时,处理完长按事件,不会再处理点击事件

当长按一个条目时,就会把这个条目从界面中移除。因为数据集合和适配器绑定,在集合在删除数据,然后调用适配器notifyDataSetChanged()方法刷新,就是实现了删除ListView条目操作。

5.ListView常用属性
- android:divider 设置分割线的颜色或者指定分割线图片
- android:dividerHeight 设置分割线的高度
- android:scrollbars = "none" 隐藏滚动条
