flutter 自定义分层级树形组件

这篇具有很好参考价值的文章主要介绍了flutter 自定义分层级树形组件。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

废话不多说,直接上图:

flutter 树形列表,code,玩转flutter,笔记,flutter,android,ui,windows

直接上代码:

前提条件,需在pubspec.yaml中依赖如下三方组件

  fluent_ui:
    git: https://github.com/bdlukaa/fluent_ui.git

  # 可展开收缩de列表小部件
  expandable: ^5.0.1

1.调用入口

SingleChildScrollView(
                      child: material.Material(
                        color: Colors.transparent,
                        child: Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 5),
                          child: ExpandableTheme(
                            data: const ExpandableThemeData(
                              useInkWell: true,
                              inkWellBorderRadius: BorderRadius.all(
                                Radius.circular(3),
                              ),
                            ),
                            child: Builder(
                              builder: (BuildContext context) {
                                return buildDepartTree(true, null,
                                    _personManagerVM.showDepartGroupTree, 0);
                              },
                            ),
                          ),
                        ),
                      ),
                    )

2.函数buildDepartTree()代码

/// 记录当前都被展开的部门id
Map<String?, ExpandableController> expandedGroupIds = {};

Widget buildDepartTree(bool isAllItem, GroupVO? parentGroupVO,
      GroupVO? groupVO, int itemIndentMultiple) {
    if (!_personManagerVM.expandedGroupIds.containsKey(groupVO?.id)) {
      _personManagerVM.expandedGroupIds[groupVO?.id] =
          ExpandableController(initialExpanded: isAllItem ? true : false);
    }
    return ExpandableNotifier(
      controller: _personManagerVM.expandedGroupIds[groupVO?.id],
      child: ScrollOnExpand(
        child: ExpandablePanel(
          theme: const ExpandableThemeData(
            headerAlignment: ExpandablePanelHeaderAlignment.center,
            tapBodyToExpand: true,
            tapBodyToCollapse: false,
            hasIcon: false,
          ),
          header: DepartGroupItemMouseRegionWidget(
            itemIndentMultiple: itemIndentMultiple,
            parentDepartGroup: parentGroupVO,
            departGroup: groupVO ?? GroupVO(name: "--"),
            needShowItemMenuIcon: isAllItem ? false : true,
            onTap: () {
              // 选中的部门,需要更新右侧展示的人员列表
              logI('onSelectionChanged: ${groupVO?.toString()}');
              if (groupVO?.id == null || (groupVO?.id?.isEmpty ?? true)) {
                _personManagerVM.getAllPersons();
              } else {
                _personManagerVM.getEmployeeByGroupIds(
                    groupIds: [groupVO?.id ?? ""], init: true);
              }
              _personManagerVM.currentSelectedGroupId = groupVO?.id;
            },
          ),
          collapsed: const SizedBox.shrink(),
          expanded: buildChildDepartList(
              groupVO, groupVO?.sub ?? [], itemIndentMultiple),
        ),
      ),
    );
  }

3.组件DepartGroupItemMouseRegionWidget代码如下:

class DepartGroupItemMouseRegionWidget extends StatefulWidget {
  final GroupVO? parentDepartGroup;
  final GroupVO departGroup;
  final bool? needShowItemMenuIcon;
  final int itemIndentMultiple;
  final GestureTapCallback? onTap;

  const DepartGroupItemMouseRegionWidget(
      {super.key,
      this.parentDepartGroup,
      required this.departGroup,
      this.needShowItemMenuIcon,
      required this.itemIndentMultiple,
      this.onTap});

  @override
  State<StatefulWidget> createState() =>
      _DepartGroupItemMouseRegionWidgetState();
}

