2017.5.16 添加demo
Demo
在开发 Android APP 的时候,难免会需要实现二级列表的情况,而在自己的项目中使用的列表是 android.support.v7.widget
包里面的 RecyclerView
,好处是可以根据情况实现不同样式的列表,可扩展程度高。而坏处是什么都要自己实现。所以在想要用 RecyclerView
实现的二级列表的时候,却发现没有类似 ListView
的 ExpandableListView
,只能自己去实现。
在使用 RecyclerView
的时候,与 ListView
类似,需要创建一个 Adapter
去告诉 RecyclerView
如何工作1,而在创建 RecyclerView
的 Adapter
的时候,一般需要重载以下几个方法:
onCreateViewHolder()
为每个项目创建 ViewHolder
onBindViewHolder()
处理每个 item
getItemViewType()
在 onCreateViewHolder
前调用,返回 item
类型
getItemCount()
获取 item
总数
加载 RecyclerView
的过程如下图:
除此之外,还需要创建一个 ViewHolder
用于寻找自定义 item
的各个控件。
根据上述,在实现二级列表的时候,我们在 onCreateViewHolder()
和 onBindViewHolder()
中,判断是加载一级项目(GroupItem)还是二级子项目(SubItem)。
一般来说,一个 GroupItem 下面有一个,或多个 SubItem,一对多。
在这里,用一个 DataTree
来封装这种数据格式,代码如下:
public class DataTree<K, V> {
private K groupItem;
private List subItems;
public DataTree(K groupItem, List subItems) {
this.groupItem = groupItem;
this.subItems = subItems;
}
public K getGroupItem() {
return groupItem;
}
public List getSubItems() {
return subItems;
}
}
ItemStatus
用封装列表每一项的状态,包括:
viewType
item的类型,group item 还是 subitem
groupItemIndex
一级索引位置
subItemIndex
如果该 item 是一个二级子项目,则保存子项目索引
private static class ItemStatus {
public static final int VIEW_TYPE_GROUPITEM = 0;
public static final int VIEW_TYPE_SUBITEM = 1;
private int viewType;
private int groupItemIndex = 0;
private int subItemIndex = -1;
public ItemStatus() {
}
public int getViewType() {
return viewType;
}
public void setViewType(int viewType) {
this.viewType = viewType;
}
public int getGroupItemIndex() {
return groupItemIndex;
}
public void setGroupItemIndex(int groupItemIndex) {
this.groupItemIndex = groupItemIndex;
}
public int getSubItemIndex() {
return subItemIndex;
}
public void setSubItemIndex(int subItemIndex) {
this.subItemIndex = subItemIndex;
}
}
这里的 groupItem
和 subItem
分别用不同的布局文件,所以我把 ViewHolder
分开写,如下:
public static class GroupItemViewHolder extends RecyclerView.ViewHolder {
...
public GroupItemViewHolder(View itemView) {
super(itemView);
...
}
}
public static class SubItemViewHolder extends RecyclerView.ViewHolder {
...
public SubItemViewHolder(View itemView) {
super(itemView);
...
}
}
context
list dataTrees
用于显示的数据
list
保存 groupItem
状态,开还是关
//向外暴露设置显示数据的方法
public void setDataTrees(List> dt) {
this.dataTrees = dt;
initGroupItemStatus(groupItemStatus);
notifyDataSetChanged();
}
//设置初始值,所有 groupItem 默认为关闭状态
private void initGroupItemStatus(List l) {
for (int i = 0; i < dataTrees.size(); i++) {
l.add(false);
}
}
顾名思义,用于根据 position
来计算判断该 item
的状态,返回一个 ItemStatus
。
代码如下:
private ItemStatus getItemStatusByPosition(int position) {
ItemStatus itemStatus = new ItemStatus();
int count = 0; //计算groupItemIndex = i 时,position最大值
int i = 0;
//轮询 groupItem 的开关状态
for (i = 0; i < groupItemStatus.size(); i++ ) {
//pos刚好等于计数时,item为groupItem
if (count == position) {
itemStatus.setViewType(ItemStatus.VIEW_TYPE_GROUPITEM);
itemStatus.setGroupItemIndex(i);
break;
//pos大于计数时,item为groupItem(i - 1)中的某个subItem
} else if (count > position) {
itemStatus.setViewType(ItemStatus.VIEW_TYPE_SUBITEM);
itemStatus.setGroupItemIndex(i - 1);
itemStatus.setSubItemIndex(position - ( count - dataTrees.get(i - 1).getSubItems().size() ) );
break;
}
//无论groupItem状态是开或者关,它在列表中都会存在,所有count++
count++;
//当轮询到的groupItem的状态为“开”的话,count需要加上该groupItem下面的子项目数目
if (groupItemStatus.get(i)) {
count += dataTrees.get(i).getSubItems().size();
}
}
//简单地处理当轮询到最后一项groupItem的时候
if (i >= groupItemStatus.size()) {
itemStatus.setGroupItemIndex(i - 1);
itemStatus.setViewType(ItemStatus.VIEW_TYPE_SUBITEM);
itemStatus.setSubItemIndex(position - ( count - dataTrees.get(i - 1).getSubItems().size() ) );
}
return itemStatus;
}
该方法在显示列表的时候会执行多次,如果返回的项目计数不正确的话程序会出现错误奔溃,代码如下:
@Override
public int getItemCount() {
Logger.i("1");
int itemCount = 0;
if (groupItemStatus.size() == 0) {
return 0;
}
for (int i = 0; i < dataTrees.size(); i++) {
if (groupItemStatus.get(i)) {
itemCount += dataTrees.get(i).getSubItems().size() + 1;
} else {
itemCount++;
}
}
return itemCount;
}
该方法会在 onCreateViewHolder()
前执行,并返回 int viewType
,代码如下:
@Override
public int getItemViewType(int position) {
return getItemStatusByPosition(position).getViewType();
}
getItemViewType()
返回的 viewType
选择不同的项目布局,代码如下: @Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v;
RecyclerView.ViewHolder viewHolder = null;
if (viewType == ItemStatus.VIEW_TYPE_GROUPITEM) {
v = LayoutInflater.from(parent.getContext()).inflate(R.layout
.item_artist_detail_album, parent, false);
viewHolder = new GroupItemViewHolder(v);
} else if (viewType == ItemStatus.VIEW_TYPE_SUBITEM) {
v = LayoutInflater.from(parent.getContext()).inflate(R.layout
.item_artist_detail_track, parent, false);
viewHolder = new SubItemViewHolder(v);
}
return viewHolder;
}
viewType
绑定不同的 ViewHolder
,代码如下: @Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final ItemStatus itemStatus = getItemStatusByPosition(position);
final DataTree dt = dataTrees.get(itemStatus.getGroupItemIndex());
if ( itemStatus.getViewType() == ItemStatus.VIEW_TYPE_GROUPITEM ) {
final GroupItemViewHolder groupItemVh = (GroupItemViewHolder) holder;
. . . //加载groupItem,处理groupItem控件
groupItemVh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int groupItemIndex = itemStatus.getGroupItemIndex();
if ( !groupItemStatus.get(groupItemIndex) ) {
. . . //groupItem由“关闭”状态到“打开”状态
groupItemStatus.set(groupItemIndex, true);
notifyItemRangeInserted(groupItemVh.getAdapterPosition() + 1, dt.getSubItems().size());
} else {
. . . //groupItem由“打开”状态到“关闭”状态
groupItemStatus.set(groupItemIndex, false);
notifyItemRangeRemoved(groupItemVh.getAdapterPosition() + 1, dt.getSubItems().size());
}
}
});
} else if (itemStatus.getViewType() == ItemStatus.VIEW_TYPE_SUBITEM) {
SubItemViewHolder subItemVh = (SubItemViewHolder) holder;
. . . //加载subItem,处理subItem控件
subItemVh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
. . . //点击subItem处理
}
});
}
}
二级列表对 RecyclerView
小小地扩展,在实现的过程中,有点麻烦的地方是对特定的 item
进行识别,即 getItemViewType()
的实现,在这里,我简单地用遍历轮询的方法去判断,应该会有更简单更节省资源的方法。实现二级列表之后,理论上可以实现多级列表,可以试试。
在用这个方法实现之前,有尝试过有 View
提供的方法—— View.setTag()
,但是由于 RecyclerView
的加载机制,当列表被划出界面时,会被销毁,而重新划进来显示时,RecyclerView
会重新创建新的 item
,而不是之前的那个,所以,之前的 Tag
会不见,最后没能实现出来,感兴趣的同学可以试试。