import 'dart:math' show pi; import 'package:flutter/material.dart'; import 'tree_view.dart'; import 'tree_view_theme.dart'; import 'expander_theme_data.dart'; import 'models/node.dart'; const double _kBorderWidth = 0.75; /// Defines the [TreeNode] widget. /// /// This widget is used to display a tree node and its children. It requires /// a single [Node] value. It uses this node to display the state of the /// widget. It uses the [TreeViewTheme] to handle the appearance and the /// [TreeView] properties to handle to user actions. /// /// __This class should not be used directly!__ /// The [TreeView] and [TreeViewController] handlers the data and rendering /// of the nodes. class TreeNode extends StatefulWidget { /// The node object used to display the widget state final Node node; const TreeNode({Key? key, required this.node}) : super(key: key); @override _TreeNodeState createState() => _TreeNodeState(); } class _TreeNodeState extends State with SingleTickerProviderStateMixin { static final Animatable _easeInTween = CurveTween(curve: Curves.easeIn); late AnimationController _controller; late Animation _heightFactor; bool _isExpanded = false; @override void initState() { super.initState(); _controller = AnimationController(duration: Duration(milliseconds: 200), vsync: this); _heightFactor = _controller.drive(_easeInTween); _isExpanded = widget.node.expanded; if (_isExpanded) _controller.value = 1.0; } @override void didChangeDependencies() { super.didChangeDependencies(); TreeView? _treeView = TreeView.of(context); _controller.duration = _treeView!.theme.expandSpeed; } @override void dispose() { _controller.dispose(); super.dispose(); } @override void didUpdateWidget(TreeNode oldWidget) { if (widget.node.expanded != oldWidget.node.expanded) { setState(() { _isExpanded = widget.node.expanded; if (_isExpanded) { _controller.forward(); } else { _controller.reverse().then((void value) { if (!mounted) return; setState(() {}); }); } }); } else if (widget.node != oldWidget.node) { setState(() {}); } super.didUpdateWidget(oldWidget); } void _handleExpand() { TreeView? _treeView = TreeView.of(context); assert(_treeView != null, 'TreeView must exist in context'); setState(() { _isExpanded = !_isExpanded; if (_isExpanded) { _controller.forward(); } else { _controller.reverse().then((void value) { if (!mounted) return; setState(() {}); }); } }); if (_treeView!.onExpansionChanged != null) _treeView.onExpansionChanged!(widget.node.key, _isExpanded); } void _handleTap() { TreeView? _treeView = TreeView.of(context); assert(_treeView != null, 'TreeView must exist in context'); if (_treeView!.onNodeTap != null) { _treeView.onNodeTap!(widget.node.key); } } void _handleDoubleTap() { TreeView? _treeView = TreeView.of(context); assert(_treeView != null, 'TreeView must exist in context'); if (_treeView!.onNodeDoubleTap != null) { _treeView.onNodeDoubleTap!(widget.node.key); } } Widget _buildNodeExpander() { TreeView? _treeView = TreeView.of(context); assert(_treeView != null, 'TreeView must exist in context'); TreeViewTheme _theme = _treeView!.theme; return widget.node.isParent ? GestureDetector( onTap: () => _handleExpand(), child: _TreeNodeExpander( speed: _controller.duration!, expanded: widget.node.expanded, themeData: _theme.expanderTheme, ), ) : Container(width: _theme.expanderTheme.size); } Widget _buildNodeIcon() { TreeView? _treeView = TreeView.of(context); assert(_treeView != null, 'TreeView must exist in context'); TreeViewTheme _theme = _treeView!.theme; bool isSelected = _treeView.controller.selectedKey != null && _treeView.controller.selectedKey == widget.node.key; return Container( alignment: Alignment.center, width: widget.node.hasIcon ? _theme.iconTheme.size! + _theme.iconPadding : 0, child: widget.node.hasIcon ? Icon( widget.node.icon, size: _theme.iconTheme.size, color: isSelected ? _theme.colorScheme.onPrimary : _theme.iconTheme.color, ) : null, ); } Widget _buildNodeLabel() { TreeView? _treeView = TreeView.of(context); assert(_treeView != null, 'TreeView must exist in context'); TreeViewTheme _theme = _treeView!.theme; bool isSelected = _treeView.controller.selectedKey != null && _treeView.controller.selectedKey == widget.node.key; final icon = _buildNodeIcon(); return Container( padding: EdgeInsets.symmetric( vertical: _theme.verticalSpacing ?? (_theme.dense ? 10 : 15), horizontal: 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ icon, Expanded( child: Text( widget.node.label, softWrap: widget.node.isParent ? _theme.parentLabelOverflow == null : _theme.labelOverflow == null, overflow: widget.node.isParent ? _theme.parentLabelOverflow : _theme.labelOverflow, style: widget.node.isParent ? _theme.parentLabelStyle.copyWith( fontWeight: _theme.parentLabelStyle.fontWeight, color: isSelected ? _theme.colorScheme.onPrimary : _theme.parentLabelStyle.color, ) : _theme.labelStyle.copyWith( fontWeight: _theme.labelStyle.fontWeight, color: isSelected ? _theme.colorScheme.onPrimary : null, ), ), ), ], ), ); } Widget _buildNodeWidget() { TreeView? _treeView = TreeView.of(context); assert(_treeView != null, 'TreeView must exist in context'); TreeViewTheme _theme = _treeView!.theme; bool isSelected = _treeView.controller.selectedKey != null && _treeView.controller.selectedKey == widget.node.key; bool canSelectParent = _treeView.allowParentSelect; final arrowContainer = _buildNodeExpander(); final labelContainer = _treeView.nodeBuilder != null ? _treeView.nodeBuilder!(context, widget.node) : _buildNodeLabel(); Widget _tappable = _treeView.onNodeDoubleTap != null ? InkWell( onTap: _handleTap, onDoubleTap: _handleDoubleTap, child: labelContainer, ) : InkWell( onTap: _handleTap, child: labelContainer, ); if (widget.node.isParent) { if (_treeView.supportParentDoubleTap && canSelectParent) { _tappable = InkWell( onTap: canSelectParent ? _handleTap : _handleExpand, onDoubleTap: () { _handleExpand(); _handleDoubleTap(); }, child: labelContainer, ); } else if (_treeView.supportParentDoubleTap) { _tappable = InkWell( onTap: _handleExpand, onDoubleTap: _handleDoubleTap, child: labelContainer, ); } else { _tappable = InkWell( onTap: canSelectParent ? _handleTap : _handleExpand, child: labelContainer, ); } } return Container( color: isSelected ? _theme.colorScheme.primary : null, child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: _theme.expanderTheme.position == ExpanderPosition.end ? [ Expanded( child: _tappable, ), arrowContainer, ] : [ arrowContainer, Expanded( child: _tappable, ), ], ), ); } @override Widget build(BuildContext context) { TreeView? _treeView = TreeView.of(context); assert(_treeView != null, 'TreeView must exist in context'); final bool closed = (!_isExpanded || !widget.node.expanded) && _controller.isDismissed; final nodeWidget = _buildNodeWidget(); return widget.node.isParent ? AnimatedBuilder( animation: _controller.view, builder: (BuildContext context, Widget? child) { return Column( mainAxisSize: MainAxisSize.min, children: [ nodeWidget, ClipRect( child: Align( heightFactor: _heightFactor.value, child: child, ), ), ], ); }, child: closed ? null : Container( margin: EdgeInsets.only( left: _treeView!.theme.horizontalSpacing ?? _treeView.theme.iconTheme.size!), child: Column( mainAxisSize: MainAxisSize.min, children: widget.node.children.map((Node node) { return TreeNode(node: node); }).toList()), ), ) : Container( child: nodeWidget, ); } } class _TreeNodeExpander extends StatefulWidget { final ExpanderThemeData themeData; final bool expanded; final Duration _expandSpeed; const _TreeNodeExpander({ required Duration speed, required this.themeData, required this.expanded, }) : _expandSpeed = speed; @override _TreeNodeExpanderState createState() => _TreeNodeExpanderState(); } class _TreeNodeExpanderState extends State<_TreeNodeExpander> with SingleTickerProviderStateMixin { late Animation animation; late AnimationController controller; @override void initState() { bool isEnd = widget.themeData.position == ExpanderPosition.end; if (widget.themeData.type != ExpanderType.plusMinus) { controller = AnimationController( duration: widget.themeData.animated ? isEnd ? widget._expandSpeed * 0.625 : widget._expandSpeed : Duration(milliseconds: 0), vsync: this, ); animation = Tween( begin: 0, end: isEnd ? 180 : 90, ).animate(controller); } else { controller = AnimationController(duration: Duration(milliseconds: 0), vsync: this); animation = Tween(begin: 0, end: 0).animate(controller); } super.initState(); } @override void dispose() { controller.dispose(); super.dispose(); } @override void didUpdateWidget(_TreeNodeExpander oldWidget) { if (widget.themeData != oldWidget.themeData || widget.expanded != oldWidget.expanded) { bool isEnd = widget.themeData.position == ExpanderPosition.end; setState(() { if (widget.themeData.type != ExpanderType.plusMinus) { controller.duration = widget.themeData.animated ? isEnd ? widget._expandSpeed * 0.625 : widget._expandSpeed : Duration(milliseconds: 0); animation = Tween( begin: 0, end: isEnd ? 180 : 90, ).animate(controller); } else { controller.duration = Duration(milliseconds: 0); animation = Tween(begin: 0, end: 0).animate(controller); } }); } super.didUpdateWidget(oldWidget); } Color? _onColor(Color? color) { if (color != null) { if (color.computeLuminance() > 0.6) { return Colors.black; } else { return Colors.white; } } return null; } @override Widget build(BuildContext context) { IconData _arrow; double _iconSize = widget.themeData.size; double _borderWidth = 0; BoxShape _shapeBorder = BoxShape.rectangle; Color _backColor = Colors.transparent; Color? _iconColor = widget.themeData.color ?? Theme.of(context).iconTheme.color; switch (widget.themeData.modifier) { case ExpanderModifier.none: break; case ExpanderModifier.circleFilled: _shapeBorder = BoxShape.circle; _backColor = widget.themeData.color ?? Colors.black; _iconColor = _onColor(_backColor); break; case ExpanderModifier.circleOutlined: _borderWidth = _kBorderWidth; _shapeBorder = BoxShape.circle; break; case ExpanderModifier.squareFilled: _backColor = widget.themeData.color ?? Colors.black; _iconColor = _onColor(_backColor); break; case ExpanderModifier.squareOutlined: _borderWidth = _kBorderWidth; break; } switch (widget.themeData.type) { case ExpanderType.chevron: _arrow = Icons.expand_more; break; case ExpanderType.arrow: _arrow = Icons.arrow_downward; _iconSize = widget.themeData.size > 20 ? widget.themeData.size - 8 : widget.themeData.size; break; case ExpanderType.caret: _arrow = Icons.arrow_drop_down; break; case ExpanderType.plusMinus: _arrow = widget.expanded ? Icons.remove : Icons.add; break; } Icon _icon = Icon( _arrow, size: _iconSize, color: _iconColor, ); if (widget.expanded) { controller.reverse(); } else { controller.forward(); } return Container( width: widget.themeData.size + 2, height: widget.themeData.size + 2, alignment: Alignment.center, decoration: BoxDecoration( shape: _shapeBorder, border: _borderWidth == 0 ? null : Border.all( width: _borderWidth, color: widget.themeData.color ?? Colors.black, ), color: _backColor, ), child: AnimatedBuilder( animation: controller, child: _icon, builder: (context, child) { return Transform.rotate( angle: animation.value * (-pi / 180), child: child, ); }, ), ); } }