class _DepartGroupItemMouseRegionWidgetState
    extends State<DepartGroupItemMouseRegionWidget> {
  final List<String> departOperateList = [
    /*move, copy, */ S.current.reSetName,
    S.current.delete
  ];
  var menuController = FlyoutController();
  var menuAttachKey = GlobalKey();
  bool isEnter = false;

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      cursor: SystemMouseCursors.click,
      onEnter: (event) {
        setState(() {
          isEnter = true;
        });
      },
      onExit: (event) {
        setState(() {
          isEnter = false;
        });
      },
      child: Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Offstage(
            offstage: (widget.departGroup.sub?.isEmpty ?? true),
            child: Row(
              children: [
                SizedBox(
                  width: widget.itemIndentMultiple * 15,
                ),
                ExpandableIcon(
                  theme: const ExpandableThemeData(
                    expandIcon: FluentIcons.chevron_right,
                    collapseIcon: FluentIcons.chevron_down,
                    iconColor: HLColors.c000000o86,
                    iconSize: 10.0,
                    iconRotationAngle: math.pi / 2,
                    iconPadding: EdgeInsets.only(left: 15, right: 13),
                  ),
                ),
              ],
            ),
          ),
          Expanded(
            child: GestureDetector(
              onTap: widget.onTap,
              child: Container(
                // 添加背景色,不然GestureDetector-》onTap点击空白区域时不响应事件
                color: Colors.transparent,
                alignment: Alignment.centerLeft,
                padding: EdgeInsets.only(
                    left: (widget.departGroup.sub?.isEmpty ?? true)
                        ? (38 + widget.itemIndentMultiple * 15)
                        : 0,
                    right: 15,
                    top: 5,
                    bottom: 5),
                child: Selector<PersonManagerVM, String?>(
                  builder: (context, currentSelectedGroupId, child) => Text(
                    ((widget.departGroup.id?.isEmpty ?? true) ||
                            (widget.departGroup.name?.isEmpty ?? true))
                        ? HLStrings.all
                        : widget.departGroup.name ?? "",
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                    style: ((currentSelectedGroupId ?? "") ==
                            widget.departGroup.id)
                        ? HLTextStyles.s14c0064FFw400
                        : HLTextStyles.s14c000000w400,
                  ),
                  selector: (_, p1) => p1.currentSelectedGroupId,
                ),
              ),
            ),
          ),
          if (widget.needShowItemMenuIcon == true)
            Offstage(
              offstage: !isEnter,
              child: FlyoutTarget(
                key: menuAttachKey,
                controller: menuController,
                child: IconButton(
                  style: ButtonStyle(padding: ButtonState.all(EdgeInsets.zero)),
                  icon: const SizedBox(
                    width: 20,
                    height: 20,
                    child: Icon(FluentIcons.more_vertical, size: 11),
                  ),
                  onPressed: () async {
                    menuController.showFlyout(
                      autoModeConfiguration: FlyoutAutoConfiguration(
                        preferredMode: FlyoutPlacementMode.bottomCenter,
                      ),
                      barrierDismissible: true,
                      dismissOnPointerMoveAway: false,
                      dismissWithEsc: true,
                      builder: (ctx) {
                        // 部门操作菜单
                        return MenuFlyout(
                          items: createDepartOperateMenuList(),
                        );
                      },
                    );
                  },
                ),
              ),
            ),
          Visibility.maintain(
            visible: isEnter,
            child: Padding(
              padding: const EdgeInsets.only(right: 10),
              child: IconButton(
                style: ButtonStyle(padding: ButtonState.all(EdgeInsets.zero)),
                icon: const SizedBox(
                  width: 20,
                  height: 20,
                  child: Icon(FluentIcons.add, size: 11),
                ),
                onPressed: () {
                  // 创建部门或组织,支持多层级部门
                  
                },
              ),
            ),
          ),
        ],
      ),
    );
  }

  /// 部门操作菜单
  List<MenuFlyoutItemBase> createDepartOperateMenuList() {
    return [
      for (int index = 0; index < departOperateList.length; index++)
        MenuFlyoutItem(
          text: Text(departOperateList[index]),
          onPressed: () {
            HLRouter.pop();
            switch (index) {
              /*case 0:
              case 1:
                // todo 菜单中部门操作项后续 》复制、移动
                
                break;*/
              case 0:
                // 重命名
                
                break;
              case 1:
                // 删除
                HLSimpleDialog(
                  context,
                  title: S.current.confirmDelete,
                  cancelBtnStr: S.current.cancel,
                  content: S.current.confirmDeleteDepartGroupTips,
                  customConfirmBtn: Button(
                    onPressed: NoDoubleClickUtils.debounce(() async {
                      // 点击删除按钮
                      
                    }),
                    child: Text(S.current.delete,
                        style: HLTextStyles.s14w400cDD3459),
                  ),
                ).showSimpleDialog();
                break;
              default:
                break;
            }
          },
        ),
    ];
  }
}

4.函数buildChildDepartList()代码如下:

buildChildDepartList(
      GroupVO? parentGroupVO, List<GroupVO> sub, int itemIndentMultiple) {
    itemIndentMultiple++;
    return Column(
      children: sub.map(
        (value) {
          return buildDepartTree(
              false, parentGroupVO, value, itemIndentMultiple);
        },
      ).toList(),
    );
  }

5.数据bean:

class GroupVO {
  String? id;
  String? name;
  List<GroupVO>? sub;
  // 部门是否选中
  bool isSelected = false;

  GroupVO({
    this.id,
    this.name,
    this.sub,
  });
}

6.提供一个带标题、内容、底部两按钮的弹窗组件HLSimpleDialog,代码如下:

