ListView和RecyclerView复用详解

做一家受人尊敬的企业,做一位受人尊敬的老师-动脑学院

前言

我们每一个Android开发人员对ListView的使用肯定是很熟悉的,然而多少人能真正的懂ListView的缓存机制呢,说白了就是ListView为了提高效率,而内部实现的一种优化,牺牲一点内存。而这种优化就需要复用ItemView(也就是item对应的View).那么下面楼主来对ListView和RecyclerView的item复用问题做一个深入的讲解

首先来解答几个问题

1.ListView为什么会存在Item复用问题

答:ListView内部为了优化而建立的复用机制,在下面方法中第二个参数就是ListView传递给你,让你进行复用的View.如果你不想复用listview传递给你的View,那你每次都需要创建一个新的View进行返回,这样子是肯定不会出现复用问题的,但是性能却是很消耗的。

public View getView(int posion, View itemView, ViewGroup viewGroup)
{
    return null;
}

2.为什么上述的getView方法中第二个参数有时候为null呢

因为ListView默认缓存一页的View,什么叫一页,也就是你当前listview界面上有几个Item可以显示,listview就缓存几个.

当现实第一页的时候,由于没有一个Item被创建,所以第一页的Item的getView方法中的第二个参数都是为null的

假如listview只能最多显示8条记录,则第一页显示的时候listview内部缓存了这8个itemView.当第九条记录出现在视野中的时候,listview就会在调用getView方法的时候在第二个参数处传入之前用过的itemView。

3.为什么需要ViewHolder呢?这个又是干嘛的

为什么需要 上述我们谈到itemView的复用是为了性能,那么ViewHolder同样也是为了提高性能.我们都知道我们要显示列表数据.就要在getView方法中拿到对应下标的数据然后对itemView中的控件进行设值,所以我们需要用到findViewById(int id)方法来找到控件,并且强转成我们想要的类型之后,然后设置数据,而findViewById(int id)方法在列表滚动的时候频繁调用getView方法的时候也是一个比较消耗性能的操作.所以ViewHolder来了

ViewHolder是干嘛的 为了在列表滚动的时候,频繁调用getView方法的时候尽量提高性能.我们可以使用一个普通类,这个类通常就起名字为ViewHolder了,当创建itemView的时候,我们也把里面要用到的控件也找到,然后放在ViewHolder类中,然后再通过itemView.setTag(Object ob)方法实现一个itemView和一个ViewHolder进行绑定.

经过上述的操作,如果在getView方法中传入了复用的itemView,那么我们可以毫不客气地从里面拿出这个itemView对应的ViewHolder,从而避免了去调用多个findViewById(int id)去找到控件并设值.因为之前你把找到的控件都放在了ViewHolder中

扩展 如果你的itemView中只有一个控件需要显示,那么ViewHolder就不需要了,你可以直接把这个控件和itemView进行关联,也就是你需要深刻理解ViewHolder的作用,它是为了把你找到的多个控件和itemView关联。所以当你只有一个控件的时候,这个ViewHolder就不需要啦

itemView.setTag(Object ob)方法直接把这个控件设置上去就可以啦,复用的时候直接拿出来

那么主要的问题解答完了,总得写点代码来让大家更深刻的体会一下.

博主几乎会重现我们开发中的常见问题,来对应的讲解

getView方法在什么时候调用

回答:在每一个item从不可见变为可见的时候

动手实践

实现一个简单的列表,使用ListView控件,并且Item中有复选框

Activity的xml文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="com.xiaojinzi.listdemo.MainActivity">

    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

就是一个列表控件

ListView的Item的xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:padding="4dp"
    android:orientation="horizontal">
    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="30dp"
        android:textSize="18sp"
        android:text="hello" />
    <CheckBox
        android:id="@+id/cb"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="30dp" />
</LinearLayout>

ListView的适配器

public class ListViewAdapter extends BaseAdapter {

    private List<String> listViewData;

    private Context mContext;

    public ListViewAdapter(List<String> listViewData, Context mContext) {
        this.listViewData = listViewData;
        this.mContext = mContext;
    }

    @Override
    public int getCount() {
        return listViewData.size();
    }

    @Override
    public Object getItem(int i) {
        return listViewData.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {

        View item = View.inflate(mContext, R.layout.listview_item, null);

        return item;
    }

}

这代码非常简单,不再啰嗦

Activity代码

public class MainActivity extends AppCompatActivity {

    private ListView lv;

    private BaseAdapter listViewAdapter;

    private List<String> listViewData = new ArrayList<String>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        for (int i = 0; i < 50; i++) {
            listViewData.add("text" + i);
        }

        lv = (ListView) findViewById(R.id.lv);

        listViewAdapter = new ListViewAdapter(listViewData, this);

        lv.setAdapter(listViewAdapter);

    }

}

代码贴完了,都是非常的简单,先看下运行效果

这里很需要你们关注的是我们的适配器中的getView中的代码

public View getView(int i, View view, ViewGroup viewGroup) {
        View item = View.inflate(mContext, R.layout.listview_item, null);
        return item;
}

