Skip to content

(未发)ListView的简介

ListView是一种原始的滚动列表,用来显示可垂直滚动的视图集合。为何说是原始的呢,因为ListView是API 1中就有的,是一种元老级控件,而在androidx包下,同样用来显示滚动列表集合,RecycelerView是一种更好的、更推荐的控件,它的功能比ListView强大很多。 在开始我们先来学一学ListView的工作原理。

ListView的工作原理

ListView 仅是作为容器(列表),用于装载显示数据,每一个列表的条目称为子项(item)。item 中的具体数据是由适配器(adapter)来提供的。 适配器(adapter):作为 View (不仅仅指的 ListView)和数据之间的桥梁或者中介,将数据映射到要展示的 View 中。这就是最简单适配器模式,也是适配器的主要作用! 当需要显示数据的时候,ListView 会从适配器(Adapter)中取出数据,然后来加载数据。

ListView的缓存机制

前面讲到ListView是用来展示数据,假如有100个子项,那么就需要加载100个子项布局,而我们知道子项布局都是相似的,仅仅是填充的数据不同而已。所以频繁的加载相似的布局不仅消耗资源而且没有必要。 为了解决这个问题,ListView采用了缓存复用机制。 我们的屏幕只有那么点大,屏幕一次最多可展示的子项也是确定的,所以第一次启动ListView会加载一屏的子项并缓存,(实际上我们最终缓存的子项布局的数量会比首次加载时多一个,这个稍后探讨)当第1个 item 离开屏幕的时候,此时这个 item 的 View 就会被回收,再入屏的 item 的 View 就会优先从该缓存中获取。

ListView的基本使用

使用ListView控件,直接通过ListView标签就引入了一个滚动列表,在design面板下也可以看到其实ListView可以在legacy选项下可以找到ListView。 image.pngimage.png ListView中的子项的数据需要通过适配器来填充,这里介绍两种常用的适配器ArrayAdapter和BaseAdapter

  • ArrayAdapter:使用简单、用于将数组、List 形式的数据绑定到列表中作为数据源,支持泛型操作
  • BaseAdapter:在实际开发中经常用到的,是ArrayAdapter的父类,使用更为灵活

ArrayAdapter适配器的使用

首先创建好数据实体类Fruit(水果类)用来定义要在ListView上显示的数据对象。其次要定义好ListView中的子项布局,因为过于简单,不再展开。

  1. 创建数据实体类Fruit(水果类)
  2. 定义子项布局文件(item_listview)
  3. 自定义Adapter继承自ArrayAdapter并重写其中的getView方法
  4. 绑定ListView和Adapter

1.自定义Adapter继承自ArrayAdapter

ArrayAdapter有多个构造方法,这里我们用包含三个参数的这个构造方法,每个参数的意义如下:

  • @param context 当前上下文。
  • @param resource 包含文本视图的布局文件的资源 ID
  • @param objects 要在列表视图中表示的对象

ArrayAdapter | Android Developers 其次是重写getView方法,这个方法在每一个子项显示在屏幕上的时候都会被调用。首先我们通过position索引从传入的FruitList集合中取出一个Fruit对象,然后通过LayoutInflater拿到子项的布局文件。再一一的为布局中的控件设置内容,最终返回值是这个滚动布局的子项View。

java
public class MyArrayAdapter extends ArrayAdapter<Fruit> {

    private final static String TAG="MyArrayAdapter";
    private int resourceId;
    private int tagId=0;
    /**
     *
     * @param context  当前上下文。
     * @param resource 包含文本视图的布局文件的资源 ID
     * @param objects  要在列表视图中表示的对象
     */
    public MyArrayAdapter(@NonNull Context context, int resource, @NonNull List<Fruit> objects) {
        super(context, resource, objects);
        resourceId=resource;
    }
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        Fruit fruit=getItem(position);
        View view= LayoutInflater.from(getContext()).inflate(resourceId,null);
        TextView textName=view.findViewById(R.id.fruit_name);
        TextView textPrice=view.findViewById(R.id.item_tag);
        textName.setText(fruit.getName());
        textPrice.setText(++tagId+"");
        Log.d(TAG, "getView: position="+position+"  缓存第"+tagId+"个子项");
        return view;
    }
}

2.绑定ListView和Adapter

第二步就是将适配器和ListView绑定。这样一个简单的滚动列表就实现了。

java
public class ListActivity extends AppCompatActivity {


    ListView listView;
    MyArrayAdapter adapter;
    List<Fruit> fruitList=new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list);
        listView=findViewById(R.id.listview);
        for(int i=1;i<=20;++i){
            Fruit fruit=new Fruit(i+"号水果");
            fruitList.add(fruit);
        }
        adapter=new MyArrayAdapter(ListActivity.this,R.layout.item_listview,fruitList);
        listView.setAdapter(adapter);
    }
}

