簡單 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