ListView复用和优化之多布局详解

版权声明:本文为博主原创文章,未经博主允许不得转载 https://blog.csdn.net/u011692041/article/details/53126435

前言

在上一篇文章中,我已经非常详细的阐述了ListView的复用原理和几个大家不太明白的地方.也同时重现了复用的问题并告诉大家如何去解决.如果你没有看上一篇,请先移步,这篇基于上一篇的知识继续讲解ListView中多布局是个什么原理

ListView复用和优化详解

实现联系人列表的展现形式

先随便放一个联系人列表的效果图,博主随便找了一张图给大家看看效果先

这里写图片描述

我们可以看到,这里肯定是一个列表来实现的,如果我们使用ListView该如何实现呢?

首先我们分析一下

这里我们一眼就可以看到有两种形式的布局
之前我们脑袋中的ListView显示的数据都是针对一个条目布局文件的,也就是每个item都是显示效果一致的

解决方式一

我们使用一个Item实现,一个Item布局里面包含两个Item,什么意思呢?其实就是一个Item里面是类似下面示意图中的布局

item12这里写图片描述

解决方式二

我们使用两个Item实现

item1:这里写图片描述

item2:这里写图片描述

解决方式三

我们也使用两个Item实现,配合ListView中的
getItemViewType(int position)方法

getViewTypeCount()方法

如果不太清楚没关系,下面博主会带你们都实现一遍的

布局文件

先放上各个界面的xml

Activity的xml

里面就是一个ListView

<?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>

Item1

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_tag"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#28C4B2"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_tag"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="12dp"
        android:text="A"
        android:textColor="#000000"
        android:textSize="16sp" />

</LinearLayout>

对应预览图:这里写图片描述

Item2

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_name"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#FFFFFF"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="12dp"
        android:layout_marginLeft="18dp"
        android:text="陈旭金"
        android:textColor="#000000"
        android:textSize="22sp" />

</LinearLayout>

对应预览图:这里写图片描述

item12

<?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="vertical">

    <include layout="@layout/item1" />
    <include layout="@layout/item2" />

</LinearLayout>

上述就是包含进来item1和item2的布局,使用include标签,这个和大家提醒一下

对应预览图:这里写图片描述

我们要实现多布局的展示,首先有一点你必须明确,就是你必须知道当下标position为任何一个数字的时候,你能知道这个position下标对应的该使用哪个布局,所以这就要求我们能从数据来源中根据position判断该使用哪种布局,所以这里博主采用在展现的集合中使用如下的形式

private List<User> listViewData = new ArrayList<User>();
public class User {

    /**
     * 当有tagName属性的时候没有name的值
     */
    private String tagName;

    /**
     * 当有name值得时候,没有tagName值
     */
    private String name;

    //构造函数
    public User(String tagName, String name) {
        this.tagName = tagName;
        this.name = name;
    }

    public String getTagName() {
        return tagName;
    }

