likes
comments
collection
share

Flutter 封装 —— 树形组件 NTree

作者站长头像
站长
· 阅读数 20

一、需求来源

最近开发遇到一种树形组件功能,周末花时间琢磨了一下,基本实现了要求: 层级缩进、折叠展开、选择功能;缩进为30,效果图下:

Flutter 封装 —— 树形组件 NTree

二、使用示例

import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/NTree/NTree.dart';

class NTreeDemo extends StatefulWidget {

  NTreeDemo({ Key? key, this.title}) : super(key: key);

  final String? title;

  @override
  _NTreeDemoState createState() => _NTreeDemoState();
}

class _NTreeDemoState extends State<NTreeDemo> {


  final _list = [
    NTreeNodeModel(
      name:'1 一级菜单',
      isExpand: true,//是否展开子项
      enabled: false,//是否可以响应事件
      items:[
        NTreeNodeModel(
          name:'1.1 二级菜单',
          isExpand: true,
          items:[
            NTreeNodeModel(
              name:'1.1.1 三级菜单',
              isExpand: true,
              items: [
                NTreeNodeModel(
                  name:'1.1.1.1 四级菜单',
                  isExpand: true,
                ),
              ]
            ),
          ]
        ),
        NTreeNodeModel(
          name:'1.2 二级菜单',
          isExpand: true,
        ),
      ]
    ),
    NTreeNodeModel(
      name:'2 一级菜单',
      // isExpand: true,
        items:[
        NTreeNodeModel(
          name:'2.1 二级菜单',
          // isExpand: true,
        ),
        NTreeNodeModel(
          name:'2.2 二级菜单',
          isExpand: false,
          items:[
            NTreeNodeModel(
              name:'2.2.1 三级菜单',
              // isExpand: true,
            ),
          ]
        ),
      ]
    ),
  ];


  @override
  void initState() {
    super.initState();
  }

   @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title ?? "$widget"),
        actions: ['done',].map((e) => TextButton(
          child: Text(e,
            style: TextStyle(color: Colors.white),
          ),
          onPressed: onPressed,)
        ).toList(),
      ),
      body: _buildBody(),
    );
  }

  _buildBody() {
    dynamic arguments = ModalRoute.of(context)!.settings.arguments;

    return CustomScrollView(
      slivers: [
        Text(arguments.toString()),
        NTree(
          list: _list,
        ),
      ].map((e) => SliverToBoxAdapter(child: e,)).toList(),
    );
  }

  onPressed(){

  }


}

三、源码

import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/enhance/enhance_expansion/enhance_expansion_tile.dart';
import 'package:flutter_templet_project/extension/color_ext.dart';

class NTree extends StatefulWidget {

  NTree({
    Key? key,
    required this.list,
    this.color = Colors.black87,
    this.iconColor = Colors.blueAccent,
    this.indent = 30,
  }) : super(key: key);

  /// 数据源
  List<NTreeNodeModel> list;
  /// 标题颜色
  Color color;
  /// 文字颜色
  Color iconColor;
  /// 层级缩进
  double indent;

  @override
  _NTreeState createState() => _NTreeState();
}

class _NTreeState extends State<NTree> {

  @override
  void initState() {
    super.initState();
  }

   @override
  Widget build(BuildContext context) {
    return Column(
      children: widget.list.map((e) {
        return Column(
          children: [
            _buildNode(
              e: e,
              color: widget.color,
              iconColor: widget.iconColor,
            ),
            if(e.isExpand)Padding(
              padding: EdgeInsets.only(left: widget.indent),
              child: NTree(
                color: widget.color,
                list: e.items,
              ),
            ),
          ],
        );
      }).toList(),
    );
  }

  Widget _buildNode({
    required NTreeNodeModel e,
    Color? color,
    Color? iconColor,
  }){
    final leadingIcon = e.isSelected ? Icon(Icons.check_box, color: iconColor,)
        : Icon(Icons.check_box_outline_blank, color: iconColor,);
    final trailing = e.items.isEmpty ? SizedBox() :
    (e.isExpand ? Icon(Icons.keyboard_arrow_down, color: iconColor,)
        : Icon(Icons.keyboard_arrow_right, color: iconColor,));

    final leading = IconButton(
      onPressed: () {
        e.isSelected = !e.isSelected;
        recursion(e: e, cb: (item) => item.isSelected = e.isSelected,);
        setState((){});
      },
      icon: leadingIcon,
    );
    return Theme(
      data: ThemeData(
        dividerColor: Colors.transparent,
      ),
      child: EnhanceExpansionTile(
        backgroundColor: ColorExt.random,
        // tilePadding: EdgeInsets.symmetric(horizontal: 100),
        // leading: leading,
        leading: leading,
        trailing: trailing,
        title: Text("${e.name}",
          style: TextStyle(
            color: color,
          ),
        ),
        // title: Text("${e.name}"),
        initiallyExpanded: e.isExpand,
        onExpansionChanged: (val){
          e.isExpand = val;
          e.onClick?.call(e);
          setState((){});
        },
        header: (onTap) => InkWell(
          onTap: (){
            onTap();
            e.isExpand = !e.isExpand;
            setState((){});
          },
          child: Container(
            padding: EdgeInsets.only(
              left: 0,
              top: 4,
              bottom: 4,
              right: 16,
            ),
            child: Row(
              children: [
                leading,
                Expanded(
                  child: Text("${e.name}",
                    style: TextStyle(
                      color: color,
                    ),
                  ),
                ),
                trailing,
              ],
            ),
          ),
        ),
      ),
    );
  }

  recursion({
     required NTreeNodeModel e,
     required void Function(NTreeNodeModel e) cb
   }) {
     cb(e);
     debugPrint("item:${e.name} ${e.isSelected}");
     e.items.forEach((item) {
       recursion(e: item, cb: cb);
    });
  }
}

class NTreeNodeModel{

  NTreeNodeModel({
    this.name,
    this.isExpand = false,
    this.isSelected = false,
    this.enabled = true,
    this.onClick,
    this.data,
    this.items = const [],
  });
  /// 标题
  String? name;
  /// 是否展开
  bool isExpand;
  /// 是否已选择
  bool isSelected;
  ///
  bool enabled;
  /// 模型
  dynamic data;
  /// 子元素
  List<NTreeNodeModel> items;
  /// 点击事件
  void Function(NTreeNodeModel e)? onClick;

  /// 遍历子树
  recursion(void Function(NTreeNodeModel e)? cb) {
    cb?.call(this);
    debugPrint("item:$name $isSelected");
    items.forEach((item) {
      recursion(cb);
    });
  }
}

总结

1、局部折叠展开不会引起外部重绘,性能比较优;
2、实现简单,任何人都可以在此基础上做二次开发,毕竟需求场景各有不同;

github