简单 Demo
定义 Item
为减少篇幅,这里省略了构造函数和 getter/setter 方法。
/**
* 省份(一级列表)
*/
public class Province extends AbstractExpandableItem<City> implements MultiItemEntity {
private String name;
@Override
public int getLevel() {
return 0;
}
@Override
public int getItemType() {
return R.layout.item_province;
}
}
/**
* 城市(二级列表)
*/
public class City extends AbstractExpandableItem<Town> implements MultiItemEntity {
private String name;
@Override
public int getLevel() {
return 1;
}
@Override
public int getItemType() {
return R.layout.item_city;
}
}
/**
* 乡镇(三级列表)
*/
public class Town implements MultiItemEntity {
@Override
public int getItemType() {
return R.layout.item_town;
}
}
- 所有带子列表的 Item 都要实现接口 IExpandable<T> 。抽象类 AbstractExpandableItem<T> 已经实现了该接口并做了常用接口封装,推荐直接继承它。
-
getLevel()
函数的返回值必须从 0 开始,子列表的 level 必须大于父列表的 level 。 - 为了使不同 Item 使用不同布局,需要实现接口 MultiItemEntity 。
布局 Item
item_province.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="10dp">
<TextView
android:id="@+id/tvProvince"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
<!-- 标识该 Item 的子列表是否展开,图片是 → ,通过旋转控制状态 -->
<ImageView
android:id="@+id/ivExpandIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:src="@mipmap/arrow_r" />
</FrameLayout>
item_city.xml
城市也含有子列表,布局与 Province 一样,仅仅 id 不同。
item_town.xml
乡镇没有子列表,布局很简单,只有一个 TextView。
技巧:可以通过设置子列表的 margin_start 控制不同级别列表的缩进效果。
定义 Adapter
/**
* 地区适配器
*/
public class LocationAdapter extends BaseMultiItemQuickAdapter<MultiItemEntity, BaseViewHolder> {
public LocationAdapter(List<MultiItemEntity> data) {
super(data);
// 指定 type 对应的布局资源
addItemType(R.layout.item_province, R.layout.item_province);
addItemType(R.layout.item_city, R.layout.item_city);
addItemType(R.layout.item_town, R.layout.item_town);
setOnItemClickListener();
}
// 设置 Item 点击事件监听器
private void setOnItemClickListener() {
OnItemClickListener onItemClickListener = new OnItemClickListener() {
@Override
public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
MultiItemEntity item = getItem(position);
if (!(item instanceof AbstractExpandableItem)) {
return;
}
if (((AbstractExpandableItem) item).isExpanded()) {
// 收起被点击 Item 的子列表
collapse(position + getHeaderLayoutCount());
} else {
// 展开被点击 Item 的子列表
expand(position + getHeaderLayoutCount());
}
}
};
setOnItemClickListener(onItemClickListener);
}
@Override
protected void convert(@NonNull BaseViewHolder helper, MultiItemEntity item) {
switch (helper.getItemViewType()) {
case R.layout.item_province:
showProvince(helper, (Province) item);
break;
case R.layout.item_city:
showCity(helper, (City) item);
break;
case R.layout.item_town:
showTown(helper, (Town) item);
break;
default:
break;
}
}
private void showProvince(@NonNull BaseViewHolder helper, Province province) {
helper.setText(R.id.tvProvince, province.getName());
helper.getView(R.id.ivExpandIcon).setRotation(province.isExpanded() ? 90 : 0);
}
private void showCity(@NonNull BaseViewHolder helper, City city) {
helper.setText(R.id.tvCity, city.getName());
helper.getView(R.id.ivExpandIcon).setRotation(city.isExpanded() ? 90 : 0);
}
private void showTown(@NonNull BaseViewHolder helper, Town town) {
helper.setText(R.id.tvTown, town.getName());
}
}
-
在构造函数中使用
addItemType(type, layoutId)
函数指定每种 Item 类型对应的布局资源。 -
在使用点击事件时要注意:回调函数的 position 参数是相对于数据列表的位置,而不是 UI 上的位置。因此,如果为 Adapter 添加了头布局,使用
collpase(pos)
expand(pos)
等函数操作子列表时 position 参数必须加上头布局的数量。expand(adapterPosition + getHeaderLayoutCount()); collapse(adapterPosition + getHeaderLayoutCount());
使用 Adapter
private void initAdapter() {
List<? extends MultiItemEntity> dataList = mockData(10);
mAdapter = new LocationAdapter((List<MultiItemEntity>) dataList);
mRecyclerView.setAdapter(mAdapter);
}
// 模拟数据
private List<? extends MultiItemEntity> mockData(int pageSize) {
Random mRandom = new Random();
List<Province> provinceList = new ArrayList<>();
for (int i = 0; i < pageSize; i++) {
// 省份
Province province = new Province(String.format("Province %s", pageSize + i));
provinceList.add(province);
int cityCount = mRandom.nextInt(5);
for (int j = 0; j < cityCount; j++) {
// 城市
City city = new City(String.format("City %s-%s", i, j));
province.addSubItem(city);
int townCount = mRandom.nextInt(5);
for (int k = 0; k < townCount; k++) {
// 乡镇
city.addSubItem(new Town(String.format("Town %s-%s-%s", i, j, k)));
}
}
}
return provinceList;
}
复杂用法
展开所有直接和间接子列表
adapter.expandAll();
默认展开某一个列表
mRecyclerView.setAdapter(mAdapter);
// 展开指定 position 的 Item 的直接子列表。
mAdapter.expand(position);
// 展开指定 position 的 Item 的所有直接和间接子列表。
mAdapter.expandAll(position, true);
最多同时展开一个子列表
List data = adapter.getData();
// 记录要展开子列表的 Item
IExpandable willExpandItem = (IExpandable) data.get(position);
// 遍历关闭已经展开的子列表
for (int i = getHeaderLayoutCount(); i < data.size(); i++) {
IExpandable expandable = (IExpandable) data.get(i);
if (expandable.isExpanded()) {
adapter.collapse(i);
}
}
// 展开被点击的 Item 的子列表
adapter.expand(data.indexOf(willExpandItem) + getHeaderLayoutCount());
由于在收起子列表会导致数据源发生变化,所以:
- 每次循环都要重新获取
data.size()
。 - 收起列表后,原本的 position 不能直接使用,需要重新获取 position 。
添加数据
添加到一级列表
Province province = new Province("Province new");
mAdapter.addData(province);
添加到子列表
// 添加新的 Town 到某个 City
Town town = new Town("Town new");
city.addSubItem(town);
// 如果该 City 的子列表已经展开,渲染新数据到 UI
int cityIndex = mAdapter.getData().indexOf(city);
if (cityIndex >= 0 && city.isExpanded()) {
mAdapter.addData(cityIndex + city.getSubItems().size(), town);
}
删除数据
删除一级列表数据
int provinceIndex = mAdapter.getData().indexOf(province);
mAdapter.remove(provinceIndex);
删除子列表数据
public void removeItem(MultiItemEntity item) {
int index = mAdapter.getData().indexOf(item);
if (index >= 0) {
// 已经加载到 Adapter 中的直接删除
mAdapter.remove(index);
} else {
// 未加载到 Adapter 中的,通过父级删除
removeFromParent(mAdapter.getData(), item);
}
}
// 从数据列表或子列表中查找指定 Item 的父级并删除 Item
public void removeFromParent(List<MultiItemEntity> dataList, MultiItemEntity removeItem) {
if (dataList == null || dataList.isEmpty()) {
return;
}
if (dataList.contains(removeItem)) {
dataList.remove(removeItem);
return;
}
for (MultiItemEntity entity : dataList) {
if (entity instanceof IExpandable) {
removeFromParent(((IExpandable) entity).getSubItems(), removeItem);
}
}
}
加载更多
上拉加载到更多数据后,自行将新的数据拼到 Adapter 的数据源(mAdapter.getData()
)的后面即可。
如果可以确定每次加载到的都是完整的一级列表,那么直接添加即可。
// 模拟加载更多
List<MultiItemEntity> newList = new ArrayList<>();
newList.add(new Province("province new"));
// 添加数据到列表
mAdapter.addData(newList);
如果每次加载时数据可能中断,如某个子列表分多次加载完毕,那么用树形列表不太合适,需求/设计可能存在缺陷。如果非要这么做,请自行拼接加载到的新数据和原数据并刷新 UI。
展开最底部的 Item
展开最底部的 Item 子列表时,用户可能需要滑动才能看到展开的数据,因此要处理一下:自动向上滚动一段距离以展示新的数据。
// 展开
mAdapter.expand(position);
// 滚动到下一个 Item,如果已经显示,则不会发生滚动
mRecyclerView.smoothScrollToPosition(position + 1);
多布局用法
树形多布局与普通多布局用法相同,比如添加直辖市类型的 Item(直辖市与省份同级)。
/**
* 直辖市(一级列表)
*/
public class Municipality extends AbstractExpandableItem<Town> implements MultiItemEntity {
private String name;
@Override
public int getLevel() {
return 0;
}
@Override
public int getItemType() {
return R.layout.item_municipality;
}
}
// 在 Adapter 中添加新的 Type 并处理数据。
addItemType(R.layout.item_municipality, R.layout.item_municipality);
易错点
关于 position
expand(position)
collapse(position)
等相关函数的 position 参数的值必须加上头布局的数量。
expand(position + getHeaderLayoutCount());
collapse(position + getHeaderLayoutCount());
关于 Item 实体类
实现 AbstractExpandableItem#getLevel()
函数,函数返回值必须从 0 开始,子列表的 level 值必须大于父列表的 level 值。
BRVAH Demo
BRVAH 项目中的 Demo。
-
普通多布局:MultipleItemUseActivity
-
树形列表: ExpandableUseActivity
网友评论