【Flutter 專題】123 圖解簡易 GroupList 二級分組列表

阿策小和尚發表於2021-05-30

    小菜在實踐過程中,想實現一個可選的二級分組列表,pub.dev 外掛庫中已經有很多類似功能的外掛,小菜還是準備從自己角度嘗試實現一個簡單的二級分組列表;

  • 列表分為兩級,預設均摺疊
  • 一級列表和二級列表均可選中和取消
  • 二級列表資料可以主動新增

    分組列表的實現有很多方式,小菜準備用最基本的兩個 ListView 巢狀的思路來進行展示,預設是展示第一級列表資訊,在點選展開操作時,展示對應的二級列表;

GroupList

1. Bean 資料結構

    小菜先準備好資料實體類,一級列表 CategoryBean 中包含對應的二級列表 SubCategoryBean,其中兩個實體中均包含一個 isChecked 欄位用於儲存當前 item 是否選中狀態;

class CategoryBean {
  String name;
  String url;
  bool _isChecked = false;
  List<SubCategoryBean> itemList;

  bool get isChecked => _isChecked ?? false;

  set isChecked(bool value) => _isChecked = value;

  CategoryBean({this.name, this.url, this.itemList});
}

class SubCategoryBean {
  String name;
  String url;
  bool _isChecked = false;

  SubCategoryBean({this.name, this.url});

  bool get isChecked => _isChecked ?? false;

  set isChecked(bool value) => _isChecked = value;
}
複製程式碼

2. 一級列表

    小菜先展示一級列表,可根據使用場景確認是否使用 SliverListView,小菜測試過程中僅採用基本的 ListView;其中單項選擇框,小菜採用了之前自定義的 ACECheckbox;其中注意,在 ACECheckbox 點選回撥時應注意更改一級實體 BeanisChecked 狀態;

return Scaffold(
    appBar: AppBar(title: Text('分組列表')),
    body: ListView.builder(
        itemCount: widget.listData.length,
        itemBuilder: (context, index) {
          return GroupItemWidget(widget.listData[index]);
        }));

class _GroupItemWidgetState extends State<GroupItemWidget> {
  bool _isExpand = false;

  @override
  Widget build(BuildContext context) {
    return InkWell(
        child: Column(children: <Widget>[
          Divider(height: 0.5, color: Colors.blue),
          Padding(
              padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
              child: Row(children: <Widget>[
                Icon(_isExpand ? Icons.arrow_drop_down : Icons.arrow_right, color: Colors.blue),
                _userIcon(false),
                SizedBox(width: 5.0),
                Expanded(child: Text('${widget.bean.name}', style: TextStyle(fontSize: 16.0))),
                _rightCheckBox(widget.bean, 0)
              ])),
          _subCategoryList(widget.bean)
        ]),
        onTap: () {
          _isExpand = !_isExpand;
          setState(() {});
        });
  }

  _userIcon(isCircle) {
    double size = isCircle ? 40.0 : 45.0;
    return PhysicalModel(
        color: Colors.transparent,
        shape: isCircle ? BoxShape.circle : BoxShape.rectangle,
        clipBehavior: Clip.antiAlias,
        elevation: 2.0,
        borderRadius: BorderRadius.all(Radius.circular(20.0)),
        child: Container(width: size, height: size, child: Image.asset(isCircle ? 'images/icon_qq.png' : 'images/icon_hzw01.jpg')));
  }

  _rightCheckBox(bean, type, {subIndex}) {
    bool _isChecked = type == 0 ? bean.isChecked : bean.itemList[subIndex].isChecked;
    return ACECheckbox(
        value: _isChecked,
        type: ACECheckBoxType.circle,
        unCheckColor: Colors.blue,
        onChanged: (value) {
          setState(() => _isChecked = value);
          if (type == 0) {
            bean.isChecked = _isChecked;
            List.generate(bean.itemList.length, (index) => bean.itemList[index].isChecked = _isChecked);
          }
        });
  }
}
複製程式碼

3. 二級列表

    當點選一級列表 item 時,展現二級列表;而實際上只是在一級 item 中新增一個新的列表資料,僅視覺效果上是展開二級列表;當再次點選一級列表 item 時把新加的二級列表替換為空的 Container 佈局即可;

_subCategoryList(CategoryBean bean) {
  Widget _widget;
  if (!_isExpand ||
      bean == null ||
      bean.itemList == null ||
      bean.itemList.length == 0) {
    _widget = Container();
  } else {
    _widget = ListView.builder(
        itemCount: bean.itemList.length,
        itemBuilder: (context, index) => Row(children: <Widget>[ Flexible(child: _subCategoryItem(bean, index)) ]));
  }
  return _widget;
}

