import 'package:flutter/material.dart'; /// 树节点数据接口 abstract class TreeNode { String get id; String get name; bool get isExpanded; bool get isDirectory; List get children; int get depth; IconData? get iconData; } /// 树视图配置 class TreeViewConfig { final bool lazyLoad; final bool singleSelect; final bool showCheckboxes; final bool showIcons; final Color? selectedColor; final double indentWidth; final Map icons; // 改为final const TreeViewConfig({ this.lazyLoad = false, this.singleSelect = false, this.showCheckboxes = false, this.showIcons = true, this.selectedColor, this.indentWidth = 24.0, this.icons = const {}, // 提供空Map作为默认值 }); } /// 通用树视图组件 class TreeView extends StatefulWidget { final List nodes; final TreeViewConfig config; final Function(TreeNode)? onNodeTap; final Function(TreeNode)? onNodeDoubleTap; final Function(TreeNode, bool?)? onNodeCheckChanged; final Widget Function(BuildContext, TreeNode, bool, VoidCallback)? nodeBuilder; // 新增参数 final ScrollController? scrollController; const TreeView({ super.key, required this.nodes, this.config = const TreeViewConfig(), this.onNodeTap, this.onNodeDoubleTap, this.onNodeCheckChanged, this.nodeBuilder, this.scrollController, }); @override State createState() => _TreeViewState(); } class _TreeViewState extends State { final Set _selectedIds = {}; final Set _checkedIds = {}; late ScrollController _effectiveController; @override void initState() { super.initState(); _effectiveController = widget.scrollController ?? ScrollController(); } @override void didUpdateWidget(TreeView oldWidget) { super.didUpdateWidget(oldWidget); if (widget.scrollController != oldWidget.scrollController) { if (oldWidget.scrollController == null) { _effectiveController.dispose(); } _effectiveController = widget.scrollController ?? ScrollController(); } } @override void dispose() { if (widget.scrollController == null) { _effectiveController.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return ListView.builder( controller: _effectiveController, physics: const ClampingScrollPhysics(), itemCount: _countVisibleNodes(widget.nodes), itemBuilder: (context, index) { final node = _getVisibleNode(widget.nodes, index); final isSelected = _selectedIds.contains(node.id); // 使用自定义构建器或默认构建器 return widget.nodeBuilder != null ? widget.nodeBuilder!(context, node, isSelected, () => _handleNodeTap(node)) : TreeNodeWidget( node: node, config: widget.config, isSelected: isSelected, isChecked: _checkedIds.contains(node.id), onTap: () => _handleNodeTap(node), onDoubleTap: () => widget.onNodeDoubleTap?.call(node), onCheckChanged: (value) => _handleNodeCheckChanged(node, value), ); }, ); } void _handleNodeTap(TreeNode node) { if (widget.config.singleSelect && !node.isDirectory) { // 只处理叶子节点的单选逻辑 setState(() { _selectedIds.clear(); _selectedIds.add(node.id); }); } widget.onNodeTap?.call(node); } void _handleNodeCheckChanged(TreeNode node, bool? value) { setState(() { if (value == true) { _checkedIds.add(node.id); } else { _checkedIds.remove(node.id); } }); widget.onNodeCheckChanged?.call(node, value); } int _countVisibleNodes(List nodes) { int count = 0; for (final node in nodes) { count++; if (node.isDirectory && node.isExpanded) { count += _countVisibleNodes(node.children); } } return count; } TreeNode _getVisibleNode(List nodes, int index) { int current = 0; for (final node in nodes) { if (current == index) return node; current++; if (node.isDirectory && node.isExpanded) { final childCount = _countVisibleNodes(node.children); if (index - current < childCount) { return _getVisibleNode(node.children, index - current); } current += childCount; } } throw Exception('Index out of bounds: $index'); } } class TreeNodeWidget extends StatelessWidget { final TreeNode node; final TreeViewConfig config; final bool isSelected; final bool isChecked; final VoidCallback onTap; final VoidCallback onDoubleTap; final Function(bool?)? onCheckChanged; const TreeNodeWidget({ required this.node, required this.config, required this.isSelected, required this.isChecked, required this.onTap, required this.onDoubleTap, this.onCheckChanged, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, onDoubleTap: onDoubleTap, splashColor: Colors.transparent, highlightColor: Colors.grey.withOpacity(0.1), child: Container( color: isSelected ? (config.selectedColor ?? Theme.of(context).primaryColor.withOpacity(0.1)) : Colors.transparent, padding: const EdgeInsets.symmetric(vertical: 0), child: ListTile( dense: true, visualDensity: const VisualDensity(vertical: -4), contentPadding: const EdgeInsets.symmetric(horizontal: 2), minVerticalPadding: 0, leading: _buildLeadingWidget(context), title: Text(node.name, style: Theme.of(context).textTheme.bodyMedium), trailing: config.showCheckboxes ? Checkbox( value: isChecked, onChanged: onCheckChanged, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ) : null, ), ), ); } Widget _buildLeadingWidget(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ // 缩进线 ...List.generate(node.depth, (index) { return Padding( padding: EdgeInsets.only(left: 6, right: config.indentWidth - 6), child: Container(width: 1.0, height: 32.0, color: Colors.grey[500]), ); }), // 展开/折叠图标 if (node.isDirectory) Icon( node.isExpanded ? Icons.expand_more : Icons.chevron_right, color: Colors.cyan[200], size: 20, ), // 节点图标 if (config.showIcons) node.isDirectory ? Icon( node.isExpanded ? Icons.folder_open : Icons.folder, color: Colors.cyan[500], size: 18, ) : Icon(node.iconData ?? Icons.insert_drive_file, color: Colors.amber[700], size: 20), ], ); } }