前言
在上一篇文章中,我已經非常詳細的闡述了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下載
總結
複用的問題博主一再強調,基本都是由於沒有初始化狀態引起的,還有很少部分是其他原因
那麼本篇也就這樣子結束啦,歡迎大家關注小金子!