    public void setTagName(String tagName) {
        this.tagName = tagName;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

这样子有一个什么好处呢?当我在getView中要显示数据的时候,我可以通过position拿到集合中对应的User
通过User中这两个属性name和tagName来判断该使用哪种布局

当然了你也可以通过其他的方式来判断,比如使用

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

然后在每一个元素前面加上标识,比如显示联系人的头的数据就需要这样子
tag:A
联系人的名字的时候就这样子:
content:陈旭金

这都是可以的,只要能用于判断即可

实现方式一:采用单布局

首先我们书写我们的适配器

public class ListViewAdapter1 extends BaseAdapter {

    private List<User> listViewData;

    private Context mContext;

    public ListViewAdapter1(List<User> 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 rowView, ViewGroup viewGroup) {
        //创建了布局,里面既有Tag的布局也有Name的布局
        rowView = View.inflate(mContext, R.layout.item12, null);

        //拿到里面的两个布局
        LinearLayout ll_tag = (LinearLayout) rowView.findViewById(R.id.ll_tag);
        LinearLayout ll_name = (LinearLayout) rowView.findViewById(R.id.ll_name);

        //拿到下标position对应的数据
        User user = listViewData.get(position);

        if (user.getTagName() != null) { //表示这应该显示联系人的字母头
            //那么应该隐藏内容的布局
            ll_name.setVisibility(View.GONE);
            //找到文本控件赋值
            TextView tv_tag = (TextView) rowView.findViewById(R.id.tv_tag);
            tv_tag.setText(user.getTagName());
        } else { //表示这应该显示联系人的名称
            //那么应该隐藏tag的布局
            ll_tag.setVisibility(View.GONE);
            //找到文本控件赋值
            TextView tv_name = (TextView) rowView.findViewById(R.id.tv_name);
            tv_name.setText(user.getName());
        }

        return rowView;
    }

}

还是关注我们的getView方法,博主还是和上一篇一样,先不使用复用View,每次都是创建了一个新的ItemView
这里的难点在于你需要判断该使用哪一种布局,然后再混合布局中隐藏不该使用的那部分,这样子就很巧妙的实现了多布局的展示,而且只使用到一个布局文件
而这里的判断条件我们上面已经说过了

然后我们在Activity中的代码

public class MainActivity extends AppCompatActivity {

    private ListView lv;

    private BaseAdapter listViewAdapter;

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

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


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

        //数据造假一些
        listViewData.add(new User("C", null));
        listViewData.add(new User(null, "陈旭金1"));
        listViewData.add(new User(null, "陈旭金2"));
        listViewData.add(new User(null, "陈旭金3"));
        listViewData.add(new User(null, "陈旭金4"));

        listViewData.add(new User("D", null));
        listViewData.add(new User(null, "大胖1"));
        listViewData.add(new User(null, "大胖2"));
        listViewData.add(new User(null, "大胖3"));
        listViewData.add(new User(null, "大胖4"));
        listViewData.add(new User(null, "大胖5"));

        listViewAdapter = new ListViewAdapter1(listViewData, this);

        lv.setAdapter(listViewAdapter);

    }

}

博主在造假数据的时候,就一定要保证User对象里面的两个属性一个有另一个没有值,这样子在适配器中才能正常判断哦

最后看效果吧

这里写图片描述

我们看到实现的效果很棒哦,哈哈哈

这里写图片描述

实现方式二和三:采用多个布局

明白了上面那种实现方法,其实再说这种应该你们觉得很容易了,只要对适配器动点手脚即可,那么开始

采用两个布局实现方式1

public class ListViewAdapter2 extends BaseAdapter {

    private List<User> listViewData;

    private Context mContext;

    public ListViewAdapter2(List<User> 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 rowView, ViewGroup viewGroup) {

        //拿到下标position对应的数据
        User user = listViewData.get(position);

        if (user.getTagName() != null) { //表示这应该显示联系人的字母头
            //创建了tag布局
            rowView = View.inflate(mContext, R.layout.item1, null);
            //找到文本控件赋值
            TextView tv_tag = (TextView) rowView.findViewById(R.id.tv_tag);
            tv_tag.setText(user.getTagName());
            return rowView;
        } else { //表示这应该显示联系人的名称
            //创建了name布局
            rowView = View.inflate(mContext, R.layout.item2, null);
            //找到文本控件赋值
            TextView tv_name = (TextView) rowView.findViewById(R.id.tv_name);
            tv_name.setText(user.getName());
            return rowView;
        }

    }

}

采用两个布局实现方式2

public class ListViewAdapter3 extends BaseAdapter {

    /**
     * 表示是字母头
     */
    private static int HEADER = 1;

    /**
     * 表示是正常的Item
     */
    private static int CONTENT = 2;

    private List<User> listViewData;

    private Context mContext;

    public ListViewAdapter3(List<User> 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 rowView, ViewGroup viewGroup) {

        //拿到下标position对应的数据
        User user = listViewData.get(position);

        int itemViewType = getItemViewType(position);

        if (itemViewType == HEADER) {
            //创建了tag布局
            rowView = View.inflate(mContext, R.layout.item1, null);
            //找到文本控件赋值
            TextView tv_tag = (TextView) rowView.findViewById(R.id.tv_tag);
            tv_tag.setText(user.getTagName());
            return rowView;
        } else {//表示这应该显示联系人的名称
            //创建了name布局
            rowView = View.inflate(mContext, R.layout.item2, null);
            //找到文本控件赋值
            TextView tv_name = (TextView) rowView.findViewById(R.id.tv_name);
            tv_name.setText(user.getName());
            return rowView;
        }


    }

    @Override
    public int getItemViewType(int position) {
        User user = listViewData.get(position);
        if (user.getTagName() != null) { //如果是字母头
            return HEADER;
        } else {
            return CONTENT;
        }
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

}

关注getView中的代码,可以发现很简单,就是判断该使用哪种布局
但是判断的第一种方式我们是自己实现的,第二种方式中,使用

ListView提供的getItemViewType(int position)

其实道理都是一样的,并且我们需要告诉适配器,这里面有两种类型的Item

public int getViewTypeCount() {
    return 2;
}

然后找到对应的控件,然后直接返回创建的View
看上去比上面一种方法还要简单,最后看看效果,我在多添加点数据

