1015 lines
36 KiB
Dart
1015 lines
36 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
/// @docImport 'viewport.dart';
|
|
library;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
import 'basic.dart';
|
|
import 'framework.dart';
|
|
import 'gesture_detector.dart';
|
|
import 'icon.dart';
|
|
import 'icon_data.dart';
|
|
import 'implicit_animations.dart';
|
|
import 'scroll_delegate.dart';
|
|
import 'sliver.dart';
|
|
import 'text.dart';
|
|
import 'ticker_provider.dart';
|
|
|
|
const double _kDefaultRowExtent = 40.0;
|
|
|
|
/// A data structure for configuring children of a [TreeSliver].
|
|
///
|
|
/// A [TreeSliverNode.content] can be of any type [T], but must correspond with
|
|
/// the same type of the [TreeSliver].
|
|
///
|
|
/// The values returned by [depth], [parent] and [isExpanded] getters are
|
|
/// managed by the [TreeSliver]'s state.
|
|
class TreeSliverNode<T> {
|
|
/// Creates a [TreeSliverNode] instance for use in a [TreeSliver].
|
|
TreeSliverNode(T content, {List<TreeSliverNode<T>>? children, bool expanded = false})
|
|
: _expanded = (children?.isNotEmpty ?? false) && expanded,
|
|
_content = content,
|
|
_children = children ?? <TreeSliverNode<T>>[];
|
|
|
|
/// The subject matter of the node.
|
|
///
|
|
/// Must correspond with the type of [TreeSliver].
|
|
T get content => _content;
|
|
final T _content;
|
|
|
|
/// Other [TreeSliverNode]s that this node will be [parent] to.
|
|
///
|
|
/// Modifying the children of nodes in a [TreeSliver] will cause the tree to be
|
|
/// rebuilt so that newly added active nodes are reflected in the tree.
|
|
List<TreeSliverNode<T>> get children => _children;
|
|
final List<TreeSliverNode<T>> _children;
|
|
|
|
/// Whether or not this node is expanded in the tree.
|
|
///
|
|
/// Cannot be expanded if there are no children.
|
|
bool get isExpanded => _expanded;
|
|
bool _expanded;
|
|
|
|
/// The number of parent nodes between this node and the root of the tree.
|
|
int? get depth => _depth;
|
|
int? _depth;
|
|
|
|
/// The parent [TreeSliverNode] of this node.
|
|
TreeSliverNode<T>? get parent => _parent;
|
|
TreeSliverNode<T>? _parent;
|
|
|
|
@override
|
|
String toString() {
|
|
return 'TreeSliverNode: $content, depth: ${depth == 0 ? 'root' : depth}, '
|
|
'${children.isEmpty ? 'leaf' : 'parent, expanded: $isExpanded'}';
|
|
}
|
|
}
|
|
|
|
/// Signature for a function that creates a [Widget] to represent the given
|
|
/// [TreeSliverNode] in the [TreeSliver].
|
|
///
|
|
/// Used by [TreeSliver.treeNodeBuilder] to build rows on demand for the
|
|
/// tree.
|
|
typedef TreeSliverNodeBuilder =
|
|
Widget Function(
|
|
BuildContext context,
|
|
TreeSliverNode<Object?> node,
|
|
AnimationStyle animationStyle,
|
|
);
|
|
|
|
/// Signature for a function that returns an extent for the given
|
|
/// [TreeSliverNode] in the [TreeSliver].
|
|
///
|
|
/// Used by [TreeSliver.treeRowExtentBuilder] to size rows on demand in the
|
|
/// tree. The provided [SliverLayoutDimensions] provide information about the
|
|
/// current scroll state and [Viewport] dimensions.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [SliverVariedExtentList], which uses a similar item extent builder for
|
|
/// dynamic child sizing in the list.
|
|
typedef TreeSliverRowExtentBuilder =
|
|
double Function(TreeSliverNode<Object?> node, SliverLayoutDimensions dimensions);
|
|
|
|
/// Signature for a function that is called when a [TreeSliverNode] is toggled,
|
|
/// changing its expanded state.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [TreeSliver.onNodeToggle], for controlling node expansion
|
|
/// programmatically.
|
|
typedef TreeSliverNodeCallback = void Function(TreeSliverNode<Object?> node);
|
|
|
|
/// A mixin for classes implementing a tree structure as expected by a
|
|
/// [TreeSliverController].
|
|
///
|
|
/// Used by [TreeSliver] to implement an interface for the
|
|
/// [TreeSliverController].
|
|
///
|
|
/// This allows the [TreeSliverController] to be used in other widgets that
|
|
/// implement this interface.
|
|
///
|
|
/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode],
|
|
/// representing the type of [TreeSliverNode.content].
|
|
mixin TreeSliverStateMixin<T> {
|
|
/// Returns whether or not the given [TreeSliverNode] is expanded.
|
|
bool isExpanded(TreeSliverNode<T> node);
|
|
|
|
/// Returns whether or not the given [TreeSliverNode] is enclosed within its
|
|
/// parent [TreeSliverNode].
|
|
///
|
|
/// If the [TreeSliverNode.parent] [isExpanded] (and all its parents are
|
|
/// expanded), or this is a root node, the given node is active and this
|
|
/// method will return true. This does not reflect whether or not the node is
|
|
/// visible in the [Viewport].
|
|
bool isActive(TreeSliverNode<T> node);
|
|
|
|
/// Switches the given [TreeSliverNode]s expanded state.
|
|
///
|
|
/// May trigger an animation to reveal or hide the node's children based on
|
|
/// the [TreeSliver.toggleAnimationStyle].
|
|
///
|
|
/// If the node does not have any children, nothing will happen.
|
|
void toggleNode(TreeSliverNode<T> node);
|
|
|
|
/// Closes all parent [TreeSliverNode]s in the tree.
|
|
void collapseAll();
|
|
|
|
/// Expands all parent [TreeSliverNode]s in the tree.
|
|
void expandAll();
|
|
|
|
/// Retrieves the [TreeSliverNode] containing the associated content, if it
|
|
/// exists.
|
|
///
|
|
/// If no node exists, this will return null. This does not reflect whether
|
|
/// or not a node [isActive], or if it is visible in the viewport.
|
|
TreeSliverNode<T>? getNodeFor(T content);
|
|
|
|
/// Returns the current row index of the given [TreeSliverNode].
|
|
///
|
|
/// If the node is not currently active in the tree, meaning its parent is
|
|
/// collapsed, this will return null.
|
|
int? getActiveIndexFor(TreeSliverNode<T> node);
|
|
}
|
|
|
|
/// Enables control over the [TreeSliverNode]s of a [TreeSliver].
|
|
///
|
|
/// It can be useful to expand or collapse nodes of the tree
|
|
/// programmatically, for example to reconfigure an existing node
|
|
/// based on a system event. To do so, create a [TreeSliver]
|
|
/// with a [TreeSliverController] that's owned by a stateful widget
|
|
/// or look up the tree's automatically created [TreeSliverController]
|
|
/// with [TreeSliverController.of]
|
|
///
|
|
/// The controller's methods to expand or collapse nodes cause the
|
|
/// the [TreeSliver] to rebuild, so they may not be called from
|
|
/// a build method.
|
|
class TreeSliverController {
|
|
/// Create a controller to be used with [TreeSliver.controller].
|
|
TreeSliverController();
|
|
|
|
TreeSliverStateMixin<Object?>? _state;
|
|
|
|
/// Whether the given [TreeSliverNode] built with this controller is in an
|
|
/// expanded state.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [expandNode], which expands a given [TreeSliverNode].
|
|
/// * [collapseNode], which collapses a given [TreeSliverNode].
|
|
/// * [TreeSliver.controller] to create a TreeSliver with a controller.
|
|
bool isExpanded(TreeSliverNode<Object?> node) {
|
|
assert(_state != null);
|
|
return _state!.isExpanded(node);
|
|
}
|
|
|
|
/// Whether or not the given [TreeSliverNode] is enclosed within its parent
|
|
/// [TreeSliverNode].
|
|
///
|
|
/// If the [TreeSliverNode.parent] [isExpanded], or this is a root node, the
|
|
/// given node is active and this method will return true. This does not
|
|
/// reflect whether or not the node is visible in the [Viewport].
|
|
bool isActive(TreeSliverNode<Object?> node) {
|
|
assert(_state != null);
|
|
return _state!.isActive(node);
|
|
}
|
|
|
|
/// Returns the [TreeSliverNode] containing the associated content, if it
|
|
/// exists.
|
|
///
|
|
/// If no node exists, this will return null. This does not reflect whether
|
|
/// or not a node [isActive], or if it is currently visible in the viewport.
|
|
TreeSliverNode<Object?>? getNodeFor(Object? content) {
|
|
assert(_state != null);
|
|
return _state!.getNodeFor(content);
|
|
}
|
|
|
|
/// Switches the given [TreeSliverNode]s expanded state.
|
|
///
|
|
/// May trigger an animation to reveal or hide the node's children based on
|
|
/// the [TreeSliver.toggleAnimationStyle].
|
|
///
|
|
/// If the node does not have any children, nothing will happen.
|
|
void toggleNode(TreeSliverNode<Object?> node) {
|
|
assert(_state != null);
|
|
return _state!.toggleNode(node);
|
|
}
|
|
|
|
/// Expands the [TreeSliverNode] that was built with this controller.
|
|
///
|
|
/// If the node is already in the expanded state (see [isExpanded]), calling
|
|
/// this method has no effect.
|
|
///
|
|
/// Calling this method may cause the [TreeSliver] to rebuild, so it may
|
|
/// not be called from a build method.
|
|
///
|
|
/// Calling this method will trigger the [TreeSliver.onNodeToggle]
|
|
/// callback.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [collapseNode], which collapses the [TreeSliverNode].
|
|
/// * [isExpanded] to check whether the tile is expanded.
|
|
/// * [TreeSliver.controller] to create a TreeSliver with a controller.
|
|
void expandNode(TreeSliverNode<Object?> node) {
|
|
assert(_state != null);
|
|
if (!node.isExpanded) {
|
|
_state!.toggleNode(node);
|
|
}
|
|
}
|
|
|
|
/// Expands all parent [TreeSliverNode]s in the tree.
|
|
void expandAll() {
|
|
assert(_state != null);
|
|
_state!.expandAll();
|
|
}
|
|
|
|
/// Closes all parent [TreeSliverNode]s in the tree.
|
|
void collapseAll() {
|
|
assert(_state != null);
|
|
_state!.collapseAll();
|
|
}
|
|
|
|
/// Collapses the [TreeSliverNode] that was built with this controller.
|
|
///
|
|
/// If the node is already in the collapsed state (see [isExpanded]), calling
|
|
/// this method has no effect.
|
|
///
|
|
/// Calling this method may cause the [TreeSliver] to rebuild, so it may
|
|
/// not be called from a build method.
|
|
///
|
|
/// Calling this method will trigger the [TreeSliver.onNodeToggle]
|
|
/// callback.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [expandNode], which expands the tile.
|
|
/// * [isExpanded] to check whether the tile is expanded.
|
|
/// * [TreeSliver.controller] to create a TreeSliver with a controller.
|
|
void collapseNode(TreeSliverNode<Object?> node) {
|
|
assert(_state != null);
|
|
if (node.isExpanded) {
|
|
_state!.toggleNode(node);
|
|
}
|
|
}
|
|
|
|
/// Returns the current row index of the given [TreeSliverNode].
|
|
///
|
|
/// If the node is not currently active in the tree, meaning its parent is
|
|
/// collapsed, this will return null.
|
|
int? getActiveIndexFor(TreeSliverNode<Object?> node) {
|
|
assert(_state != null);
|
|
return _state!.getActiveIndexFor(node);
|
|
}
|
|
|
|
/// Finds the [TreeSliverController] for the closest [TreeSliver] instance
|
|
/// that encloses the given context.
|
|
///
|
|
/// If no [TreeSliver] encloses the given context, calling this
|
|
/// method will cause an assert in debug mode, and throw an
|
|
/// exception in release mode.
|
|
///
|
|
/// To return null if there is no [TreeSliver] use [maybeOf] instead.
|
|
///
|
|
/// Typical usage of the [TreeSliverController.of] function is to call it
|
|
/// from within the `build` method of a descendant of a [TreeSliver].
|
|
///
|
|
/// When the [TreeSliver] is actually created in the same `build`
|
|
/// function as the callback that refers to the controller, then the
|
|
/// `context` argument to the `build` function can't be used to find
|
|
/// the [TreeSliverController] (since it's "above" the widget
|
|
/// being returned in the widget tree). In cases like that you can
|
|
/// add a [Builder] widget, which provides a new scope with a
|
|
/// [BuildContext] that is "under" the [TreeSliver].
|
|
static TreeSliverController of(BuildContext context) {
|
|
final _TreeSliverState<Object?>? result = context
|
|
.findAncestorStateOfType<_TreeSliverState<Object?>>();
|
|
if (result != null) {
|
|
return result.controller;
|
|
}
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary(
|
|
'TreeController.of() called with a context that does not contain a '
|
|
'TreeSliver.',
|
|
),
|
|
ErrorDescription(
|
|
'No TreeSliver ancestor could be found starting from the context that '
|
|
'was passed to TreeController.of(). '
|
|
'This usually happens when the context provided is from the same '
|
|
'StatefulWidget as that whose build function actually creates the '
|
|
'TreeSliver widget being sought.',
|
|
),
|
|
ErrorHint(
|
|
'There are several ways to avoid this problem. The simplest is to use '
|
|
'a Builder to get a context that is "under" the TreeSliver.',
|
|
),
|
|
ErrorHint(
|
|
'A more efficient solution is to split your build function into '
|
|
'several widgets. This introduces a new context from which you can '
|
|
'obtain the TreeSliver. In this solution, you would have an outer '
|
|
'widget that creates the TreeSliver populated by instances of your new '
|
|
'inner widgets, and then in these inner widgets you would use '
|
|
'TreeController.of().',
|
|
),
|
|
context.describeElement('The context used was'),
|
|
]);
|
|
}
|
|
|
|
/// Finds the [TreeSliver] from the closest instance of this class that
|
|
/// encloses the given context and returns its [TreeSliverController].
|
|
///
|
|
/// If no [TreeSliver] encloses the given context then return null.
|
|
/// To throw an exception instead, use [of] instead of this function.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [of], a similar function to this one that throws if no [TreeSliver]
|
|
/// encloses the given context. Also includes some sample code in its
|
|
/// documentation.
|
|
static TreeSliverController? maybeOf(BuildContext context) {
|
|
return context.findAncestorStateOfType<_TreeSliverState<Object?>>()?.controller;
|
|
}
|
|
}
|
|
|
|
int _kDefaultSemanticIndexCallback(Widget _, int localIndex) => localIndex;
|
|
|
|
/// A widget that displays [TreeSliverNode]s that expand and collapse in a
|
|
/// vertically and horizontally scrolling [Viewport].
|
|
///
|
|
/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode],
|
|
/// representing the type of [TreeSliverNode.content].
|
|
///
|
|
/// The rows of the tree are laid out on demand by the [Viewport]'s render
|
|
/// object, using [TreeSliver.treeNodeBuilder]. This will only be called for the
|
|
/// nodes that are visible, or within the [Viewport.cacheExtent].
|
|
///
|
|
/// The [TreeSliver.treeNodeBuilder] returns the [Widget] that represents the
|
|
/// given [TreeSliverNode].
|
|
///
|
|
/// The [TreeSliver.treeRowExtentBuilder] returns a double representing the
|
|
/// extent of a given node in the main axis.
|
|
///
|
|
/// Providing a [TreeSliverController] will enable querying and controlling the
|
|
/// state of nodes in the tree.
|
|
///
|
|
/// A [TreeSliver] only supports a vertical axis direction of
|
|
/// [AxisDirection.down] and a horizontal axis direction of
|
|
/// [AxisDirection.right].
|
|
///
|
|
///{@tool dartpad}
|
|
/// This example uses a [TreeSliver] to display nodes, highlighting nodes as
|
|
/// they are selected.
|
|
///
|
|
/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.0.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This example shows a highly customized [TreeSliver] configured to
|
|
/// [TreeSliverIndentationType.none]. This allows the indentation to be handled
|
|
/// by the developer in [TreeSliver.treeNodeBuilder], where a decoration is
|
|
/// used to fill the indented space.
|
|
///
|
|
/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.1.dart **
|
|
/// {@end-tool}
|
|
class TreeSliver<T> extends StatefulWidget {
|
|
/// Creates an instance of a [TreeSliver] for displaying [TreeSliverNode]s
|
|
/// that animate expanding and collapsing of nodes.
|
|
const TreeSliver({
|
|
super.key,
|
|
required this.tree,
|
|
this.treeNodeBuilder = TreeSliver.defaultTreeNodeBuilder,
|
|
this.treeRowExtentBuilder = TreeSliver.defaultTreeRowExtentBuilder,
|
|
this.controller,
|
|
this.onNodeToggle,
|
|
this.toggleAnimationStyle,
|
|
this.indentation = TreeSliverIndentationType.standard,
|
|
this.addAutomaticKeepAlives = true,
|
|
this.addRepaintBoundaries = true,
|
|
this.addSemanticIndexes = true,
|
|
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
|
|
this.semanticIndexOffset = 0,
|
|
this.findChildIndexCallback,
|
|
});
|
|
|
|
/// The list of [TreeSliverNode]s that may be displayed in the [TreeSliver].
|
|
///
|
|
/// Beyond root nodes, whether or not a given [TreeSliverNode] is displayed
|
|
/// depends on the [TreeSliverNode.isExpanded] value of its parent. The
|
|
/// [TreeSliver] will set the [TreeSliverNode.parent] and
|
|
/// [TreeSliverNode.depth] as nodes are built on demand to ensure the
|
|
/// integrity of the tree.
|
|
final List<TreeSliverNode<T>> tree;
|
|
|
|
/// Called to build and entry of the [TreeSliver] for the given node.
|
|
///
|
|
/// By default, if this is unset, the [TreeSliver.defaultTreeNodeBuilder]
|
|
/// is used.
|
|
final TreeSliverNodeBuilder treeNodeBuilder;
|
|
|
|
/// Called to calculate the extent of the widget built for the given
|
|
/// [TreeSliverNode].
|
|
///
|
|
/// By default, if this is unset, the
|
|
/// [TreeSliver.defaultTreeRowExtentBuilder] is used.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [SliverVariedExtentList.itemExtentBuilder], a very similar method that
|
|
/// allows users to dynamically compute extents on demand.
|
|
final TreeSliverRowExtentBuilder treeRowExtentBuilder;
|
|
|
|
/// If provided, the controller can be used to expand and collapse
|
|
/// [TreeSliverNode]s, or lookup information about the current state of the
|
|
/// [TreeSliver].
|
|
final TreeSliverController? controller;
|
|
|
|
/// Called when a [TreeSliverNode] expands or collapses.
|
|
///
|
|
/// This will not be called if a [TreeSliverNode] does not have any children.
|
|
final TreeSliverNodeCallback? onNodeToggle;
|
|
|
|
/// The default [AnimationStyle] for expanding and collapsing nodes in the
|
|
/// [TreeSliver].
|
|
///
|
|
/// The default [AnimationStyle.duration] uses
|
|
/// [TreeSliver.defaultAnimationDuration], which is 150 milliseconds.
|
|
///
|
|
/// The default [AnimationStyle.curve] uses [TreeSliver.defaultAnimationCurve],
|
|
/// which is [Curves.linear].
|
|
///
|
|
/// To disable the tree animation, use [AnimationStyle.noAnimation].
|
|
final AnimationStyle? toggleAnimationStyle;
|
|
|
|
/// The number of pixels children will be offset by in the cross axis based on
|
|
/// their [TreeSliverNode.depth].
|
|
///
|
|
/// {@macro flutter.rendering.TreeSliverIndentationType}
|
|
final TreeSliverIndentationType indentation;
|
|
|
|
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
|
|
final bool addAutomaticKeepAlives;
|
|
|
|
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
|
|
final bool addRepaintBoundaries;
|
|
|
|
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addSemanticIndexes}
|
|
final bool addSemanticIndexes;
|
|
|
|
/// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexCallback}
|
|
final SemanticIndexCallback semanticIndexCallback;
|
|
|
|
/// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexOffset}
|
|
final int semanticIndexOffset;
|
|
|
|
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
|
|
final int? Function(Key)? findChildIndexCallback;
|
|
|
|
/// The default [AnimationStyle] used for node expand and collapse animations,
|
|
/// when one has not been provided in [toggleAnimationStyle].
|
|
static AnimationStyle defaultToggleAnimationStyle = const AnimationStyle(
|
|
curve: defaultAnimationCurve,
|
|
duration: defaultAnimationDuration,
|
|
);
|
|
|
|
/// A default of [Curves.linear], which is used in the tree's expanding and
|
|
/// collapsing node animation.
|
|
static const Curve defaultAnimationCurve = Curves.linear;
|
|
|
|
/// A default [Duration] of 150 milliseconds, which is used in the tree's
|
|
/// expanding and collapsing node animation.
|
|
static const Duration defaultAnimationDuration = Duration(milliseconds: 150);
|
|
|
|
/// A wrapper method for triggering the expansion or collapse of a
|
|
/// [TreeSliverNode].
|
|
///
|
|
/// Used as part of [TreeSliver.defaultTreeNodeBuilder] to wrap the leading
|
|
/// icon of parent [TreeSliverNode]s such that tapping on it triggers the
|
|
/// animation.
|
|
///
|
|
/// If defining your own [TreeSliver.treeNodeBuilder], this method can be used
|
|
/// to wrap any part, or all, of the returned widget in order to trigger the
|
|
/// change in state for the node.
|
|
static Widget wrapChildToToggleNode({
|
|
required TreeSliverNode<Object?> node,
|
|
required Widget child,
|
|
}) {
|
|
return Builder(
|
|
builder: (BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: () {
|
|
TreeSliverController.of(context).toggleNode(node);
|
|
},
|
|
child: child,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Returns the fixed default extent for rows in the tree, which is 40 pixels.
|
|
///
|
|
/// Used by [TreeSliver.treeRowExtentBuilder].
|
|
static double defaultTreeRowExtentBuilder(
|
|
TreeSliverNode<Object?> node,
|
|
SliverLayoutDimensions dimensions,
|
|
) {
|
|
return _kDefaultRowExtent;
|
|
}
|
|
|
|
/// Returns the default tree row for a given [TreeSliverNode].
|
|
///
|
|
/// Used by [TreeSliver.treeNodeBuilder].
|
|
///
|
|
/// This will return a [Row] containing the [toString] of
|
|
/// [TreeSliverNode.content]. If the [TreeSliverNode] is a parent of
|
|
/// additional nodes, a arrow icon will precede the content, and will trigger
|
|
/// an expand and collapse animation when tapped.
|
|
static Widget defaultTreeNodeBuilder(
|
|
BuildContext context,
|
|
TreeSliverNode<Object?> node,
|
|
AnimationStyle toggleAnimationStyle,
|
|
) {
|
|
final Duration animationDuration =
|
|
toggleAnimationStyle.duration ?? TreeSliver.defaultAnimationDuration;
|
|
final Curve animationCurve = toggleAnimationStyle.curve ?? TreeSliver.defaultAnimationCurve;
|
|
final int index = TreeSliverController.of(context).getActiveIndexFor(node)!;
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Row(
|
|
children: <Widget>[
|
|
// Icon for parent nodes
|
|
TreeSliver.wrapChildToToggleNode(
|
|
node: node,
|
|
child: SizedBox.square(
|
|
dimension: 30.0,
|
|
child: node.children.isNotEmpty
|
|
? AnimatedRotation(
|
|
key: ValueKey<int>(index),
|
|
turns: node.isExpanded ? 0.25 : 0.0,
|
|
duration: animationDuration,
|
|
curve: animationCurve,
|
|
// Renders a unicode right-facing arrow. >
|
|
child: const Icon(IconData(0x25BA), size: 14),
|
|
)
|
|
: null,
|
|
),
|
|
),
|
|
// Spacer
|
|
const SizedBox(width: 8.0),
|
|
// Content
|
|
Text(node.content.toString()),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
State<TreeSliver<T>> createState() => _TreeSliverState<T>();
|
|
}
|
|
|
|
// Used in _SliverTreeState for code simplicity.
|
|
typedef _AnimationRecord = ({
|
|
AnimationController controller,
|
|
CurvedAnimation animation,
|
|
UniqueKey key,
|
|
});
|
|
|
|
class _TreeSliverState<T> extends State<TreeSliver<T>>
|
|
with TickerProviderStateMixin, TreeSliverStateMixin<T> {
|
|
TreeSliverController get controller => _treeController!;
|
|
TreeSliverController? _treeController;
|
|
|
|
final List<TreeSliverNode<T>> _activeNodes = <TreeSliverNode<T>>[];
|
|
bool _shouldUnpackNode(TreeSliverNode<T> node) {
|
|
if (node.children.isEmpty) {
|
|
// No children to unpack.
|
|
return false;
|
|
}
|
|
if (_currentAnimationForParent[node] != null) {
|
|
// Whether expanding or collapsing, the child nodes are still active, so
|
|
// unpack.
|
|
return true;
|
|
}
|
|
// If we are not animating, respect node.isExpanded.
|
|
return node.isExpanded;
|
|
}
|
|
|
|
void _unpackActiveNodes({
|
|
int depth = 0,
|
|
List<TreeSliverNode<T>>? nodes,
|
|
TreeSliverNode<T>? parent,
|
|
}) {
|
|
if (nodes == null) {
|
|
_activeNodes.clear();
|
|
nodes = widget.tree;
|
|
}
|
|
for (final TreeSliverNode<T> node in nodes) {
|
|
node._depth = depth;
|
|
node._parent = parent;
|
|
_activeNodes.add(node);
|
|
if (_shouldUnpackNode(node)) {
|
|
_unpackActiveNodes(depth: depth + 1, nodes: node.children, parent: node);
|
|
}
|
|
}
|
|
}
|
|
|
|
final Map<TreeSliverNode<T>, _AnimationRecord> _currentAnimationForParent =
|
|
<TreeSliverNode<T>, _AnimationRecord>{};
|
|
final Map<UniqueKey, TreeSliverNodesAnimation> _activeAnimations =
|
|
<UniqueKey, TreeSliverNodesAnimation>{};
|
|
|
|
@override
|
|
void initState() {
|
|
_unpackActiveNodes();
|
|
assert(
|
|
widget.controller?._state == null,
|
|
'The provided TreeSliverController is already associated with another '
|
|
'TreeSliver. A TreeSliverController can only be associated with one '
|
|
'TreeSliver.',
|
|
);
|
|
_treeController = widget.controller ?? TreeSliverController();
|
|
_treeController!._state = this;
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(TreeSliver<T> oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
// Internal or provided, there is always a tree controller.
|
|
assert(_treeController != null);
|
|
if (oldWidget.controller == null && widget.controller != null) {
|
|
// A new tree controller has been provided, update and dispose of the
|
|
// internally generated one.
|
|
_treeController!._state = null;
|
|
_treeController = widget.controller;
|
|
_treeController!._state = this;
|
|
} else if (oldWidget.controller != null && widget.controller == null) {
|
|
// A tree controller had been provided, but was removed. We need to create
|
|
// one internally.
|
|
assert(oldWidget.controller == _treeController);
|
|
oldWidget.controller!._state = null;
|
|
_treeController = TreeSliverController();
|
|
_treeController!._state = this;
|
|
} else if (oldWidget.controller != widget.controller) {
|
|
assert(oldWidget.controller != null);
|
|
assert(widget.controller != null);
|
|
assert(oldWidget.controller == _treeController);
|
|
// The tree is still being provided a controller, but it has changed. Just
|
|
// update it.
|
|
_treeController!._state = null;
|
|
_treeController = widget.controller;
|
|
_treeController!._state = this;
|
|
}
|
|
// Internal or provided, there is always a tree controller.
|
|
assert(_treeController != null);
|
|
assert(_treeController!._state != null);
|
|
_unpackActiveNodes();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_treeController!._state = null;
|
|
for (final _AnimationRecord record in _currentAnimationForParent.values) {
|
|
record.animation.dispose();
|
|
record.controller.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _SliverTree(
|
|
itemCount: _activeNodes.length,
|
|
activeAnimations: _activeAnimations,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
final TreeSliverNode<T> node = _activeNodes[index];
|
|
Widget child = widget.treeNodeBuilder(
|
|
context,
|
|
node,
|
|
widget.toggleAnimationStyle ?? TreeSliver.defaultToggleAnimationStyle,
|
|
);
|
|
|
|
if (widget.addRepaintBoundaries) {
|
|
child = RepaintBoundary(child: child);
|
|
}
|
|
if (widget.addSemanticIndexes) {
|
|
final int? semanticIndex = widget.semanticIndexCallback(child, index);
|
|
if (semanticIndex != null) {
|
|
child = IndexedSemantics(
|
|
index: semanticIndex + widget.semanticIndexOffset,
|
|
child: child,
|
|
);
|
|
}
|
|
}
|
|
|
|
return _TreeNodeParentDataWidget(depth: node.depth!, child: child);
|
|
},
|
|
itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
|
|
return widget.treeRowExtentBuilder(_activeNodes[index], dimensions);
|
|
},
|
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
|
findChildIndexCallback: widget.findChildIndexCallback,
|
|
indentation: widget.indentation.value,
|
|
);
|
|
}
|
|
|
|
// TreeStateMixin Implementation
|
|
|
|
@override
|
|
bool isExpanded(TreeSliverNode<T> node) {
|
|
return _getNode(node.content, widget.tree)?.isExpanded ?? false;
|
|
}
|
|
|
|
@override
|
|
bool isActive(TreeSliverNode<T> node) => _activeNodes.contains(node);
|
|
|
|
@override
|
|
TreeSliverNode<T>? getNodeFor(T content) => _getNode(content, widget.tree);
|
|
TreeSliverNode<T>? _getNode(T content, List<TreeSliverNode<T>> tree) {
|
|
final nextDepth = <TreeSliverNode<T>>[];
|
|
for (final node in tree) {
|
|
if (node.content == content) {
|
|
return node;
|
|
}
|
|
if (node.children.isNotEmpty) {
|
|
nextDepth.addAll(node.children);
|
|
}
|
|
}
|
|
if (nextDepth.isNotEmpty) {
|
|
return _getNode(content, nextDepth);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
int? getActiveIndexFor(TreeSliverNode<T> node) {
|
|
if (_activeNodes.contains(node)) {
|
|
return _activeNodes.indexOf(node);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
void expandAll() {
|
|
final activeNodesToExpand = <TreeSliverNode<T>>[];
|
|
_expandAll(widget.tree, activeNodesToExpand);
|
|
activeNodesToExpand.reversed.forEach(toggleNode);
|
|
}
|
|
|
|
void _expandAll(List<TreeSliverNode<T>> tree, List<TreeSliverNode<T>> activeNodesToExpand) {
|
|
for (final node in tree) {
|
|
if (node.children.isNotEmpty) {
|
|
// This is a parent node.
|
|
// Expand all the children, and their children.
|
|
_expandAll(node.children, activeNodesToExpand);
|
|
if (!node.isExpanded) {
|
|
// The node itself needs to be expanded.
|
|
if (_activeNodes.contains(node)) {
|
|
// This is an active node in the tree, add to
|
|
// the list to toggle once all hidden nodes
|
|
// have been handled.
|
|
activeNodesToExpand.add(node);
|
|
} else {
|
|
// This is a hidden node. Update its expanded state.
|
|
node._expanded = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void collapseAll() {
|
|
final activeNodesToCollapse = <TreeSliverNode<T>>[];
|
|
_collapseAll(widget.tree, activeNodesToCollapse);
|
|
activeNodesToCollapse.reversed.forEach(toggleNode);
|
|
}
|
|
|
|
void _collapseAll(List<TreeSliverNode<T>> tree, List<TreeSliverNode<T>> activeNodesToCollapse) {
|
|
for (final node in tree) {
|
|
if (node.children.isNotEmpty) {
|
|
// This is a parent node.
|
|
// Collapse all the children, and their children.
|
|
_collapseAll(node.children, activeNodesToCollapse);
|
|
if (node.isExpanded) {
|
|
// The node itself needs to be collapsed.
|
|
if (_activeNodes.contains(node)) {
|
|
// This is an active node in the tree, add to
|
|
// the list to toggle once all hidden nodes
|
|
// have been handled.
|
|
activeNodesToCollapse.add(node);
|
|
} else {
|
|
// This is a hidden node. Update its expanded state.
|
|
node._expanded = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _updateActiveAnimations() {
|
|
// The indexes of various child node animations can change constantly based
|
|
// on more nodes being expanded or collapsed. Compile the indexes and their
|
|
// animations keys each time we build with an updated active node list.
|
|
_activeAnimations.clear();
|
|
for (final TreeSliverNode<T> node in _currentAnimationForParent.keys) {
|
|
final _AnimationRecord animationRecord = _currentAnimationForParent[node]!;
|
|
final int leadingChildIndex = _activeNodes.indexOf(node) + 1;
|
|
final TreeSliverNodesAnimation animatingChildren = (
|
|
fromIndex: leadingChildIndex,
|
|
toIndex: leadingChildIndex + node.children.length - 1,
|
|
value: animationRecord.animation.value,
|
|
);
|
|
_activeAnimations[animationRecord.key] = animatingChildren;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void toggleNode(TreeSliverNode<T> node) {
|
|
assert(_activeNodes.contains(node));
|
|
if (node.children.isEmpty) {
|
|
// No state to change.
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
node._expanded = !node._expanded;
|
|
if (widget.onNodeToggle != null) {
|
|
widget.onNodeToggle!(node);
|
|
}
|
|
if (_currentAnimationForParent[node] != null) {
|
|
// Dispose of the old animation if this node was already animating.
|
|
_currentAnimationForParent[node]!.animation.dispose();
|
|
}
|
|
|
|
// If animation is disabled or the duration is zero, we skip the animation
|
|
// and immediately update the active nodes. This prevents the app from freezing
|
|
// due to the tree being incorrectly updated when the animation duration is zero.
|
|
// This is because, in this case, the node's children are no longer active.
|
|
if (widget.toggleAnimationStyle == AnimationStyle.noAnimation ||
|
|
widget.toggleAnimationStyle?.duration == Duration.zero) {
|
|
_unpackActiveNodes();
|
|
return;
|
|
}
|
|
|
|
final AnimationController controller =
|
|
_currentAnimationForParent[node]?.controller ??
|
|
AnimationController(
|
|
value: node._expanded ? 0.0 : 1.0,
|
|
vsync: this,
|
|
duration:
|
|
widget.toggleAnimationStyle?.duration ?? TreeSliver.defaultAnimationDuration,
|
|
)
|
|
..addStatusListener((AnimationStatus status) {
|
|
switch (status) {
|
|
case AnimationStatus.dismissed:
|
|
case AnimationStatus.completed:
|
|
_currentAnimationForParent[node]!.animation.dispose();
|
|
_currentAnimationForParent[node]!.controller.dispose();
|
|
_currentAnimationForParent.remove(node);
|
|
_updateActiveAnimations();
|
|
// If the node is collapsing, we need to unpack the active
|
|
// nodes to remove the ones that were removed from the tree.
|
|
// This is only necessary if the node is collapsing.
|
|
if (!node._expanded) {
|
|
_unpackActiveNodes();
|
|
}
|
|
case AnimationStatus.forward:
|
|
case AnimationStatus.reverse:
|
|
}
|
|
})
|
|
..addListener(() {
|
|
setState(() {
|
|
_updateActiveAnimations();
|
|
});
|
|
});
|
|
|
|
switch (controller.status) {
|
|
case AnimationStatus.forward:
|
|
case AnimationStatus.reverse:
|
|
// We're interrupting an animation already in progress.
|
|
controller.stop();
|
|
case AnimationStatus.dismissed:
|
|
case AnimationStatus.completed:
|
|
}
|
|
|
|
final newAnimation = CurvedAnimation(
|
|
parent: controller,
|
|
curve: widget.toggleAnimationStyle?.curve ?? TreeSliver.defaultAnimationCurve,
|
|
);
|
|
_currentAnimationForParent[node] = (
|
|
controller: controller,
|
|
animation: newAnimation,
|
|
// This key helps us keep track of the lifetime of this animation in the
|
|
// render object, since the indexes can change at any time.
|
|
key: UniqueKey(),
|
|
);
|
|
switch (node._expanded) {
|
|
case true:
|
|
// Expanding
|
|
_unpackActiveNodes();
|
|
controller.forward();
|
|
case false:
|
|
// Collapsing
|
|
controller.reverse();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class _TreeNodeParentDataWidget extends ParentDataWidget<TreeSliverNodeParentData> {
|
|
const _TreeNodeParentDataWidget({required this.depth, required super.child}) : assert(depth >= 0);
|
|
|
|
final int depth;
|
|
|
|
@override
|
|
void applyParentData(RenderObject renderObject) {
|
|
final parentData = renderObject.parentData! as TreeSliverNodeParentData;
|
|
var needsLayout = false;
|
|
|
|
if (parentData.depth != depth) {
|
|
assert(depth >= 0);
|
|
parentData.depth = depth;
|
|
needsLayout = true;
|
|
}
|
|
|
|
if (needsLayout) {
|
|
renderObject.parent?.markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Type get debugTypicalAncestorWidgetClass => _SliverTree;
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(IntProperty('depth', depth));
|
|
}
|
|
}
|
|
|
|
class _SliverTree extends SliverVariedExtentList {
|
|
_SliverTree({
|
|
required NullableIndexedWidgetBuilder itemBuilder,
|
|
required super.itemExtentBuilder,
|
|
required this.activeAnimations,
|
|
required this.indentation,
|
|
ChildIndexGetter? findChildIndexCallback,
|
|
required int itemCount,
|
|
bool addAutomaticKeepAlives = true,
|
|
}) : super(
|
|
delegate: SliverChildBuilderDelegate(
|
|
itemBuilder,
|
|
findChildIndexCallback: findChildIndexCallback,
|
|
childCount: itemCount,
|
|
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
|
addRepaintBoundaries: false, // Added in the _SliverTreeState
|
|
addSemanticIndexes: false, // Added in the _SliverTreeState
|
|
),
|
|
);
|
|
|
|
final Map<UniqueKey, TreeSliverNodesAnimation> activeAnimations;
|
|
final double indentation;
|
|
|
|
@override
|
|
RenderTreeSliver createRenderObject(BuildContext context) {
|
|
final element = context as SliverMultiBoxAdaptorElement;
|
|
return RenderTreeSliver(
|
|
itemExtentBuilder: itemExtentBuilder,
|
|
activeAnimations: activeAnimations,
|
|
indentation: indentation,
|
|
childManager: element,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, RenderTreeSliver renderObject) {
|
|
renderObject
|
|
..itemExtentBuilder = itemExtentBuilder
|
|
..activeAnimations = activeAnimations
|
|
..indentation = indentation;
|
|
}
|
|
}
|