image.png

3.convertView优化ListView

刚刚我们在重写getView方法的时候有提到每一次滚动布局的子项出现在屏幕上的时候它都会调用一次,那么这就存在一个问题,当一个子项的布局较为复杂时时,每一次我们的布局都要重新调用getView,用户如果频繁的上下滚动,那么可能会出现卡顿或者布局显示不全的现象。因此我们需要对加载好的子项布局进行缓存

INFO

动画解读:滚动列表滑动到了最底部后,所有子项布局都以加载过,但是往上回滚发现那些加载过只是没出现在屏幕中的子项又被重新加载,短短的20条子项因为频繁的上下滑动加载了50多次,显然不合理。由此也证实getView在子项显示在屏幕上时都会被调用

1.gif 那么如何缓存之前加载好的布局呢?缓存多少个布局合适呢?缓存一个,然后后面的子项都复用这一个可以吗? 我们再回看getView方法,它有一个参数是convertView,这就是一个缓存View。在刚启动加载这个ListView时,毋庸置疑convertView肯定为空,这时我们只能去加载子项布局,缓存一屏的子项布局。而后再往下滑动加载出来的就是复用的布局了。 如下我构造了一个有20个子项的滚动布局,一开始显示了7个子项布局(第7个出来了一点点),但是最终一共缓存了8个子项,之后往上滑动底部出现的就是复用的子项布局,并没有加载新的。有两点值得注意: 为什么是8个,是个定值吗?为什么不能缓存1个其他都复用?

WARNING

首先需要知道只有 item 完全离开屏幕后才会复用,这也是为什么 ListView 要创建比屏幕需要显示视图多 1 个的原因:缓冲显示视图。 第 1 个 子项 离开屏幕是有一个过程的,会有 1 个 第一个 item 的下半部分 & 第 X+1 个 item 的上半部分同时在屏幕中显示的状态 这种情况是没法使用缓存的 View 的。只能继续用新创建的视图 View。 8个并不是定值,而是我们的屏幕最多只能显示8个,如图第一个只见其尾,第八个只见其头,恰好8个。如果你的子项占的空间小,包括为展示完全的最多能够展示20个子项,那么就会缓存20个convertView。

WARNING

恰好缓存一屏的convertView是为了用户在快速滑动时,可以尽可能的满足新显示的子项布局复用。如果只有缓存一个convertView,就不会有子项被回收,自然也不可能实现复用。 关于ListView中convertView缓存个数的探讨:关于ListView中convertView的缓存个数的探究 - SilentKnight - 博客园

image.png

java
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        ···
        return view;
    }

INFO

动画解读:一开始屏幕展示了7个子项(第7个露出来了一点点),但是上面我们分析知道此场景下一屏最多显示8个,所以最终缓存了8个**convertView。**之后显示出来的子项布局均为复用的,而非重新加载。

  • 缓慢滚动时复用是按顺序的从0-8复用
  • 快速滚动时复用是无序的,从0-8中呈现随机性复用
  • 总而言之其实复用的顺序是随机的。

1.gif

java
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        Fruit fruit=getItem(position);
        if(convertView==null){
            convertView= LayoutInflater.from(getContext()).inflate(resourceId,null);
            TextView textName=convertView.findViewById(R.id.fruit_name);
            TextView textTag=convertView.findViewById(R.id.item_tag);
            textName.setText(fruit.getName());
            textTag.setText(++tagId+"");
            convertView.setTag(tagId);
            Log.d(TAG, "getView: position="+position+"  缓存第"+tagId+"个子项");
        }else{
            TextView textName=convertView.findViewById(R.id.fruit_name);
            TextView textTag=convertView.findViewById(R.id.item_tag);
            textName.setText(fruit.getName());
            textTag.setText("tagId="+convertView.getTag()+"");
            Log.d(TAG, "getView: position="+position+"  复用第"+convertView.getTag()+"个子项");
        }
        return convertView;
    }

4.结合ViewHolder使用

通过上面的分析我们知晓了可以通过convertView来缓存布局,优化了ListView的加载速度。但我们并不能便捷的取到缓存的布局中的控件,需要再次通过findViewById获取控件并绑定数据。因此convertView一般都与ViewHolder搭配使用。ViewHolder并不是一个很深奥的东西,在这里它不过就是一个类,定义了子项布局中的控件。 然后我们将holder对象作为convertView的Tag保存起来,再需要复用之时我们通过getTag就可以取出holder对象,借助holder操作布局上的控件,绑定数据。其实啊,这样分析不难发现**ViewHolder中持有的组件和converView中的组件指向同一个对象,**借助ViewHolder替换掉findViewById。

java

public class MyArrayAdapter extends ArrayAdapter<Fruit> {

