ListView複用和優化之多佈局詳解

前言

在上一篇文章中,我已經非常詳細的闡述了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下載

源碼下載

總結

複用的問題博主一再強調,基本都是由於沒有初始化狀態引起的,還有很少部分是其他原因

那麼本篇也就這樣子結束啦,歡迎大家關注小金子!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章