sq_Hayden's blog

ListView控件复用详解

2018/05/17 Share

ListView控件复用详解

ListView是什么?

  ListView,即列表视图,是用来以垂直的方式在项目中显示相关数据项列表的控件。其主要分为了两个方面,一是子项Item的布局,还有一个就是数据填充的Adapter。其主要的应用领域就是使具有相同特征的多条Item以垂直列表的形式展现给用户,类似下图的样子:

ListView

ListView的控件复用

  在我们对ListView设置Adapter后,需要去重写自定义Adapter的各个方法,其中getView()便是一个最常用要被复写的方法。按照一般的方式,我们需要在getView()方法中加载子项Item的布局,并利用构造器传进来的数据List进行逐个的赋值操作,这其中,就涉及到了控件的复用问题。
  子项listview_item.xml的布局如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?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:orientation="horizontal"
android:layout_margin="8dp"
>

<ImageView
android:id="@+id/girl_img"
android:layout_width="76dp"
android:layout_height="128dp"
android:layout_margin="10dp"
android:src="@drawable/a01"
/>

<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginLeft="10dp"
android:layout_gravity="center"
>
<TextView
android:id="@+id/girl_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/titleColor"
android:textSize="30sp"
android:text="美女1"
android:layout_margin="10dp"
android:layout_gravity="center"
/>
<TextView
android:id="@+id/girl_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="这是第一个美女"
android:layout_margin="10dp"
android:textColor="@color/contentColor"
/>
</LinearLayout>
<Button
android:id="@+id/click_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/girl_btn_style"
android:text="加为好友"
android:layout_gravity="bottom"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_marginRight="5dp"
android:layout_marginBottom="5dp"
/>
</LinearLayout>

  展示的效果如下:

activity


  以下这段代码,将会是我们最初接触到ListView时,理所当然想到的一种解决方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override  
public View getView(int position, View convertView, ViewGroup parent) {
//获取视图
View view = LayoutInflater.from(mContext).inflate(R.layout.listview_items, null);
//获取相关控件
ImageView imageView = view.findViewById(R.id.girl_img);
Text title = view.findViewById(R.id.girl_title);
Text xcontent = view.findViewById(R.id.girl_content);
//加载数据
//图片
imageView.setImageResource(list.get(position).getId())
//标题
title.setText(list.get(position).getTittle());
//内容
content.setText(list.get(position).getContent());
return view;
}

  以上代码,我们并没有对视图的加载进行任何的优化处理,那么这样做会引发什么样的问题呢?从上述代码我们可以看出,每次去加载Listview中的一个子项时,都会去加载一遍视图,然后都会去查找各个控件,再对每个条目进行赋值,从而创建了大量的对象,极大耗费了内存。当我们的数据项达到千条时,滑动浏览将会由于系统加载的极大压力而导致程序崩溃,那么该如何去优化呢?
  大家可以看到getView()方法中为我们提供了一个视图组件ConvertView,初始时ListView会从Adapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来,放在converView中。当我们向上滑动使得第一个视图被划出屏幕而下面一项显示出来的时候,下边的子项会去复用第一个的视图组件,从而提升了效率。
 

activity


  那么代码可能就像下边的样子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override  
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
if(convertView==null){
//获取视图
view = LayoutInflater.from(mContext).inflate(R.layout.listview_items, null);
}
else {
//复用视图
view = converView;
}
//获取相关控件
ImageView imageView = view.findViewById(R.id.girl_img);
Text title = view.findViewById(R.id.girl_title);
Text xcontent = view.findViewById(R.id.girl_content);
//加载数据
//图片
imageView.setImageResource(list.get(position).getId())
//标题
title.setText(list.get(position).getTittle());
//内容
content.setText(list.get(position).getContent());
return view;
}

  优化到这里,可能会觉得这下应该没有什么问题了,但是其实我们还要对这种方式更进一步的优化,不难发现,在这种模式下,虽然我们对子项Item的视图进行了复用,但是每次却还是需要去查找对应控件后再赋值,十分的麻烦,降低了效率。要是我们在首次缓存视图的同时,就将对应的各个组件同这个视图绑定起来供以后复用的话,将会大大提高效率。那就不得不提到ViewHolder这个自定义类了。
  Viewholder只是一个用户自定义类,它的作用就是将首次加载出来的ConvertView中的需要用到的控件封装成一个Bean对象,也就是所谓的打包起来,然后让对应的View给它打个标签,告诉它你以后跟着我走。这样,当我们在复用View的同时,也就省略了复用控件的查找操作。
  那么最终我们优化后的代码,也就是所谓ConvertView+ViewHolder模式代码,将如下所示:

@Override  
public View getView(int position, View convertView, ViewGroup parent) {
    View view = null;
    Holder holder = null;
    if(convertView==null){
       //创建对象
       holder = new Holder();
       //获取视图
       view = LayoutInflater.from(mContext).inflate(R.layout.listview_items, null);
       //给Holder绑定控件
       holder.imageView = view.findViewById(R.id.girl_img);  
       holder.title = view.findViewById(R.id.girl_title);
       holder.content = view.findViewById(R.id.girl_content);
       //将holder对象绑定给view
       convertView.setTag(holder);
    }else{
       //直接拿出绑定的Holder去用
       holder = (Holder)convertView.getTag();
    }
    //加载数据
    //图片
    holder.imageView.setImageResource(list.get(position).getId())
    //标题
    holder.title.setText(list.get(position).getTittle());
    //内容
    holder.content.setText(list.get(position).getContent());
    return view;
} 
 class Holder {
    ImageView imageView;
    TextView title;
    TextView content;
    Button click;
}

  至此,listView的优化工作就完成了,不过其中还有几个问题需要探究。   

ListView难点探究

