BRVAH 樹形列表詳解の使用篇

BaseRecyclerViewAdapterHelper

簡單 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());
    }

}
  • 繼承 BaseMultiItemQuickAdapter<T, VH>

  • 在構造函數中使用 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());

由於在收起子列表會導致數據源發生變化,所以:

  1. 每次循環都要重新獲取 data.size()
  2. 收起列表後,原本的 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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章