    private final static String TAG="MyArrayAdapter";
    private int resourceId;
    private int tagId=0;
    /**
     *
     * @param context  当前上下文。
     * @param resource 包含文本视图的布局文件的资源 ID
     * @param objects  要在列表视图中表示的对象
     */
    public MyArrayAdapter(@NonNull Context context, int resource, @NonNull List<Fruit> objects) {
        super(context, resource, objects);
        resourceId=resource;
    }
    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        ViewHolder holder;
        Fruit fruit=getItem(position);
        if(convertView==null){
            tagId++;
            holder=new ViewHolder();
            convertView= LayoutInflater.from(getContext()).inflate(resourceId,null);
            holder.textName=convertView.findViewById(R.id.fruit_name);
            holder.textTag=convertView.findViewById(R.id.item_tag);
            holder.tagId=tagId;
            convertView.setTag(holder);
            Log.d(TAG, "getView: position="+position+"  缓存第"+tagId+"个子项");

        }else{
            holder= (ViewHolder) convertView.getTag();
            Log.d(TAG, "getView: position"+position+"  复用第"+holder.tagId+"个子项");
        }
        holder.textName.setText(fruit.getName());
        holder.textTag.setText("itemTag="+holder.tagId.toString());
        return convertView;
    }
    static class ViewHolder{
        public TextView textName;
        public TextView textTag;
        public Integer tagId;
    }
}

1.gif

BaseAdapter适配器的使用

这里的数据类和子项布局仍然是使用之前的。

  1. 创建数据实体类Fruit(水果类)
  2. 定义子项布局文件(item_listview)
  3. 自定义Adapter继承自BaseAdapter并重写其中的四个方法
  4. 绑定ListView和Adapter

可见使用BaseAdapter的差异点就是在重写四个方法上,正是因为这一点BaseAdapter才能够灵活的操作子项。 在使用之前先介绍一下这四个方法:

  1. 通过调用 getCount() 获取 ListView 的长度(item 的个数)
  2. 通过调用getView() ,根据 ListView 的长度逐一绘制 ListView 的每一行(在Arrayadapter中就是重写了这个方法)
  3. 获取数据时,通过 getItem返回的是当前Item的数据对象。
  4. 获取数据时,通过 getItemId返回的是当前Item的下标
java


public class MyBaseAdapter extends BaseAdapter{
    private final static String TAG="MyBaseAdapter";

    //获取 ListView 的长度
    @Override
    public int getCount() {
        return 0;
    }
    //返回的是当前Item的数据对象。
    @Override
    public Object getItem(int position) {
        return null;
    }
    //返回的是当前Item的下标
    @Override
    public long getItemId(int position) {
        return 0;
    }
    //根据 ListView 的长度逐一绘制 ListView 的每一行
    //返回的是每一个item视图
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return null;
    }
}

其中最为重要的方法仍然是getView,使用策略和ArrayAdapter几乎没有差异,这里同样使用convertView+ViewHolder优化实现getView,最终的效果和使用ArrayAdapter一样。

java
public class MyBaseAdapter extends BaseAdapter{
    private final static String TAG="MyBaseAdapter";
    private LayoutInflater layoutInflater;
    private List<Fruit> fruitList;
    private Integer resourceId;
    private int tagId=0;

    public MyBaseAdapter(Context context, List<Fruit> fruitList,int resource) {
        this.layoutInflater = LayoutInflater.from(context);
        this.fruitList = fruitList;
        this.resourceId = resource;
    }

    //获取 ListView 的长度
    @Override
    public int getCount() {
        return fruitList.size();
    }

    //返回的是当前Item的数据对象。
    @Override
    public Object getItem(int position) {
        return fruitList.get(position);
    }

    //返回的是当前Item的下标
    @Override
    public long getItemId(int position) {
        return position;
    }

    //根据 ListView 的长度逐一绘制 ListView 的每一行
    //返回的是每一个item视图
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            tagId++;
            holder = new ViewHolder();
            convertView = layoutInflater.inflate(resourceId, null,false);
            holder.textName = convertView.findViewById(R.id.fruit_name);
            holder.textTag = convertView.findViewById(R.id.item_tag);
            holder.tagId = tagId;
            convertView.setTag(holder);
            Log.d(TAG, "getView: position=" + position + "  缓存第" + tagId + "个子项");

        } else {
            holder = (ViewHolder) convertView.getTag();
            Log.d(TAG, "getView: position" + position + "  复用第" + holder.tagId + "个子项");
        }
        Fruit fruit=fruitList.get(position);
        holder.textName.setText(fruit.getName());
        holder.textTag.setText("itemTag=" + holder.tagId.toString());
        return convertView;
    }

    static class ViewHolder{
        public TextView textName;
        public TextView textTag;
        public Integer tagId;
    }
}