flutter_treeview/lib/src/tree_node.dart
2021-07-07 10:14:27 +01:00

479 lines
14 KiB
Dart

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<TreeNode>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
late AnimationController _controller;
late Animation<double> _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>((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>((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: <Widget>[
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
? <Widget>[
Expanded(
child: _tappable,
),
arrowContainer,
]
: <Widget>[
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: <Widget>[
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<double> 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<double>(
begin: 0,
end: isEnd ? 180 : 90,
).animate(controller);
} else {
controller =
AnimationController(duration: Duration(milliseconds: 0), vsync: this);
animation = Tween<double>(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<double>(
begin: 0,
end: isEnd ? 180 : 90,
).animate(controller);
} else {
controller.duration = Duration(milliseconds: 0);
animation = Tween<double>(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,
);
},
),
);
}
}