我们上面说过了方法中第二个参数是ListView会传的itemView,提高效率用的,而这里博主先不用,每次调用getView都会创建一个新的View然后返回

实现一个小目标,嗯:奇数的Item中的复选框要被选中

那么很容易,只需要这样子

    public View getView(int position, View view, ViewGroup viewGroup) {
        View item = View.inflate(mContext, R.layout.listview_item, null);

        //找到文本框
        TextView tv = (TextView) item.findViewById(R.id.tv);
        //设置文本内容
        tv.setText(listViewData.get(position));

        //找到复选框
        CheckBox cb = (CheckBox) item.findViewById(R.id.cb);

        if(position % 2 != 0){ //如果是奇数
            cb.setChecked(true);
        }

        return item;
    }

代码也很简单,就是找到了创建的布局item中的文本控件和复选框,然后设置相应的内容

看效果

我们可以看到,功能实现了,而且没有出现任何问题,比如常见的复用问题,嗯

喂喂喂,我们没复用回传的View,哪里来的复用问题啊,哈哈哈,所以我们的列表是肯定没有任何问题的,因为根本没有复用,性能是最差的一种写法

实现一个小目标,复用Item,嗯

    public View getView(int position, View view, ViewGroup viewGroup) {

        View item = null;

        if (view == null) {
            item = View.inflate(mContext, R.layout.listview_item, null);
        }else{
            item = view;
        }

        //找到文本框
        TextView tv = (TextView) item.findViewById(R.id.tv);
        //设置文本内容
        tv.setText(listViewData.get(position));

        //找到复选框
        CheckBox cb = (CheckBox) item.findViewById(R.id.cb);

        if(position % 2 == 0){ //如果是奇数
            cb.setChecked(true);
        }

        return item;
    }

这段代码改动的地方就是方法最开始,判断了一下回传给我的view是不是为null,为null的情况博文最开始已经讲过了

如果为null就创建一个新的,如果不是就直接赋值给item,达到条目的复用!

那我们看看效果呗!

请大声的告诉我,发生了什么?复用问题

没错,复用问题出现了,博主给大家重现了错误

那么这里是怎么引起的呢?

只有知道其中的原理,你解决问题才能快准狠!

首先我先帮大家统计一下创建Item的次数

可以看到,我用一个变量记录创建的次数,我重新运行

从App运行到滑动来滑动去,我们可以看见,最开始创建了16次,然后随着滑动多来了一次,你可以使用截图定格一下动图,你会发现这个列表最多显示17条记录(当然了你的界面是多少个和我这个界面是不同的,反正就是界面能显示的Item最多个数),所以证明了上面的一个观点,ListView默认缓存一个界面的Item个数

原理

所以当我们复用ListView回传的View的时候,这个View是被之前使用过的,也就是说给你的这个View保存了之前用过的状态

这里的情况就是给你的view刚好是之前复选框被选中的那个View,所以就造成复用啦

解决方法

对产生问题的控件进行初始化,初始化时什么意思呢?

意思就是说,把出问题的控件,状态还原一下

看代码!

别看了,就是框框里面的一句话,是不是感觉很简单呀,如果你知晓原理,为什么这样子就没有了复用的问题呢?

因为如果给你的View里面的复选框是被选中的,这里你对他还原了呀,所以就ok啦

使用ViewHolder

上面我们也说了ViewHolder的作用和使用的必要性,那么博主直接来用一下吧

由于getView内部稍微改动有点大,我贴上Adapter中的代码

public class ListViewAdapter extends BaseAdapter {

    private List<String> listViewData;

    private Context mContext;

    public ListViewAdapter(List<String> listViewData, Context mContext) {
        this.listViewData = listViewData;
        this.mContext = mContext;
    }

    @Override
    public int getCount() {
        return listViewData.size();
    }

    @Override
    public Object getItem(int i) {
        return listViewData.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int position, View view, ViewGroup viewGroup) {

        //Item对应的试图
        View item = null;

        ViewHolder vh = null;

        if (view == null) {
            item = View.inflate(mContext, R.layout.listview_item, null);
            vh = new ViewHolder();
            //找到文本框
            vh.tv = (TextView) item.findViewById(R.id.tv);
            //找到复选框
            vh.cb = (CheckBox) item.findViewById(R.id.cb);
            //让item和ViewHolder绑定在一起
            item.setTag(vh);
        } else {
            //复用ListView给的View
            item = view;
            //拿出ViewHolder
            vh = (ViewHolder) item.getTag();
        }

        //设置文本内容
        vh.tv.setText(listViewData.get(position));

        //还原状态
        vh.cb.setChecked(false);

        if (position % 2 == 0) { //如果是奇数
            vh.cb.setChecked(true);
        }

        return item;
    }

    /**
     * 用于存放一个ItemView中的控件,由于这里只有两个控件,那么声明两个控件即可
     */
    class ViewHolder {
        TextView tv;
        CheckBox cb;
    }

}

1.如果复用的View为null,我们需要创建一个新的item,同时也创建了一个ViewHolder,然后把条目视图中的控件通过findViewById方法寻找到