_subCategoryItem(CategoryBean bean, index) {
  return Column(children: <Widget>[
    Divider(height: 0.5, color: Colors.deepOrange),
    Padding(
        padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
        child: Row(children: <Widget>[
          SizedBox(width: 40.0),
          _userIcon(true),
          SizedBox(width: 5.0),
          Expanded(child: Text(bean.itemList[index].name ?? 'SubName')),
          _rightCheckBox(bean, 1, subIndex: index)
        ]))
  ]);
}
複製程式碼

4. ACECheckbox 選中 & 取消

    小菜最想處理的是列表 item 的選中和取消狀態;小菜首先在實體 Bean 中新增一個 isChecked 狀態用於記錄當前選中狀態;

    當一級列表選中時,無論展開或摺疊,二級列表中各元素也全部選中,小菜通過 List.generate 遍歷二級列表更改 isChecked 狀態;

    當二級列表 item 部分選中時,對應的一級列表取消選中狀態;同時當把二級列表中所有 items 均選中時,對應一級列表也要選中;小菜通過遍歷判斷二級列表中選中數量來調整一級 item 對應的 isChecked 狀態;

_rightCheckBox(bean, type, {subIndex}) {
  bool _isChecked = type == 0 ? bean.isChecked : bean.itemList[subIndex].isChecked;
  return ACECheckbox(
      value: _isChecked,
      type: ACECheckBoxType.circle,
      unCheckColor: Colors.blue,
      onChanged: (value) {
        setState(() => _isChecked = value);
        if (type == 0) {
          bean.isChecked = _isChecked;
          List.generate(bean.itemList.length, (index) => bean.itemList[index].isChecked = _isChecked);
        } else {
          bean.itemList[subIndex].isChecked = _isChecked;
          int checkedSize = 0;
          List.generate(bean.itemList.length, (index) {
            if (bean.itemList[index].isChecked == false) {
              bean.isChecked = false;
            } else {
              checkedSize += 1;
            }
            if (checkedSize == bean.itemList.length) {
              bean.isChecked = true;
            }
          });
        }
      });
}
複製程式碼

5. 資料動態新增

    小菜預期的目標是,首次資料只展示一級列表資料,再點選一級列表 item 時才會請求二級列表資料,並動態新增到資料列表中;這樣的優勢便是減少資料請求,簡化資料格式;小菜預設在 分組五 中不設定二級列表資料,在點選時動態新增;

return InkWell(
    child: Column(children: <Widget>[
      Divider(height: 0.5, color: Colors.blue),
      Padding(
          padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
          child: Row(children: <Widget>[
            Icon(_isExpand ? Icons.arrow_drop_down : Icons.arrow_right, color: Colors.blue),
            _userIcon(false), SizedBox(width: 5.0),
            Expanded(child: Text('${widget.bean.name}', style: TextStyle(fontSize: 16.0))),
            _rightCheckBox(widget.bean, 0)
          ])),
      _subCategoryList(widget.bean)
    ]),
    onTap: () {
      _isExpand = !_isExpand;
      setState(() {});
      if (widget.bean.name == '分組五' &&
          (widget.bean.itemList == null || widget.bean.itemList.length == 0)) {
        widget.bean.itemList = [
          SubCategoryBean(name: 'O'),
          SubCategoryBean(name: 'P'),
          SubCategoryBean(name: 'Q')
        ];
      }
    });
複製程式碼

6. 滑動衝突

    小菜通過兩個 ListView 來實現二級分組列表,涉及到手勢衝突,在二級列表展開時,手勢只能在一級列表處觸發,二級列表不會整體滑動且上下有主題色水波紋;小菜之前也曾處理過,只需要在 ListView 中設定 primary: false & shrinkWrap: true 等即可;

_widget = ListView.builder(
    primary: false,
    shrinkWrap: true,
    itemCount: bean.itemList.length,
    itemBuilder: (context, index) => Row(children: <Widget>[ Flexible(child: _subCategoryItem(bean, index)) ]));
複製程式碼


    GroupListPage 案例原始碼


    小菜對 GroupList 二級分組列表的練習暫時告一段落,但真正應用到實際中還應配合具體的資料請求以及頁面操作進行調整,以上僅為一個簡單的測試 Demo;如有錯誤,請多多指導!

來源: 阿策小和尚

相關文章