        //数据造假一些
        listViewData.add(new User("C", null));
        listViewData.add(new User(null, "陈旭金1"));
        listViewData.add(new User(null, "陈旭金2"));
        listViewData.add(new User(null, "陈旭金3"));
        listViewData.add(new User(null, "陈旭金4"));

        listViewData.add(new User("D", null));
        listViewData.add(new User(null, "大胖1"));
        listViewData.add(new User(null, "大胖2"));
        listViewData.add(new User(null, "大胖3"));
        listViewData.add(new User(null, "大胖4"));
        listViewData.add(new User(null, "大胖5"));

        listViewData.add(new User("H", null));
        listViewData.add(new User(null, "胡歌1"));
        listViewData.add(new User(null, "胡歌2"));
        listViewData.add(new User(null, "胡歌3"));
        listViewData.add(new User(null, "胡歌4"));
        listViewData.add(new User(null, "胡歌5"));
        listViewData.add(new User(null, "胡歌6"));

这里写图片描述

完美哦这里写图片描述

上面的实现方法我们都是直接创建了新的View然后返回的,那么如何结合复用和ViewHolder呢?

结合复用和ViewHolder

我们在上面用了两种的方法来实现,那么下面博主也同样在两种情况下分别结复用和ViewHolder来讲解

单布局下的复用和ViewHolder的使用

复用的根本就是如果传递给你的View你得用起来,在单个布局下,传进来的肯定是同一种类型的View,什么意思呢?
就是说单个布局的列表,由于每次创建新的条目View都是使用同一个布局文件,所以在复用的时候和上一篇的复用一样,直接判断是否为空然后使用就可以了
而我们上述的第二种方法实现的,我们就不能直接用了,因为里面用到了两个布局文件,传进来复用的View可能不是同一个类型的

那么直接写代码

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

        ViewHolder vh;
        if (rowView == null) {
            //创建了布局,里面既有Tag的布局也有Name的布局
            rowView = View.inflate(mContext, R.layout.item12, null);
            //创建ViewHolder
            vh = new ViewHolder();
            vh.tv_tag = (TextView) rowView.findViewById(R.id.tv_tag);
            vh.tv_name = (TextView) rowView.findViewById(R.id.tv_name);
            //绑定ViewHolder
            rowView.setTag(vh);
        } else {
            //拿出ViewHolder
            vh = (ViewHolder) rowView.getTag();
        }

        //拿到里面的两个布局
        LinearLayout ll_tag = (LinearLayout) rowView.findViewById(R.id.ll_tag);
        LinearLayout ll_name = (LinearLayout) rowView.findViewById(R.id.ll_name);

        //状态还原,少了这两句代码就会出现复用问题
        //ll_tag.setVisibility(View.VISIBLE);
        //ll_name.setVisibility(View.VISIBLE);

        //拿到下标position对应的数据
        User user = listViewData.get(position);

        if (user.getTagName() != null) { //表示这应该显示联系人的字母头
            //那么应该隐藏内容的布局
            ll_name.setVisibility(View.GONE);
            //赋值
            vh.tv_tag.setText(user.getTagName());
        } else { //表示这应该显示联系人的名称
            //那么应该隐藏tag的布局
            ll_tag.setVisibility(View.GONE);
            //赋值
            vh.tv_name.setText(user.getName());
        }

        return rowView;
    }

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

我们可以看到和上一篇几乎一模一样,所以这里不再详解.
效果呢?

这里写图片描述

细心一点就可以看出来,这里面明显出现了复用的问题,而这个问题和上一篇的多选框不一样,而是有些条目不再显示了,这是为什么呢?

比如你的tag的item在显示的时候,你把另一半name的部分给隐藏了,如果这个item在后面复用的时候刚好需要作为name的item显示,那么此时你又把tag的部分给隐藏了.而你从来没有还原过这些状态

所以记牢一句话,列表复用的问题80%都是因为没有初始化的原因!

所以给getView方法里面加上初始化的代码

这里写图片描述

这里写图片描述

可以看到复用的问题解决啦!

多布局下的复用和ViewHolder的使用

方式1

我们说了多布局就是在判断出position下标对应该使用哪一个Item,从而创建对应的布局文件,那么当复用的View在方法getView中传递给你的时候,你能知道这个View是不是能够复用呢?

假如你当前需要显示name,那么你需要item2的布局文件对应的View,可以复用传递给你的View可能是item1对应的View也可能是item2对应的View,此时你又该如何做判断呢?

多布局在复用的时候产生的问题
如何判断传递给你的View是你可以复用的View

定位问题原因
没办法区别传进来的View是否是tag的还是name的

解决办法
利用View类自带的setTag方法,我们复用的时候,肯定还利用了ViewHolder

结合ViewHolder

所以我们的ViewHolder是这样子哒!tag用来区别是哪个Item

class ViewHolder {
    TextView tv_tag;
    TextView tv_name;
    int tag;
}

然后getView方法再改一下。。。。大家耐心看哈。。。。

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