1.ListView的加载问题

  经过前面的探究,我们现在已经可以利用ListView的复用,去完成控件的加载工作了,但是回过头来看看我们当时在主布局里是如何去定义ListView这个控件的。

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

  不难发现,在布局里我们给ListView设置了宽高均为填充父窗体,也就是所谓的屏幕的宽高,在这种模式下,我们打印Log来观察ListView是如何进行加载的。
 

listviewLog


  可以看到,在我们的屏幕中预先加载出了七个条目,往下滑动一条后,第八条复用了第一条,是没有任何问题的。那么,在将xml文件进行变更,将listView的高度设置为wrapContent即包裹内容的时候,又会是什么样的状况呢?
 
listviewLog


  从上边的log大概可以看出,listView的复用情况完全变了,好像是先统一指向了第一个被创建的地址,然后再次去创建屏幕上需要显示的子项。对于这一块的复用情况我暂时还没有完全弄清楚,希望有看到的这一块比较懂得,可以在下方评论处谈谈高见。
  至少从这一块,我们可以分析一些问题:当我们将其高度设置为全屏大小时,由于系统已经得知将要加载的ListVIew控件的高度,因此可以很好地进行有几条就显示几条的加载,但是在用包裹内容的方式去加载时,由于系统不清楚预先需要多大的空间去加载这个ListView,因此为了保险情起见它就会多去加载好几个控件的长度,这可能也是导致此问题产生的关键原因,至于细节,我也不是很清楚了。。

2.ListView的控件复用中的控件混乱问题

  当我们实现了ListView的控件复用以后,如果此时子Item中含有Button按钮,对Button按钮设置监听器,去获取当前Item的位置,会发现由于控件会复用已存在的Convertview,从而使按钮获取的位置并不是子项的真正位置。
  如下所示:
 

ListViewGif


  那么,如何解决这个问题呢?其实在我们下拉ListView的时候可以发现,getView()方法回调回来的参数position,它的值是随着Item项的下拉加载而顺序递增的,那么我们就有了解决的办法。即在每次ListView进入一次getView()方法的时候,我们在最后对Viewholder的Button对象设置一个唯一的标志位,用来记录此时这个Button对象的实际位置,然后在点击事件中去获取这个标志值,拿到的就是此时的位置。
  代码如下:

@Override  
public View getView(int position, View convertView, ViewGroup parent) {
    View view = null;
    Holder holder = null;
    if(convertView==null){
       //创建对象
       holder = new Holder();
       //获取视图
       view = LayoutInflater.from(mContext).inflate(R.layout.listview_items, null);
       //给Holder绑定控件
       //获取按钮
       holder.click = convertView.findViewById(R.id.click_btn);
       holder.click.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //获取不同的btn对象位置
                int pos = (Integer) holder.click.getTag();
            }
        });
        convertView.setTag(holder);
    }else{
       //直接拿出绑定的Holder去用
       holder = (Holder)convertView.getTag();
    }
    //加载数据相关

    //存储不同的btn对象位置
    holder.click.setTag(position);
    return view;
} 
 class Holder {
    Button click;
}

  到了这一步,我们还有最后的一个问题需要去解决。
  此时,我们已经实现让每个按钮都拥有自己的位置。假设现在需要如下逻辑:当我们在点击按钮后,需要去改变对应按钮上的文字,同时改变这个按钮的监听事件,当然这个想必大家都会处理,但由此却引发出了问题:
  由于控件的复用性,当我们在改变了首页某按钮的文字及功能之后,会使之后复用的组件上,按钮的文字以及对应的监听事件也被改变,从而造成混乱,怎么去解决这个问题呢?
  要解决这个问题,我们首先得明白listView究竟复用的是什么?listView它并没有去复用某个控件上的数据内容,只是对其子Item视图以及视图内相关的控件进行复用,数据块还是通过Adapter的构造传递过来的数据进行引用的。明白了这一点,我们就很容易去解决这个问题了。
  主要思路:
  用一个Map<Integer,String>去当做子Item视图中Button按钮的数据源。这个Map的Key代表了监听器的状态,String代表了Button按钮的文本,那么刚开始每个的赋值都将是map.put(0,"加为好友");当我们点击按钮时,只需要将此按钮对应位置List数据源中,此Button对应的Map进行赋值map.put(1,"开始聊天"),然后通知listView去更新数据,这样就可以实现了。
  代码如下:

//复用按钮时监听器的判断逻辑
holder.click.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //获取不同的btn对象
                int pos = (Integer) holder.click.getTag();
                //分情况
                if (beanList.get(pos).getBtnMap().get(1) != null) {
                    //处理新的监听逻辑
                    Intent intent = new Intent();
                    intent.setClass(mContext, RecyclerActivity.class);
                    intent.putExtra("position", pos);
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    mContext.startActivity(intent);
                } else {
                    //处理最初的逻辑
                    callBackToParent.callToParent(false, pos);
                }
            }
        });
//复用按钮时按钮文本的赋值逻辑
//btn内容(分情况)
if (beanList.get(position).getBtnMap().get(1) != null) {
    holder.click.setText(beanList.get(position).getBtnMap().get(1));
} else {
    holder.click.setText(beanList.get(position).getBtnMap().get(0));
}

  好了,这样的话,大功告成!我们看看最终实现的效果吧!
 

listViewFinish


  关于ListView的内容,就先总结到这里,接下来将会对高德地图的开发工作进行回顾与总结。

CATALOG
  1. 1. ListView控件复用详解
    1. 1.1. ListView是什么?
    2. 1.2. ListView的控件复用
    3. 1.3. ListView难点探究
      1. 1.3.1. 1.ListView的加载问题
      2. 1.3.2. 2.ListView的控件复用中的控件混乱问题