/// HLSimpleDialog
/// 简单通用的dialog
///
/// 包含:标题、内容(传入contentWidget可自定义)、两个按钮(左边cancel按钮无背景色,右边confirm按钮背景有默认主题色也支持修改);
/// 支持修改按钮字体样式,支持自定义点击事件回调
class HLSimpleDialog {
  late final BuildContext context;
  late final String title;
  late final double? dialogWidth;
  late final double? dialogHeight;
  late final bool? showCloseIcon;
  late final String? content;
  late final Widget? contentWidget;
  late final Widget? customConfirmBtn;
  late final String? cancelBtnStr;
  late final String? confirmBtnStr;
  late final TextStyle? cancelBtnTextStyle;
  late final TextStyle? confirmBtnTextStyle;
  late final Color? confirmBtnBgColor;
  late final VoidCallback? cancelClick;
  late final VoidCallback? confirmClick;

  HLSimpleDialog(this.context,
      {required this.title,
      this.dialogWidth,
      this.dialogHeight,
      this.showCloseIcon,
      this.content,
      this.contentWidget,
      this.customConfirmBtn,
      this.confirmBtnStr,
      this.cancelBtnStr,
      this.cancelBtnTextStyle,
      this.confirmBtnTextStyle,
      this.confirmBtnBgColor,
      this.confirmClick,
      this.cancelClick});

  Future<bool?> showSimpleDialog() async {
    return await showDialog<bool>(
      barrierDismissible:
          (confirmBtnStr == null && cancelBtnStr == null) ? true : false,
      context: context,
      builder: (_) {
        return ContentDialog(
          constraints: BoxConstraints.expand(
            width: dialogWidth ?? 448,
            height: dialogHeight ?? 188,
          ),
          title: Row(
            children: [
              Text(
                title ?? "",
                style: HLTextStyles.s20c000000w600,
              ),
              const Spacer(),
              if (showCloseIcon == true)
                IconButton(
                  icon: Image.asset(
                    'assets/images/2.0x/close.png',
                    width: 10.9,
                    height: 10.9,
                  ),
                  onPressed: () {
                    Navigator.pop(context, true);
                  },
                )
            ],
          ),
          content: SizedBox.expand(
            child: contentWidget ??
                Text(
                  content ?? "",
                  style: HLTextStyles.s14w400,
                ),
          ),
          actions: (confirmBtnStr == null &&
                  customConfirmBtn == null &&
                  cancelBtnStr == null)
              ? null
              : [
                  const SizedBox(),
                  if (confirmBtnStr == null && customConfirmBtn == null)
                    const SizedBox(),
                  cancelBtnStr != null
                      ? Button(
                          onPressed: cancelClick ??
                              () {
                                Navigator.pop(context, false);
                              },
                          child: Text(cancelBtnStr ?? "",
                              style: cancelBtnTextStyle ??
                                  HLTextStyles.s14c000000w400),
                        )
                      : const SizedBox(),
                  customConfirmBtn ??
                      ((confirmBtnStr != null)
                          ? FilledButton(
                              style: ButtonStyle(
                                  backgroundColor: ButtonState.all(
                                      confirmBtnBgColor ?? HLColors.c0064FF)),
                              onPressed: confirmClick ??
                                  () {
                                    Navigator.pop(context, true);
                                  },
                              child: Text(confirmBtnStr ?? "",
                                  style: confirmBtnTextStyle ??
                                      HLTextStyles.s14w400cFFFFFF),
                            )
                          : const SizedBox.shrink()),
                ],
        );
      },
    );
  }
}

扩展:可追加多选框

具体效果如下:

flutter 树形列表,code,玩转flutter,笔记,flutter,android,ui,windows

可修改DepartGroupItemMouseRegionWidget中子组件排版或全新自定义head字段所对应的组件,不如定义一个ItemCheckBoxWidget组件,通过ValueListenableBuilder局部刷新勾选状态,同时勾选状态也可定义一个list做保存,下次进来加载上次的勾选状态,类似代码片段如下:文章来源地址https://www.toymoban.com/news/detail-727624.html

typedef TreeViewSelectionChanged = Future<void> Function(
    Iterable<GroupVO?> selectedItems)?;


class ItemCheckBoxWidget extends StatelessWidget {
@override
  Widget build(BuildContext context) {
    ValueNotifier<bool?> isSelected =
        ValueNotifier<bool?>(mGroupVO?.isSelected);
    var controlManagerPageVM = context.read<AccessControlManagerPageVM>();
    controlManagerPageVM.checkBoxValueNotifierList[GroupVO] =
        isSelected;
    return ValueListenableBuilder(
      valueListenable: isSelected,
      builder: (context, value, child) => SizedBox(
        width: 18,
        height: 18,
        child: Checkbox(
          style: const CheckboxThemeData(margin: EdgeInsets.all(0)),
          checked: value,
          onChanged: (value) {
            logI("ItemCheckBoxWidget - onChanged value = $value");
            if (value == null) return;
            // 更新选中项、刷新UI
            updateGroupVoSelect(controlManagerPageVM, accessControlBean, value);
            // 回调选中集合,此处由于用到vm存储集合,可无需回调直接使用vm中的
            onSelectionChanged
                ?.call(controlManagerPageVM.selectedAccessControlList);
          },
        ),
      ),
    );
  }
}