        //拿到下标position对应的数据
        User user = listViewData.get(position);

        ViewHolder vh = null;

        boolean isTag = user.getTagName() != null;

        if (rowView == null) {

            //创建ItemView和ViewHolder并绑定
            rowView = createItemViewAndViewHolder(isTag);

        } else {
            if (isTag && vh.tag == CONTENT) { //表示传入的视图不匹配
                //创建ItemView和ViewHolder并绑定
                rowView = createItemViewAndViewHolder(isTag);
            } else if (!isTag && vh.tag == HEADER) {//表示传入的视图不匹配
                //创建ItemView和ViewHolder并绑定
                rowView = createItemViewAndViewHolder(isTag);
            }
        }

        //拿到ViewHolder
        vh = (ViewHolder) rowView.getTag();

        if (isTag) {
            //赋值
            vh.tv_tag.setText(user.getTagName());
        } else {
            //赋值
            vh.tv_name.setText(user.getName());
        }

        return rowView;

    }

    /**
     * 创建ItemView和ViewHolder并绑定
     * @param isTag
     * @return
     */
    private View createItemViewAndViewHolder(boolean isTag) {
        View rowView;
        //创建ViewHolder
        ViewHolder vh = new ViewHolder();
        if (isTag) {
            //创建了Tag的布局
            rowView = View.inflate(mContext, R.layout.item1, null);
            vh.tv_tag = (TextView) rowView.findViewById(R.id.tv_tag);
            vh.tag = HEADER;
        } else {
            //创建了Name的布局
            rowView = View.inflate(mContext, R.layout.item2, null);
            vh.tv_name = (TextView) rowView.findViewById(R.id.tv_name);
            vh.tag = CONTENT;
        }
        rowView.setTag(vh);
        return rowView;
    }

    /**
     * 用于存放一个ItemView中的控件,由于这里最多两个控件,那么声明两个控件即可
     */
    class ViewHolder {
        TextView tv_tag;
        TextView tv_name;
        int tag;
    }

博主感觉没啥好说的了,因为都写在注释上了……..

方式2

搭配使用ListView的方法

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

    //拿到下标position对应的数据
    User user = listViewData.get(position);

    ViewHolder vh = null;

    int type = getItemViewType(position);

    if (rowView == null) {
        //创建ViewHolder
        vh = new ViewHolder();
        if (type == HEADER) {
            //创建了Tag的布局
            rowView = View.inflate(mContext, R.layout.item1, null);
            vh.tv_tag = (TextView) rowView.findViewById(R.id.tv_tag);
        }else{
            //创建了Name的布局
            rowView = View.inflate(mContext, R.layout.item2, null);
            vh.tv_name = (TextView) rowView.findViewById(R.id.tv_name);
        }
    }else{
        vh = (ViewHolder) rowView.getTag();
    }

    if (type == HEADER) {
        //赋值
        vh.tv_tag.setText(user.getTagName());
    }else{
        //赋值
        vh.tv_name.setText(user.getName());
    }

    rowView.setTag(vh);

    return rowView;

}

@Override
public int getItemViewType(int position) {
    User user = listViewData.get(position);
    if (user.getTagName() != null) { //如果是字母头
        return HEADER;
    } else {
        return CONTENT;
    }
}

@Override
public int getViewTypeCount() {
    return 2;
}

/**
 * 用于存放一个ItemView中的控件,由于这里最多两个控件,那么声明两个控件即可
 */
class ViewHolder {
    TextView tv_tag;
    TextView tv_name;
}
getViewTypeCount()

上面方式1和方式2主要区别是以下几点:

方式1自己判断每一个Item该使用的布局文件,所以复用的需要对穿进来的rowView进行判断是否是item1的还是item2的
方式2由ListView的getItemViewType方法和getViewTypeCount方法控制,所以传进来的rowView肯定是和这个Item对应的,不需要担心方式1的问题

这里明显使用方式2比较方便,而且是ListView支持的,但是博主记得这两个方法以前是没有的,所以博主对博客进行了改进

demo下载

源码下载

总结

复用的问题博主一再强调,基本都是由于没有初始化状态引起的,还有很少部分是其他原因

那么本篇也就这样子结束啦,欢迎大家关注小金子!

猜你喜欢

转载自blog.csdn.net/u011692041/article/details/53126435