ViewHolder中,然后我们说了需要和条目视图进行绑定,所以调用了setTag方法

2.而另一边,如果复用的View不是为null,那么直接拿过来用,并且从里面拿出ViewHolder,因为每一个复用的ViewHolder肯定是经过1处创建并且返回的

到这里为止,一个完成的列表的展示和优化已经完成啦,并且中间讲述了复用问题是如何产生的,如何解决!

未完待续,下面会讲述,

ListView多布局展示是个什么原理,

需要注意什么

demo下载

上述的代码我放在这里,传送门:

demo下载

时间: 2024-10-27 12:55:15

ListView和RecyclerView复用详解的相关文章

RecyclerView使用详解(三)

在上一篇(RecyclerView使用详解(二))文章中介绍了RecyclerView的多Item布局实现,接下来要来讲讲RecyclerView的Cursor实现,相较于之前的实现,Cursor有更多的使用场景,也更加的常用,特别是配合LoaderManager和CursorLoader进行数据的缓存及加载显示,基于此我们来重点看看RecyclerView的CursorAdapter具体要怎么实现. 一.CursorAdapter实现(配合LoaderManager和CursorLoader)

RecyclerView使用详解(二)

在上一篇(RecyclerView使用详解(一))文章中简单的介绍了RecyclerView的基本用法,接下来要来讲讲RecyclerView的更多用法,要实现不同的功能效果,大部分都还是在于RecyclerView的Adapter写法,所以我们着重来看看几种不同功能的Adapter写法. 一.多Item布局实现(MultipleItem) 如果之前你用过ListView实现过此功能,那么你一定对下面这两个方法并不陌生 @Override public int getItemViewType(i

ListView 和 GridView应用详解-----本文转自博客园

1. 选择 ListView 或 GridView ListView 和 GridView 控件均用于显示应用中数据的集合.它们的功能十分相似,但是显示数据的方式不同.它们都派生自 ItemsControl 类. ListView 采用垂直堆叠的方式显示数据.该控件常用于显示按顺序排列的项目列表,如电子邮件列表或搜索结果列表.它在主从式列表情况下也很有用,其中的列表项仅包含少量信息,并且选定项目的详细信息会单独显示. GridView 采用水平堆叠的方式显示数据.对于占驻较多控件的每个项目(如照

RecyclerView使用详解(一)

一.前言 RecyclerView是谷歌V7包下新增的控件,用来替代ListView的使用,在RecyclerView标准化了ViewHolder类似于ListView中convertView用来做视图缓. 先来说说RecyclerView的有点就是,他可以通过设置LayoutManager来快速实现listview.gridview.瀑布流的效果,而且还可以设置横向和纵向显示,添加动画效果也非常简单(自带了ItemAnimation,可以设置加载和移除时的动画,方便做出各种动态浏览的效果),也

Android 中RecyclerView使用详解(一)

概述 针对RecyclerView,谷歌有一段介绍的话: RecyclerView is a more advanced and flexible version of ListView. This widget is a Container for large sets of views that can be recycled and scrolled very efficiently. Use the RecyclerView widget when you have lists with

Android 高级UI设计笔记20:RecyclerView 的详解之RecyclerView添加Item点击事件

1. 引言: RecyclerView侧重的是布局的灵活性,虽说可以替代ListView但是连基本的点击事件都没有,这篇文章就来详细讲解如何为RecyclerView的item添加点击事件,顺便复习一下观察者模式. 2. 最终目的 模拟ListView的setOnItemClickListener()方法,调用者只须调用类似于setOnItemClickListener的东西就能获得被点击item的相关数据.   3. 原理 为RecyclerView的每个子item设置setOnClickLi

RecyclerView使用详解

RecyclerView是Android 5.x版本中新添加的一个全新控件,他比ListView,GridView更加的灵活,我们能够使用RecyclerView就完成ListView,GridView所做的工作,同时使用RecyclerView也能非常方便的实现瀑布流的效果. 一.竖屏ListView,横屏GridView效果 MainActivity代码: public class MainActivity extends Activity implements MyRecyclerView

ListView 一些重要属性详解

首先是stackFromBottom属性,这只该属性之后你做好的列表就会显示你列表的最下面,值为true和false android:stackFromBottom="true" 第二是transciptMode属性,需要用ListView或者其它显示大量Items的控件实时跟踪或者查看信息,并且希望最新的条目可以自动滚动到可视范围内.通过设置的控件transcriptMode属性可以将Android平台的控件(支持ScrollBar)自动滑动到最底部.android:transcri

Android listview addHeaderView 和 addFooterView 详解

addHeaderView()方法:主要是向listView的头部添加布局 addFooterView()方法:主要是向listView的底部添加布局 需要注意的是添加布局的时候应该添加从父容器开始添加,而不能直接添加父容器中的子控件.例如:从一个xml布局文件中添加一个button控件, 只能将整个布局xml文件添加进去.而不能单单只添加button控件. 当添加头部和底部布局还有另外一个重载方法就是addHeaderView(headView, null, false) 和addFooter