ListView和Adapter

438 阅读7分钟

1 ListView

ListViewAndroid中显示数据常用的控件之一,主要用于显示一个垂直滚动的数据集合,随着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取出数据,从而加载显示

image.png

1.2 缓存原理

试想一个场景:若把所有数据集合的信息都加载到ListView上显示,若 ListView要为每个数据都创建一个视图,那么会占用非常多的内存

  • 为了节省空间和时间,ListView不会为每一个数据创建一个视图,而是采用了Recycler组件,用于回收 & 复用 View

  • 当屏幕需显示nItem时,那么ListView会创建 n+1个视图;当第1个Item离开屏幕时,此ItemView被回收至缓存,入屏的ItemView会优先从该缓存中获取

  • 只有Item完全离开屏幕后才可复用,这也是为什么ListView要创建比屏幕需显示视图多1个的原因:缓冲显示视图。即:第1个Item离开屏幕是有过程的,会有第1个Item的下半部分 & 第n+1个Item上半部分同时在屏幕中显示的状态,此时仍无法使用缓存的View,只能继续用新创建的视图View

  • 实例演示。假设:屏幕只能显示5个Item,那么ListView只会创建(5+1)个Item的视图;当第1个Item完全离开屏幕后才会回收至缓存从而复用(用于显示第7个Item

image.png

2 具体使用

  1. 引入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>
  1. 根据实际需求定制列表项:实现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>
  1. 定义一个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);
}
  1. 构造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);
  1. LsitView绑定到SimpleAdapter
listView.setAdapter(mSimpleAdapter);

结果显示
device-2022-08-28-182239

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给我们提供好的适配器,直接使用即可,不过模式都已经写死了

    1. ArrayAdapter:简单、易用的Adapter,用于将数组绑定为列表项的数据源,支持泛型操作
    2. SimpleAdapter:功能强大的Adapter,可以将数据源的数据绑定到item中的view中
    3. SimpleCursorAdapter:与SimpleAdapter类似,用于绑定游标(直接从数据数取出数据)作为列表项的数据源,不常用
    4. BaseAdapter:常用,我们需要继承BaseAdapter来自定义我们自己的适配器

3.1 ArrayAdapter

使用简单、用于将数组、List 形式的数据绑定到列表中作为数据源,支持泛型操作

步骤:

  1. 在xml文件布局上实现ListView
  2. 在Activity中定义数据源(列表或者数组)
List<String> listData = new ArrayList<>(); 
for(int i=0;i<20;i++){ 
    listData.add("item数据"+i);
}
  1. 构造ArrayAdapter对象,设置适配器
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(this, android.R.layaout.simple_list_item_1, listData);

第一参数都是Context
第二个参数就是要添加的item的布局id 
第三个参数是数据,数据可以使用数组也可以使用List
备注:如果 List 里面存放的是一个普通对象而不是String的话,则显示在item中的数据为这个对象调用toString后的结果
  1. 将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

使用步骤:

  1. 定义主xml布局
  2. 根据需要定义ListView每行所实现的xml布局(这里直接使用SimpleAdapter中的item布局)
  3. 创建数据源
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);
    }
}
  1. 定义一个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;
    }
}
  1. 构造Adapter对象,设置适配器
mMyAdapter = new MyAdapter(this, mItemDataList);
  1. 将LsitView绑定到Adapter上
mListView.setAdapter(mMyAdapter);

3.3.1 getView()方法的几种实现方式

实现方式1:直接返回索引对应的数据的视图
这是最直接的一种方式,目标很明确,就是返回对应的视图。同样缺点也很明确,没有利用ListViewitem的复用机制,假如有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. 形成了AdapteritemView重用机制,减少了重绘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的优化总结: image.png

4 进阶使用:添加头部 & 尾部View

在日常使用中,我们常常会需要在ListView头部/尾部添加视图

  1. 添加头部/尾部视图。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>
  1. 添加到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);

笔记源码:gitee.com/maojiu0825/…