到了这里,关于flutter 自定义分层级树形组件的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • [效率工具] [VS Code] 一文玩转VSCode下的Markdown笔记

    Markdown 常用工具 : 各种云笔记优点:云备份,多设备同步,缺点是敏感字,图床不可外链等等。 本地常用IDE又sublime,Typora等等。这样造成本地会安装很多ide。像Typora后期也收费等等。所见所得等等问题,本地的markdown可能会上传到代码仓库等等需求。最终发现vscode与自己需求

    2024年02月05日
    浏览(30)
  • Flutter(九)Flutter动画和自定义组件

    Animation、Curve、Controller、Tween这四个角色,它们一起配合来完成一个完整动画 Animation Animation是抽象类,和UI渲染没有关系,功能是保存动画的插值和状态;比较常用的是Animation addListener:帧监听器中最常见的行为是改变状态后调用setState()来触发UI重建 addStatusListener:动画开始

    2024年02月10日
    浏览(39)
  • Flutter自定义可拖动组件

    2024年02月10日
    浏览(32)
  • Vue Element UI 自定义描述列表组件

    效果图  写在前面 由于vue使用的版本太低,vue element UI 的描述列表不生效,但是有时候又不想换版本的可以自定义一个描述列表。 实现哪些功能 1、每行的高度根据改行中某一列的最大高度自动撑开 2、 列宽度自动补全,避免最后一列出现残缺的情况 3、支持纯文本与HTML插槽

    2024年02月03日
    浏览(50)
  • 前端Vue自定义简单好用商品分类列表组件 侧边栏商品分类组件

    前端Vue自定义简单好用商品分类列表组件 侧边栏商品分类组件 , 下载完整代码请访问uni-app插件市场地址:https://ext.dcloud.net.cn/plugin?id=13148 效果图如下: 使用方法 HTML代码实现部分

    2024年02月10日
    浏览(36)
  • Flutter:自定义组件的上下左右弹出层

    最近要使用Flutter实现一个下拉菜单,需求就是,在当前组件下点击,其下方弹出一个菜单选项,如下图所示: 实现起来,貌似没什么障碍,在Flutter中本身就提供了弹出层PopupMenuButton组件和showMenu方法,于是开搞,代码如下: 直接使用showMenu也行,代码如下: PopupMenuButton运行

    2024年02月10日
    浏览(58)
  • 【SQL开发实战技巧】系列(三十):数仓报表场景☞树形(分层)查询如何排序?以及如何在树形查询中正确的使用where条件

    【SQL开发实战技巧】系列(一):关于SQL不得不说的那些事 【SQL开发实战技巧】系列(二):简单单表查询 【SQL开发实战技巧】系列(三):SQL排序的那些事 【SQL开发实战技巧】系列(四):从执行计划讨论UNION ALL与空字符串UNION与OR的使用注意事项 【SQL开发实战技巧】系列

    2023年04月09日
    浏览(46)
  • 自定义精美商品分类列表组件 侧边栏商品分类组件 category组件(适配vue3)

    随着技术的发展,开发的复杂度也越来越高,传统开发方式将一个系统做成了整块应用,经常出现的情况就是一个小小的改动或者一个小功能的增加可能会引起整体逻辑的修改,造成牵一发而动全身。通过组件化开发,可以有效实现单独开发,单独维护,而且他们之间可以随

    2024年02月05日
    浏览(37)
  • Flutter 笔记 | Flutter 布局组件

    布局类组件都会包含一个或多个子组件,布局类组件都是直接或间接继承 SingleChildRenderObjectWidget 和 MultiChildRenderObjectWidget 的Widget,它们一般都会有一个 child 或 children 属性用于接收子 Widget。 不同的布局类组件对子组件排列(layout)方式不同,如下表所示: Widget 说明 用途

    2024年02月06日
    浏览(37)
  • Element-ui自定义组件:可折叠按钮列表

    1、工具栏的功能按钮要超宽不换行,宽度不够折叠进”更多“按钮; 2、下拉菜单按钮和纯图标按钮默认不折叠; 3、折叠前后按钮组顺序保持不变。 1、默认展开全量按钮,并对其宽度进行缓存; 2、循环计算展开按钮的总宽度 与 容器宽度 的差值,并进行按钮的折叠与释放

    2024年02月11日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包