前言
在上一篇文章中,我已经非常详细的阐述了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下载
总结
复用的问题博主一再强调,基本都是由于没有初始化状态引起的,还有很少部分是其他原因
那么本篇也就这样子结束啦,欢迎大家关注小金子!