Reverts "TreeSliver & associated classes (#147171)" (#149754)

Reverts: flutter/flutter#147171
Initiated by: QuncCccccc
Reason for reverting: tree is red due to a test in this PR
Original PR Author: Piinks

Reviewed By: {QuncCccccc, TahaTesser, bleroux}

This change reverts the following previous change:
**FYI for Reviewers:** Much of the API surface matches that of the 2D TreeView in https://github.com/flutter/packages/pull/6592. If it changes here, it should change there, and vice versa.

📜  [Design Document](https://docs.google.com/document/d/1-aFI7VjkF9yMkWpP94J8T_JREDS-M3bOak26PVehUYg/edit?usp=sharing)

This adds classes and associated callbacks and controllers for TreeSliver. Core components:
- TreeSliver
- RenderTreeSliver
- TreeSliverNode
- TreeSliverController
- TreeSliverStateMixin
- TreeSliverIndentationType

Fixes https://github.com/flutter/flutter/issues/114299

https://github.com/flutter/flutter/assets/16964204/3facd095-7262-4068-aa33-d713e2deca99

https://github.com/flutter/flutter/assets/16964204/f851ae30-8e71-45c7-82a4-9606986a5872
This commit is contained in:
auto-submit[bot] 2024-06-05 17:17:19 +00:00 committed by GitHub
parent 39472d9b61
commit 27e06569a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 0 additions and 3326 deletions

View File

@ -1,104 +0,0 @@
// 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.
import 'package:flutter/material.dart';
/// Flutter code sample for [TreeSliver].
void main() => runApp(const TreeSliverExampleApp());
class TreeSliverExampleApp extends StatelessWidget {
const TreeSliverExampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: TreeSliverExample(),
);
}
}
class TreeSliverExample extends StatefulWidget {
const TreeSliverExample({super.key});
@override
State<TreeSliverExample> createState() => _TreeSliverExampleState();
}
class _TreeSliverExampleState extends State<TreeSliverExample> {
TreeSliverNode<String>? _selectedNode;
final TreeSliverController controller = TreeSliverController();
final List<TreeSliverNode<String>> _tree = <TreeSliverNode<String>>[
TreeSliverNode<String>('First'),
TreeSliverNode<String>(
'Second',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>(
'alpha',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('uno'),
TreeSliverNode<String>('dos'),
TreeSliverNode<String>('tres'),
],
),
TreeSliverNode<String>('beta'),
TreeSliverNode<String>('kappa'),
],
),
TreeSliverNode<String>(
'Third',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('gamma'),
TreeSliverNode<String>('delta'),
TreeSliverNode<String>('epsilon'),
],
),
TreeSliverNode<String>('Fourth'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TreeSliver Demo'),
),
body: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: _tree,
controller: controller,
treeNodeBuilder: (
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle animationStyle,
) {
Widget child = GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
setState(() {
controller.toggleNode(node);
_selectedNode = node as TreeSliverNode<String>;
});
},
child: TreeSliver.defaultTreeNodeBuilder(
context,
node,
animationStyle,
),
);
if (_selectedNode == node as TreeSliverNode<String>) {
child = ColoredBox(
color: Colors.purple[100]!,
child: child,
);
}
return child;
},
),
],
),
);
}
}

View File

@ -1,189 +0,0 @@
// 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.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
/// Flutter code sample for [TreeSliver].
void main() => runApp(const TreeSliverExampleApp());
class TreeSliverExampleApp extends StatelessWidget {
const TreeSliverExampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: TreeSliverExample(),
);
}
}
class TreeSliverExample extends StatefulWidget {
const TreeSliverExample({super.key});
@override
State<TreeSliverExample> createState() => _TreeSliverExampleState();
}
class _TreeSliverExampleState extends State<TreeSliverExample> {
TreeSliverNode<String>? _selectedNode;
final List<TreeSliverNode<String>> tree = <TreeSliverNode<String>>[
TreeSliverNode<String>('README.md'),
TreeSliverNode<String>('analysis_options.yaml'),
TreeSliverNode<String>(
'lib',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>(
'src',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>(
'widgets',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('about.dart.dart'),
TreeSliverNode<String>('app.dart'),
TreeSliverNode<String>('basic.dart'),
TreeSliverNode<String>('constants.dart'),
],
),
],
),
TreeSliverNode<String>('widgets.dart'),
],
),
TreeSliverNode<String>('pubspec.lock'),
TreeSliverNode<String>('pubspec.yaml'),
TreeSliverNode<String>(
'test',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>(
'widgets',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('about_test.dart'),
TreeSliverNode<String>('app_test.dart'),
TreeSliverNode<String>('basic_test.dart'),
TreeSliverNode<String>('constants_test.dart'),
],
),
],
),
];
Widget _treeNodeBuilder(
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle toggleAnimationStyle,
) {
final bool isParentNode = node.children.isNotEmpty;
final BorderSide border = BorderSide(
width: 2,
color: Colors.purple[300]!,
);
return TreeSliver.wrapChildToToggleNode(
node: node,
child: Row(
children: <Widget>[
// Custom indentation
SizedBox(width: 10.0 * node.depth! + 8.0),
DecoratedBox(
decoration: BoxDecoration(
border: node.parent != null
? Border(left: border, bottom: border)
: null,
),
child: const SizedBox(height: 50.0, width: 20.0),
),
// Leading icon for parent nodes
if (isParentNode)
DecoratedBox(
decoration: BoxDecoration(border: Border.all()),
child: SizedBox.square(
dimension: 20.0,
child: Icon(
node.isExpanded ? Icons.remove : Icons.add,
size: 14,
),
),
),
// Spacer
const SizedBox(width: 8.0),
// Content
Text(node.content.toString()),
],
),
);
}
Widget _getTree() {
return DecoratedSliver(
decoration: BoxDecoration( border: Border.all()),
sliver: TreeSliver<String>(
tree: tree,
onNodeToggle: (TreeSliverNode<Object?> node) {
setState(() {
_selectedNode = node as TreeSliverNode<String>;
});
},
treeNodeBuilder: _treeNodeBuilder,
treeRowExtentBuilder: (
TreeSliverNode<Object?> node,
SliverLayoutDimensions layoutDimensions,
) {
// This gives more space to parent nodes.
return node.children.isNotEmpty ? 60.0 : 50.0;
},
// No internal indentation, the custom treeNodeBuilder applies its
// own indentation to decorate in the indented space.
indentation: TreeSliverIndentationType.none,
),
);
}
@override
Widget build(BuildContext context) {
// This example is assumes the full screen is available.
final Size screenSize = MediaQuery.sizeOf(context);
final List<Widget> selectedChildren = <Widget>[];
if (_selectedNode != null) {
selectedChildren.addAll(<Widget>[
const Spacer(),
Icon(
_selectedNode!.children.isEmpty
? Icons.file_open_outlined
: Icons.folder_outlined,
),
const SizedBox(height: 16.0),
Text(_selectedNode!.content),
const Spacer(),
]);
}
return Scaffold(
body: Row(children: <Widget>[
SizedBox(
width: screenSize.width / 2,
height: double.infinity,
child: CustomScrollView(
slivers: <Widget>[
_getTree(),
],
),
),
DecoratedBox(
decoration: BoxDecoration(
border: Border.all(),
),
child: SizedBox(
width: screenSize.width / 2,
height: double.infinity,
child: Center(
child: Column(
children: selectedChildren,
),
),
),
),
]),
);
}
}

View File

@ -1,21 +0,0 @@
// 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.
import 'package:flutter_api_samples/widgets/sliver/sliver_tree.0.dart'
as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Can toggle nodes in TreeSliver', (WidgetTester tester) async {
await tester.pumpWidget(
const example.TreeSliverExampleApp(),
);
expect(find.text('Second'), findsOneWidget);
expect(find.text('alpha'), findsNothing);
// Toggle tree node.
await tester.tap(find.text('Second'));
await tester.pumpAndSettle();
expect(find.text('alpha'), findsOneWidget);
});
}

View File

@ -1,21 +0,0 @@
// 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.
import 'package:flutter_api_samples/widgets/sliver/sliver_tree.1.dart'
as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Can toggle nodes in TreeSliver', (WidgetTester tester) async {
await tester.pumpWidget(
const example.TreeSliverExampleApp(),
);
expect(find.text('lib'), findsOneWidget);
expect(find.text('src'), findsNothing);
// Toggle tree node.
await tester.tap(find.text('lib'));
await tester.pumpAndSettle();
expect(find.text('src'), findsOneWidget);
});
}

View File

@ -67,7 +67,6 @@ export 'src/rendering/sliver_list.dart';
export 'src/rendering/sliver_multi_box_adaptor.dart';
export 'src/rendering/sliver_padding.dart';
export 'src/rendering/sliver_persistent_header.dart';
export 'src/rendering/sliver_tree.dart';
export 'src/rendering/stack.dart';
export 'src/rendering/table.dart';
export 'src/rendering/table_border.dart';

View File

@ -8,7 +8,6 @@ import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'object.dart';
import 'sliver.dart';
import 'sliver_fixed_extent_list.dart';
/// A delegate used by [RenderSliverMultiBoxAdaptor] to manage its children.
///

View File

@ -1,404 +0,0 @@
// 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.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'box.dart';
import 'layer.dart';
import 'object.dart';
import 'sliver.dart';
import 'sliver_fixed_extent_list.dart';
import 'sliver_multi_box_adaptor.dart';
/// Represents the animation of the children of a parent [TreeSliverNode] that
/// are animating into or out of view.
///
/// The `fromIndex` and `toIndex` identify the animating children following
/// the parent, with the `value` representing the status of the current
/// animation. The value of `toIndex` is inclusive, meaning the child at that
/// index is included in the animating segment.
///
/// Provided to [RenderTreeSliver] as part of
/// [RenderTreeSliver.activeAnimations] by [TreeSliver] to properly offset
/// animating children.
typedef TreeSliverNodesAnimation = ({
int fromIndex,
int toIndex,
double value,
});
/// Used to pass information down to [RenderTreeSliver].
class TreeSliverNodeParentData extends SliverMultiBoxAdaptorParentData {
/// The depth of the node, used by [RenderTreeSliver] to offset children by
/// by the [TreeSliverIndentationType].
int depth = 0;
}
/// The style of indentation for [TreeSliverNode]s in a [TreeSliver], as
/// handled by [RenderTreeSliver].
///
/// {@template flutter.rendering.TreeSliverIndentationType}
/// By default, the indentation is handled by [RenderTreeSliver]. Child nodes
/// are offset by the indentation specified by
/// [TreeSliverIndentationType.value] in the cross axis of the viewport. This
/// means the space allotted to the indentation will not be part of the space
/// made available to the widget returned by [TreeSliver.treeNodeBuilder].
///
/// Alternatively, the indentation can be implemented in
/// [TreeSliver.treeNodeBuilder], with the depth of the given tree row
/// accessed by [TreeSliverNode.depth]. This allows for more customization in
/// building tree rows, such as filling the indented area with decorations or
/// ink effects.
///
/// {@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}
///
/// {@endtemplate}
class TreeSliverIndentationType {
const TreeSliverIndentationType._internal(double value) : _value = value;
/// The number of pixels by which [TreeSliverNode]s will be offset according
/// to their [TreeSliverNode.depth].
double get value => _value;
final double _value;
/// The default indentation of child [TreeSliverNode]s in a [TreeSliver].
///
/// Child nodes will be offset by 10 pixels for each level in the tree.
static const TreeSliverIndentationType standard = TreeSliverIndentationType._internal(10.0);
/// Configures no offsetting of child nodes in a [TreeSliver].
///
/// Useful if the indentation is implemented in the
/// [TreeSliver.treeNodeBuilder] instead for more customization options.
///
/// Child nodes will not be offset in the tree.
static const TreeSliverIndentationType none = TreeSliverIndentationType._internal(0.0);
/// Configures a custom offset for indenting child nodes in a
/// [TreeSliver].
///
/// Child nodes will be offset by the provided number of pixels in the tree.
/// The [value] must be a non negative number.
static TreeSliverIndentationType custom(double value) {
assert(value >= 0.0);
return TreeSliverIndentationType._internal(value);
}
}
// Used during paint to delineate animating portions of the tree.
typedef _PaintSegment = ({int leadingIndex, int trailingIndex});
/// A sliver that places multiple [TreeSliverNode]s in a linear array along the
/// main axis, while staggering nodes that are animating into and out of view.
///
/// The extent of each child node is determined by the [itemExtentBuilder].
///
/// See also:
///
/// * [TreeSliver], the widget that creates and manages this render
/// object.
class RenderTreeSliver extends RenderSliverVariedExtentList {
/// Creates the render object that lays out the [TreeSliverNode]s of a
/// [TreeSliver].
RenderTreeSliver({
required super.childManager,
required super.itemExtentBuilder,
required Map<UniqueKey, TreeSliverNodesAnimation> activeAnimations,
required double indentation,
}) : _activeAnimations = activeAnimations,
_indentation = indentation;
// TODO(Piinks): There are some opportunities to cache even further as far as
// extents and layout offsets when using itemExtentBuilder from the super
// class as we do here. I want to yak shave that in a separate change.
/// The currently active [TreeSliverNode] animations.
///
/// Since the index of animating nodes can change at any time, the unique key
/// is used to track an animation of nodes across frames.
Map<UniqueKey, TreeSliverNodesAnimation> get activeAnimations => _activeAnimations;
Map<UniqueKey, TreeSliverNodesAnimation> _activeAnimations;
set activeAnimations(Map<UniqueKey, TreeSliverNodesAnimation> value) {
if (_activeAnimations == value) {
return;
}
_activeAnimations = value;
markNeedsLayout();
}
/// The number of pixels by which child nodes will be offset in the cross axis
/// based on their [TreeSliverNodeParentData.depth].
///
/// If zero, can alternatively offset children in
/// [TreeSliver.treeNodeBuilder] for more options to customize the
/// indented space.
double get indentation => _indentation;
double _indentation;
set indentation(double value) {
if (_indentation == value) {
return;
}
assert(indentation >= 0.0);
_indentation = value;
markNeedsLayout();
}
// Maps the index of parents to the animation key of their children.
final Map<int, UniqueKey> _animationLeadingIndices = <int, UniqueKey>{};
// Maps the key of child node animations to the fixed distance they are
// traversing during the animation. Determined at the start of the animation.
final Map<UniqueKey, double> _animationOffsets = <UniqueKey, double>{};
void _updateAnimationCache() {
_animationLeadingIndices.clear();
_activeAnimations.forEach((UniqueKey key, TreeSliverNodesAnimation animation) {
_animationLeadingIndices[animation.fromIndex - 1] = key;
});
// Remove any stored offsets or clip layers that are no longer actively
// animating.
_animationOffsets.removeWhere((UniqueKey key, _) => !_activeAnimations.keys.contains(key));
_clipHandles.removeWhere((UniqueKey key, LayerHandle<ClipRectLayer> handle) {
if (!_activeAnimations.keys.contains(key)) {
handle.layer = null;
return true;
}
return false;
});
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! TreeSliverNodeParentData) {
child.parentData = TreeSliverNodeParentData();
}
}
@override
void dispose() {
_clipHandles.removeWhere((UniqueKey key, LayerHandle<ClipRectLayer> handle) {
handle.layer = null;
return true;
});
super.dispose();
}
// TODO(Piinks): This should be made a public getter on the super class.
// Multiple subclasses are making use of it now, yak shave that refactor
// separately.
late SliverLayoutDimensions _currentLayoutDimensions;
@override
void performLayout() {
assert(
constraints.axisDirection == AxisDirection.down,
'TreeSliver is only supported in Viewports with an AxisDirection.down. '
'The current axis direction is: ${constraints.axisDirection}.',
);
_updateAnimationCache();
_currentLayoutDimensions = SliverLayoutDimensions(
scrollOffset: constraints.scrollOffset,
precedingScrollExtent: constraints.precedingScrollExtent,
viewportMainAxisExtent: constraints.viewportMainAxisExtent,
crossAxisExtent: constraints.crossAxisExtent,
);
super.performLayout();
}
@override
int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
// itemExtent is deprecated in the super class, we ignore it because we use
// the builder anyways.
return _getChildIndexForScrollOffset(scrollOffset);
}
@override
int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
// itemExtent is deprecated in the super class, we ignore it because we use
// the builder anyways.
return _getChildIndexForScrollOffset(scrollOffset);
}
int _getChildIndexForScrollOffset(double scrollOffset) {
if (scrollOffset == 0.0) {
return 0;
}
double position = 0.0;
int index = 0;
double totalAnimationOffset = 0.0;
double? itemExtent;
final int? childCount = childManager.estimatedChildCount;
while (position < scrollOffset) {
if (childCount != null && index > childCount - 1) {
break;
}
itemExtent = itemExtentBuilder(index, _currentLayoutDimensions);
if (itemExtent == null) {
break;
}
if (_animationLeadingIndices.keys.contains(index)) {
final UniqueKey animationKey = _animationLeadingIndices[index]!;
if (_animationOffsets[animationKey] == null) {
// We have not computed the distance this block is traversing over the
// lifetime of the animation.
_computeAnimationOffsetFor(animationKey, position);
}
// We add the offset accounting for the animation value.
totalAnimationOffset += _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value);
}
position += itemExtent - totalAnimationOffset;
++index;
}
return index - 1;
}
void _computeAnimationOffsetFor(UniqueKey key, double position) {
assert(_activeAnimations[key] != null);
final double targetPosition = constraints.scrollOffset + constraints.remainingCacheExtent;
double currentPosition = position;
final int startingIndex = _activeAnimations[key]!.fromIndex;
final int lastIndex = _activeAnimations[key]!.toIndex;
int currentIndex = startingIndex;
double totalAnimatingOffset = 0.0;
// We animate only a portion of children that would be visible/in the cache
// extent, unless all children would fit on the screen.
while (currentIndex <= lastIndex && currentPosition < targetPosition) {
final double itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions)!;
totalAnimatingOffset += itemExtent;
currentPosition += itemExtent;
currentIndex++;
}
// For the life of this animation, which affects all children following
// startingIndex (regardless of if they are a child of the triggering
// parent), they will be offset by totalAnimatingOffset * the
// animation value. This is because even though more children can be
// scrolled into view, the same distance must be maintained for a smooth
// animation.
_animationOffsets[key] = totalAnimatingOffset;
}
@override
double indexToLayoutOffset(double itemExtent, int index) {
// itemExtent is deprecated in the super class, we ignore it because we use
// the builder anyways.
double position = 0.0;
int currentIndex = 0;
double totalAnimationOffset = 0.0;
double? itemExtent;
final int? childCount = childManager.estimatedChildCount;
while (currentIndex < index) {
if (childCount != null && currentIndex > childCount - 1) {
break;
}
itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions);
if (itemExtent == null) {
break;
}
if (_animationLeadingIndices.keys.contains(currentIndex)) {
final UniqueKey animationKey = _animationLeadingIndices[currentIndex]!;
assert(_animationOffsets[animationKey] != null);
// We add the offset accounting for the animation value.
totalAnimationOffset += _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value);
}
position += itemExtent;
currentIndex++;
}
return position - totalAnimationOffset;
}
final Map<UniqueKey, LayerHandle<ClipRectLayer>> _clipHandles = <UniqueKey, LayerHandle<ClipRectLayer>>{};
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null) {
return;
}
RenderBox? nextChild = firstChild;
void paintUpTo(
int index,
RenderBox? startWith,
PaintingContext context,
Offset offset,
) {
RenderBox? child = startWith;
while (child != null && indexOf(child) <= index) {
final double mainAxisDelta = childMainAxisPosition(child);
final TreeSliverNodeParentData parentData = child.parentData! as TreeSliverNodeParentData;
final Offset childOffset = Offset(
parentData.depth * indentation,
parentData.layoutOffset!,
);
// If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child))
// does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden.
if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0) {
context.paintChild(child, childOffset);
}
child = childAfter(child);
}
nextChild = child;
}
if (_animationLeadingIndices.isEmpty) {
// There are no animations running.
paintUpTo(indexOf(lastChild!), firstChild, context, offset);
return;
}
// We are animating.
// Separate animating segments to clip for any overlap.
int leadingIndex = indexOf(firstChild!);
final List<int> animationIndices = _animationLeadingIndices.keys.toList()..sort();
final List<_PaintSegment> paintSegments = <_PaintSegment>[];
while (animationIndices.isNotEmpty) {
final int trailingIndex = animationIndices.removeAt(0);
paintSegments.add((leadingIndex: leadingIndex, trailingIndex: trailingIndex));
leadingIndex = trailingIndex + 1;
}
paintSegments.add((leadingIndex: leadingIndex, trailingIndex: indexOf(lastChild!)));
// Paint, clipping for all but the first segment.
paintUpTo(paintSegments.removeAt(0).trailingIndex, nextChild, context, offset);
// Paint the rest with clip layers.
while (paintSegments.isNotEmpty) {
final _PaintSegment segment = paintSegments.removeAt(0);
// Rect is calculated by the trailing edge of the parent (preceding
// leadingIndex), and the trailing edge of the trailing index. We cannot
// rely on the leading edge of the leading index, because it is currently
// moving.
final int parentIndex = math.max(segment.leadingIndex - 1, 0);
final double leadingOffset = indexToLayoutOffset(0.0, parentIndex)
+ (parentIndex == 0 ? 0.0 : itemExtentBuilder(parentIndex, _currentLayoutDimensions)!);
final double trailingOffset = indexToLayoutOffset(0.0, segment.trailingIndex)
+ itemExtentBuilder(segment.trailingIndex, _currentLayoutDimensions)!;
final Rect rect = Rect.fromPoints(
Offset(0.0, leadingOffset),
Offset(constraints.crossAxisExtent, trailingOffset),
);
// We use the same animation key to keep track of the clip layer, unless
// this is the odd man out segment.
final UniqueKey key = _animationLeadingIndices[parentIndex]!;
_clipHandles[key] ??= LayerHandle<ClipRectLayer>();
_clipHandles[key]!.layer = context.pushClipRect(
needsCompositing,
offset,
rect,
(PaintingContext context, Offset offset) {
paintUpTo(segment.trailingIndex, nextChild, context, offset);
},
oldLayer: _clipHandles[key]!.layer,
);
}
}
}

View File

@ -1,997 +0,0 @@
// 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.
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 != null && children.isNotEmpty && 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<T> = Widget Function(
BuildContext context,
TreeSliverNode<T> 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<T> = double Function(
TreeSliverNode<T> 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<T> = void Function(TreeSliverNode<T> 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<T> 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<T> 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<T>? 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 = 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 List<TreeSliverNode<T>> nextDepth = <TreeSliverNode<T>>[];
for (final TreeSliverNode<T> 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 List<TreeSliverNode<T>> activeNodesToExpand = <TreeSliverNode<T>>[];
_expandAll(widget.tree, activeNodesToExpand);
activeNodesToExpand.reversed.forEach(toggleNode);
}
void _expandAll(
List<TreeSliverNode<T>> tree,
List<TreeSliverNode<T>> activeNodesToExpand,
) {
for (final TreeSliverNode<T> 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 List<TreeSliverNode<T>> activeNodesToCollapse = <TreeSliverNode<T>>[];
_collapseAll(widget.tree, activeNodesToCollapse);
activeNodesToCollapse.reversed.forEach(toggleNode);
}
void _collapseAll(
List<TreeSliverNode<T>> tree,
List<TreeSliverNode<T>> activeNodesToCollapse,
) {
for (final TreeSliverNode<T> 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);
}
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]!.controller.dispose();
_currentAnimationForParent.remove(node);
_updateActiveAnimations();
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 CurvedAnimation 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().then((_) {
_unpackActiveNodes();
});
}
});
}
}
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 TreeSliverNodeParentData parentData = renderObject.parentData! as TreeSliverNodeParentData;
bool 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 SliverMultiBoxAdaptorElement 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;
}
}

View File

@ -137,7 +137,6 @@ export 'src/widgets/sliver_fill.dart';
export 'src/widgets/sliver_layout_builder.dart';
export 'src/widgets/sliver_persistent_header.dart';
export 'src/widgets/sliver_prototype_extent_list.dart';
export 'src/widgets/sliver_tree.dart';
export 'src/widgets/slotted_render_object_widget.dart';
export 'src/widgets/snapshot_widget.dart';
export 'src/widgets/spacer.dart';

View File

@ -1,860 +0,0 @@
// 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.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
List<TreeSliverNode<String>> _setUpNodes() {
return <TreeSliverNode<String>>[
TreeSliverNode<String>('First'),
TreeSliverNode<String>(
'Second',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>(
'alpha',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('uno'),
TreeSliverNode<String>('dos'),
TreeSliverNode<String>('tres'),
],
),
TreeSliverNode<String>('beta'),
TreeSliverNode<String>('kappa'),
],
),
TreeSliverNode<String>(
'Third',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('gamma'),
TreeSliverNode<String>('delta'),
TreeSliverNode<String>('epsilon'),
],
),
TreeSliverNode<String>('Fourth'),
];
}
List<TreeSliverNode<String>> treeNodes = _setUpNodes();
void main() {
testWidgets('asserts proper axis directions', (WidgetTester tester) async {
final List<Object?> exceptions = <Object?>[];
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
addTearDown(() {
FlutterError.onError = oldHandler;
});
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
reverse: true,
slivers: <Widget>[
TreeSliver<String>(tree: treeNodes),
],
),
));
FlutterError.onError = oldHandler;
expect(exceptions.isNotEmpty, isTrue);
expect(
exceptions[0].toString(),
contains('TreeSliver is only supported in Viewports with an AxisDirection.down.'),
);
exceptions.clear();
await tester.pumpWidget(Container());
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
scrollDirection: Axis.horizontal,
reverse: true,
slivers: <Widget>[
TreeSliver<String>(tree: treeNodes),
],
),
));
FlutterError.onError = oldHandler;
expect(exceptions.isNotEmpty, isTrue);
expect(
exceptions[0].toString(),
contains('TreeSliver is only supported in Viewports with an AxisDirection.down.'),
);
exceptions.clear();
await tester.pumpWidget(Container());
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: <Widget>[
TreeSliver<String>(tree: treeNodes),
],
),
));
FlutterError.onError = oldHandler;
expect(exceptions.isNotEmpty, isTrue);
expect(
exceptions[0].toString(),
contains('TreeSliver is only supported in Viewports with an AxisDirection.down.'),
);
});
testWidgets('Basic layout', (WidgetTester tester) async {
treeNodes = _setUpNodes();
// Default layout, custom indentation values, row extents.
TreeSliver<String> treeSliver = TreeSliver<String>(
tree: treeNodes,
);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[ treeSliver ],
),
));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
expect(find.text('First'), findsOneWidget);
expect(
tester.getRect(find.text('First')),
const Rect.fromLTRB(46.0, 8.0, 286.0, 32.0),
);
expect(find.text('Second'), findsOneWidget);
expect(
tester.getRect(find.text('Second')),
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
);
expect(find.text('Third'), findsOneWidget);
expect(
tester.getRect(find.text('Third')),
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
);
expect(find.text('gamma'), findsOneWidget);
expect(
tester.getRect(find.text('gamma')),
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
);
expect(find.text('delta'), findsOneWidget);
expect(
tester.getRect(find.text('delta')),
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
);
expect(find.text('epsilon'), findsOneWidget);
expect(
tester.getRect(find.text('epsilon')),
const Rect.fromLTRB(46.0, 208.0, 382.0, 232.0),
);
expect(find.text('Fourth'), findsOneWidget);
expect(
tester.getRect(find.text('Fourth')),
const Rect.fromLTRB(46.0, 248.0, 334.0, 272.0),
);
treeSliver = TreeSliver<String>(
tree: treeNodes,
indentation: TreeSliverIndentationType.none,
);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[ treeSliver ],
),
));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(46.0, 128.0))
..paragraph(offset: const Offset(46.0, 168.0))
..paragraph(offset: const Offset(46.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
expect(find.text('First'), findsOneWidget);
expect(
tester.getRect(find.text('First')),
const Rect.fromLTRB(46.0, 8.0, 286.0, 32.0),
);
expect(find.text('Second'), findsOneWidget);
expect(
tester.getRect(find.text('Second')),
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
);
expect(find.text('Third'), findsOneWidget);
expect(
tester.getRect(find.text('Third')),
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
);
expect(find.text('gamma'), findsOneWidget);
expect(
tester.getRect(find.text('gamma')),
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
);
expect(find.text('delta'), findsOneWidget);
expect(
tester.getRect(find.text('delta')),
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
);
expect(find.text('epsilon'), findsOneWidget);
expect(
tester.getRect(find.text('epsilon')),
const Rect.fromLTRB(46.0, 208.0, 382.0, 232.0),
);
expect(find.text('Fourth'), findsOneWidget);
expect(
tester.getRect(find.text('Fourth')),
const Rect.fromLTRB(46.0, 248.0, 334.0, 272.0),
);
treeSliver = TreeSliver<String>(
tree: treeNodes,
indentation: TreeSliverIndentationType.custom(50.0),
);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[ treeSliver ],
),
));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(96.0, 128.0))
..paragraph(offset: const Offset(96.0, 168.0))
..paragraph(offset: const Offset(96.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
expect(find.text('First'), findsOneWidget);
expect(
tester.getRect(find.text('First')),
const Rect.fromLTRB(46.0, 8.0, 286.0, 32.0),
);
expect(find.text('Second'), findsOneWidget);
expect(
tester.getRect(find.text('Second')),
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
);
expect(find.text('Third'), findsOneWidget);
expect(
tester.getRect(find.text('Third')),
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
);
expect(find.text('gamma'), findsOneWidget);
expect(
tester.getRect(find.text('gamma')),
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
);
expect(find.text('delta'), findsOneWidget);
expect(
tester.getRect(find.text('delta')),
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
);
expect(find.text('epsilon'), findsOneWidget);
expect(
tester.getRect(find.text('epsilon')),
const Rect.fromLTRB(46.0, 208.0, 382.0, 232.0),
);
expect(find.text('Fourth'), findsOneWidget);
expect(
tester.getRect(find.text('Fourth')),
const Rect.fromLTRB(46.0, 248.0, 334.0, 272.0),
);
treeSliver = TreeSliver<String>(
tree: treeNodes,
treeRowExtentBuilder: (_, __) => 100,
);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[ treeSliver ],
),
));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 26.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 126.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 226.0))
..paragraph(offset: const Offset(56.0, 326.0))
..paragraph(offset: const Offset(56.0, 426.0))
..paragraph(offset: const Offset(56.0, 526.0))
);
expect(find.text('First'), findsOneWidget);
expect(
tester.getRect(find.text('First')),
const Rect.fromLTRB(46.0, 26.0, 286.0, 74.0),
);
expect(find.text('Second'), findsOneWidget);
expect(
tester.getRect(find.text('Second')),
const Rect.fromLTRB(46.0, 126.0, 334.0, 174.0),
);
expect(find.text('Third'), findsOneWidget);
expect(
tester.getRect(find.text('Third')),
const Rect.fromLTRB(46.0, 226.0, 286.0, 274.0),
);
expect(find.text('gamma'), findsOneWidget);
expect(
tester.getRect(find.text('gamma')),
const Rect.fromLTRB(46.0, 326.0, 286.0, 374.0),
);
expect(find.text('delta'), findsOneWidget);
expect(
tester.getRect(find.text('delta')),
const Rect.fromLTRB(46.0, 426.0, 286.0, 474.0),
);
expect(find.text('epsilon'), findsOneWidget);
expect(
tester.getRect(find.text('epsilon')),
const Rect.fromLTRB(46.0, 526.0, 382.0, 574.0),
);
expect(find.text('Fourth'), findsNothing);
});
testWidgets('Animating node segment', (WidgetTester tester) async {
treeNodes = _setUpNodes();
TreeSliver<String> treeSliver = TreeSliver<String>(tree: treeNodes);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[ treeSliver ],
),
));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
expect(find.text('alpha'), findsNothing);
await tester.tap(find.byType(Icon).first);
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph(offset: const Offset(56.0, 8.0)) // beta animating in
..paragraph(offset: const Offset(56.0, 48.0)) // kappa animating in
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
// New nodes have been inserted into the tree, alpha
// is not visible yet.
expect(find.text('alpha'), findsNothing);
expect(find.text('beta'), findsOneWidget);
expect(
tester.getRect(find.text('beta')),
const Rect.fromLTRB(46.0, 8.0, 238.0, 32.0),
);
expect(find.text('kappa'), findsOneWidget);
expect(
tester.getRect(find.text('kappa')),
const Rect.fromLTRB(46.0, 48.0, 286.0, 72.0),
);
// Progress the animation.
await tester.pump(const Duration(milliseconds: 50));
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // alpha icon
..paragraph(offset: const Offset(56.0, 8.0)) // alpha animating in
..paragraph(offset: const Offset(56.0, 48.0)) // beta animating in
..paragraph(offset: const Offset(56.0, 88.0)) // kappa animating in
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(56.0, 248.0))
..paragraph(offset: const Offset(46.0, 288.0))
);
expect(
tester.getRect(find.text('alpha')).top.floor(),
8.0,
);
expect(find.text('beta'), findsOneWidget);
expect(
tester.getRect(find.text('beta')).top.floor(),
48.0,
);
expect(find.text('kappa'), findsOneWidget);
expect(
tester.getRect(find.text('kappa')).top.floor(),
88.0,
);
// Complete the animation
await tester.pumpAndSettle();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0)) // First
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0)) // Second
..paragraph() // alpha icon
..paragraph(offset: const Offset(56.0, 88.0)) // alpha
..paragraph(offset: const Offset(56.0, 128.0)) // beta
..paragraph(offset: const Offset(56.0, 168.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 208.0)) // Third
..paragraph(offset: const Offset(56.0, 248.0)) // gamma
..paragraph(offset: const Offset(56.0, 288.0)) // delta
..paragraph(offset: const Offset(56.0, 328.0)) // epsilon
..paragraph(offset: const Offset(46.0, 368.0)) // Fourth
);
expect(find.text('alpha'), findsOneWidget);
expect(
tester.getRect(find.text('alpha')),
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
);
expect(find.text('beta'), findsOneWidget);
expect(
tester.getRect(find.text('beta')),
const Rect.fromLTRB(46.0, 128.0, 238.0, 152.0),
);
expect(find.text('kappa'), findsOneWidget);
expect(
tester.getRect(find.text('kappa')),
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
);
// Customize the animation
treeSliver = TreeSliver<String>(
tree: treeNodes,
toggleAnimationStyle: AnimationStyle(
duration: const Duration(milliseconds: 500),
curve: Curves.bounceIn,
),
);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[ treeSliver ],
),
));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0)) // First
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0)) // Second
..paragraph() // alpha icon
..paragraph(offset: const Offset(56.0, 88.0)) // alpha
..paragraph(offset: const Offset(56.0, 128.0)) // beta
..paragraph(offset: const Offset(56.0, 168.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 208.0)) // Third
..paragraph(offset: const Offset(56.0, 248.0)) // gamma
..paragraph(offset: const Offset(56.0, 288.0)) // delta
..paragraph(offset: const Offset(56.0, 328.0)) // epsilon
..paragraph(offset: const Offset(46.0, 368.0)) // Fourth
);
// Still visible from earlier.
expect(find.text('alpha'), findsOneWidget);
expect(
tester.getRect(find.text('alpha')),
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
);
// Collapse the node now
await tester.tap(find.byType(Icon).first);
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('alpha'), findsOneWidget);
expect(
tester.getRect(find.text('alpha')).top.floor(),
-22,
);
expect(find.text('beta'), findsOneWidget);
expect(
tester.getRect(find.text('beta')).top.floor(),
18,
);
expect(find.text('kappa'), findsOneWidget);
expect(
tester.getRect(find.text('kappa')).top.floor(),
58,
);
// Progress the animation.
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('alpha'), findsOneWidget);
expect(
tester.getRect(find.text('alpha')).top.floor(),
-25,
);
expect(find.text('beta'), findsOneWidget);
expect(
tester.getRect(find.text('beta')).top.floor(),
15,
);
expect(find.text('kappa'), findsOneWidget);
expect(
tester.getRect(find.text('kappa')).top.floor(),
55.0,
);
// Complete the animation
await tester.pumpAndSettle();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
expect(find.text('alpha'), findsNothing);
// Disable the animation
treeSliver = TreeSliver<String>(
tree: treeNodes,
toggleAnimationStyle: AnimationStyle.noAnimation,
);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[ treeSliver ],
),
));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
// Not in the tree.
expect(find.text('alpha'), findsNothing);
// Collapse the node now
await tester.tap(find.byType(Icon).first);
await tester.pump();
// No animating, straight to positions.
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0)) // First
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0)) // Second
..paragraph() // alpha icon
..paragraph(offset: const Offset(56.0, 88.0)) // alpha
..paragraph(offset: const Offset(56.0, 128.0)) // beta
..paragraph(offset: const Offset(56.0, 168.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 208.0)) // Third
..paragraph(offset: const Offset(56.0, 248.0)) // gamma
..paragraph(offset: const Offset(56.0, 288.0)) // delta
..paragraph(offset: const Offset(56.0, 328.0)) // epsilon
..paragraph(offset: const Offset(46.0, 368.0)) // Fourth
);
expect(find.text('alpha'), findsOneWidget);
expect(
tester.getRect(find.text('alpha')),
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
);
expect(find.text('beta'), findsOneWidget);
expect(
tester.getRect(find.text('beta')),
const Rect.fromLTRB(46.0, 128.0, 238.0, 152.0),
);
expect(find.text('kappa'), findsOneWidget);
expect(
tester.getRect(find.text('kappa')),
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
);
});
testWidgets('Multiple animating node segments', (WidgetTester tester) async {
treeNodes = _setUpNodes();
final TreeSliver<String> treeSliver = TreeSliver<String>(tree: treeNodes);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[ treeSliver ],
),
));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
expect(find.text('Second'), findsOneWidget);
expect(find.text('alpha'), findsNothing); // Second is collapsed
expect(find.text('Third'), findsOneWidget);
expect(find.text('gamma'), findsOneWidget); // Third is expanded
expect(
tester.getRect(find.text('Second')),
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
);
expect(
tester.getRect(find.text('Third')),
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
);
expect(
tester.getRect(find.text('gamma')),
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
);
// Trigger two animations to run together.
// Collapse Third
await tester.tap(find.byType(Icon).last);
// Expand Second
await tester.tap(find.byType(Icon).first);
await tester.pump(const Duration(milliseconds: 15));
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph(offset: const Offset(56.0, 8.0)) // beta entering
..paragraph(offset: const Offset(56.0, 48.0)) // kappa entering
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
// Third is collapsing
expect(
tester.getRect(find.text('Third')),
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
);
expect(
tester.getRect(find.text('gamma')),
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
);
// Second is expanding
expect(
tester.getRect(find.text('Second')),
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
);
// beta has been added and is animating into view.
expect(
tester.getRect(find.text('beta')).top.floor(),
8.0,
);
await tester.pump(const Duration(milliseconds: 15));
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // alpha icon animating
..paragraph(offset: const Offset(56.0, -20.0)) // alpha naimating
..paragraph(offset: const Offset(56.0, 20.0)) // beta
..paragraph(offset: const Offset(56.0, 60.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 100.0)) // Third
// Children of Third are animating, but the expand and
// collapse counter each other, so their position is unchanged.
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
// Third is still collapsing. Third is sliding down
// as Seconds's children slide in, gamma is still exiting.
expect(
tester.getRect(find.text('Third')).top.floor(),
100.0,
);
// gamma appears to not have moved, this is because it is
// intersecting both animations, the positive offset of
// Second animation == the negative offset of Third
expect(
tester.getRect(find.text('gamma')),
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
);
// Second is still expanding
expect(
tester.getRect(find.text('Second')),
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
);
// alpha is still animating into view.
expect(
tester.getRect(find.text('alpha')).top.floor(),
-20.0,
);
// Progress the animation further
await tester.pump(const Duration(milliseconds: 15));
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // alpha icon animating
..paragraph(offset: const Offset(56.0, -8.0)) // alpha animating
..paragraph(offset: const Offset(56.0, 32.0)) // beta
..paragraph(offset: const Offset(56.0, 72.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 112.0)) // Third
// Children of Third are animating, but the expand and
// collapse counter each other, so their position is unchanged.
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
// Third is still collapsing. Third is sliding down
// as Seconds's children slide in, gamma is still exiting.
expect(
tester.getRect(find.text('Third')).top.floor(),
112.0,
);
// gamma appears to not have moved, this is because it is
// intersecting both animations, the positive offset of
// Second animation == the negative offset of Third
expect(
tester.getRect(find.text('gamma')),
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
);
// Second is still expanding
expect(
tester.getRect(find.text('Second')),
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
);
// alpha is still animating into view.
expect(
tester.getRect(find.text('alpha')).top.floor(),
-8.0,
);
// Complete the animations
await tester.pumpAndSettle();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(56.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0))
);
expect(
tester.getRect(find.text('Third')),
const Rect.fromLTRB(46.0, 208.0, 286.0, 232.0),
);
// gamma has left the building
expect(find.text('gamma'), findsNothing);
expect(
tester.getRect(find.text('Second')),
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
);
// alpha is in place.
expect(
tester.getRect(find.text('alpha')),
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
);
});
testWidgets('only paints visible rows', (WidgetTester tester) async {
treeNodes = _setUpNodes();
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
treeNodes = _setUpNodes();
final TreeSliver<String> treeSliver = TreeSliver<String>(
treeRowExtentBuilder: (_, __) => 200,
tree: treeNodes,
);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
controller: scrollController,
slivers: <Widget>[ treeSliver ],
),
));
await tester.pump();
expect(scrollController.position.pixels, 0.0);
expect(scrollController.position.maxScrollExtent, 800.0);
bool rowNeedsPaint(String row) {
return find.text(row).evaluate().first.renderObject!.debugNeedsPaint;
}
expect(rowNeedsPaint('First'), isFalse);
expect(rowNeedsPaint('Second'), isFalse);
expect(rowNeedsPaint('Third'), isFalse);
expect(find.text('gamma'), findsNothing); // Not visible
// Change the scroll offset
scrollController.jumpTo(200);
await tester.pump();
expect(find.text('First'), findsNothing);
expect(rowNeedsPaint('Second'), isFalse);
expect(rowNeedsPaint('Third'), isFalse);
expect(rowNeedsPaint('gamma'), isFalse); // Now visible
});
}

View File

@ -1,727 +0,0 @@
// 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.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
List<TreeSliverNode<String>> simpleNodeSet = <TreeSliverNode<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
void main() {
group('TreeSliverNode', () {
test('getters, toString', () {
final List<TreeSliverNode<String>> children = <TreeSliverNode<String>>[
TreeSliverNode<String>('child'),
];
final TreeSliverNode<String> node = TreeSliverNode<String>(
'parent',
children: children,
expanded: true,
);
expect(node.content, 'parent');
expect(node.children, children);
expect(node.isExpanded, isTrue);
expect(node.children.first.content, 'child');
expect(node.children.first.children.isEmpty, isTrue);
expect(node.children.first.isExpanded, isFalse);
// Set by TreeSliver when built for tree integrity
expect(node.depth, isNull);
expect(node.parent, isNull);
expect(node.children.first.depth, isNull);
expect(node.children.first.parent, isNull);
expect(
node.toString(),
'TreeSliverNode: parent, depth: null, parent, expanded: true',
);
expect(
node.children.first.toString(),
'TreeSliverNode: child, depth: null, leaf',
);
});
testWidgets('TreeSliverNode sets ups parent and depth properties', (WidgetTester tester) async {
final List<TreeSliverNode<String>> children = <TreeSliverNode<String>>[
TreeSliverNode<String>('child'),
];
final TreeSliverNode<String> node = TreeSliverNode<String>(
'parent',
children: children,
expanded: true,
);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: <TreeSliverNode<String>>[node],
),
],
)
));
expect(node.content, 'parent');
expect(node.children, children);
expect(node.isExpanded, isTrue);
expect(node.children.first.content, 'child');
expect(node.children.first.children.isEmpty, isTrue);
expect(node.children.first.isExpanded, isFalse);
// Set by TreeSliver when built for tree integrity
expect(node.depth, 0);
expect(node.parent, isNull);
expect(node.children.first.depth, 1);
expect(node.children.first.parent, node);
expect(
node.toString(),
'TreeSliverNode: parent, depth: root, parent, expanded: true',
);
expect(
node.children.first.toString(),
'TreeSliverNode: child, depth: 1, leaf',
);
});
});
group('TreeController', () {
setUp(() {
// Reset node conditions for each test.
simpleNodeSet = <TreeSliverNode<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
});
testWidgets('Can set controller on TreeSliver', (WidgetTester tester) async {
final TreeSliverController controller = TreeSliverController();
TreeSliverController? returnedController;
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
treeNodeBuilder: (
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle toggleAnimationStyle,
) {
returnedController ??= TreeSliverController.of(context);
return TreeSliver.defaultTreeNodeBuilder(
context,
node,
toggleAnimationStyle,
);
},
),
],
),
));
expect(controller, returnedController);
});
testWidgets('Can get default controller on TreeSliver', (WidgetTester tester) async {
TreeSliverController? returnedController;
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
treeNodeBuilder: (
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle toggleAnimationStyle,
) {
returnedController ??= TreeSliverController.maybeOf(context);
return TreeSliver.defaultTreeNodeBuilder(
context,
node,
toggleAnimationStyle,
);
},
),
],
),
));
expect(returnedController, isNotNull);
});
testWidgets('Can get node for TreeSliverNode.content', (WidgetTester tester) async {
final TreeSliverController controller = TreeSliverController();
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
),
],
),
));
expect(controller.getNodeFor('Root 0'), simpleNodeSet[0]);
});
testWidgets('Can get isExpanded for a node', (WidgetTester tester) async {
final TreeSliverController controller = TreeSliverController();
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
),
],
),
));
expect(
controller.isExpanded(simpleNodeSet[0]),
isFalse,
);
expect(
controller.isExpanded(simpleNodeSet[1]),
isTrue,
);
});
testWidgets('Can get isActive for a node', (WidgetTester tester) async {
final TreeSliverController controller = TreeSliverController();
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
),
],
),
));
expect(
controller.isActive(simpleNodeSet[0]),
isTrue,
);
expect(
controller.isActive(simpleNodeSet[1]),
isTrue,
);
// The parent 'Root 2' is not expanded, so its children are not active.
expect(
controller.isExpanded(simpleNodeSet[2]),
isFalse,
);
expect(
controller.isActive(simpleNodeSet[2].children[0]),
isFalse,
);
});
testWidgets('Can toggleNode, to collapse or expand', (WidgetTester tester) async {
final TreeSliverController controller = TreeSliverController();
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
),
],
),
));
// The parent 'Root 2' is not expanded, so its children are not active.
expect(
controller.isExpanded(simpleNodeSet[2]),
isFalse,
);
expect(
controller.isActive(simpleNodeSet[2].children[0]),
isFalse,
);
// Toggle 'Root 2' to expand it
controller.toggleNode(simpleNodeSet[2]);
expect(
controller.isExpanded(simpleNodeSet[2]),
isTrue,
);
expect(
controller.isActive(simpleNodeSet[2].children[0]),
isTrue,
);
// The parent 'Root 1' is expanded, so its children are active.
expect(
controller.isExpanded(simpleNodeSet[1]),
isTrue,
);
expect(
controller.isActive(simpleNodeSet[1].children[0]),
isTrue,
);
// Collapse 'Root 1'
controller.toggleNode(simpleNodeSet[1]);
expect(
controller.isExpanded(simpleNodeSet[1]),
isFalse,
);
expect(
controller.isActive(simpleNodeSet[1].children[0]),
isTrue,
);
// Nodes are not removed from the active list until the collapse animation
// completes. The parent expansion state also updates.
await tester.pumpAndSettle();
expect(
controller.isExpanded(simpleNodeSet[1]),
isFalse,
);
expect(
controller.isActive(simpleNodeSet[1].children[0]),
isFalse,
);
});
testWidgets('Can expandNode, then collapseAll',
(WidgetTester tester) async {
final TreeSliverController controller = TreeSliverController();
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
),
],
),
));
// The parent 'Root 2' is not expanded, so its children are not active.
expect(
controller.isExpanded(simpleNodeSet[2]),
isFalse,
);
expect(
controller.isActive(simpleNodeSet[2].children[0]),
isFalse,
);
// Expand 'Root 2'
controller.expandNode(simpleNodeSet[2]);
expect(
controller.isExpanded(simpleNodeSet[2]),
isTrue,
);
expect(
controller.isActive(simpleNodeSet[2].children[0]),
isTrue,
);
// Both parents from our simple node set are expanded.
// 'Root 1'
expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
// 'Root 2'
expect(controller.isExpanded(simpleNodeSet[2]), isTrue);
// Collapse both.
controller.collapseAll();
await tester.pumpAndSettle();
// Both parents from our simple node set have collapsed.
// 'Root 1'
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
// 'Root 2'
expect(controller.isExpanded(simpleNodeSet[2]), isFalse);
});
testWidgets('Can collapseNode, then expandAll', (WidgetTester tester) async {
final TreeSliverController controller = TreeSliverController();
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
),
],
),
));
// The parent 'Root 1' is expanded, so its children are active.
expect(
controller.isExpanded(simpleNodeSet[1]),
isTrue,
);
expect(
controller.isActive(simpleNodeSet[1].children[0]),
isTrue,
);
// Collapse 'Root 1'
controller.collapseNode(simpleNodeSet[1]);
expect(
controller.isExpanded(simpleNodeSet[1]),
isFalse,
);
expect(
controller.isActive(simpleNodeSet[1].children[0]),
isTrue,
);
// Nodes are not removed from the active list until the collapse animation
// completes.
await tester.pumpAndSettle();
expect(
controller.isActive(simpleNodeSet[1].children[0]),
isFalse,
);
// Both parents from our simple node set are collapsed.
// 'Root 1'
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
// 'Root 2'
expect(controller.isExpanded(simpleNodeSet[2]), isFalse);
// Expand both.
controller.expandAll();
// Both parents from our simple node set are expanded.
// 'Root 1'
expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
// 'Root 2'
expect(controller.isExpanded(simpleNodeSet[2]), isTrue);
});
});
test('TreeSliverIndentationType values are properly reflected', () {
double value = TreeSliverIndentationType.standard.value;
expect(value, 10.0);
value = TreeSliverIndentationType.none.value;
expect(value, 0.0);
value = TreeSliverIndentationType.custom(50.0).value;
expect(value, 50.0);
});
testWidgets('.toggleNodeWith, onNodeToggle', (WidgetTester tester) async {
final TreeSliverController controller = TreeSliverController();
// The default node builder wraps the leading icon with toggleNodeWith.
bool toggled = false;
TreeSliverNode<String>? toggledNode;
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
onNodeToggle: (TreeSliverNode<Object?> node) {
toggled = true;
toggledNode = node as TreeSliverNode<String>;
},
),
],
),
));
expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
await tester.tap(find.byType(Icon).first);
await tester.pump();
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
expect(toggled, isTrue);
expect(toggledNode, simpleNodeSet[1]);
await tester.pumpAndSettle();
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
toggled = false;
toggledNode = null;
// Use toggleNodeWith to make the whole row trigger the node state.
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
onNodeToggle: (TreeSliverNode<Object?> node) {
toggled = true;
toggledNode = node as TreeSliverNode<String>;
},
treeNodeBuilder: (
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle toggleAnimationStyle,
) {
final Duration animationDuration =
toggleAnimationStyle.duration ?? TreeSliver.defaultAnimationDuration;
final Curve animationCurve =
toggleAnimationStyle.curve ?? TreeSliver.defaultAnimationCurve;
// This makes the whole row trigger toggling.
return TreeSliver.wrapChildToToggleNode(
node: node,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: <Widget>[
// Icon for parent nodes
SizedBox.square(
dimension: 30.0,
child: node.children.isNotEmpty
? AnimatedRotation(
turns: node.isExpanded ? 0.25 : 0.0,
duration: animationDuration,
curve: animationCurve,
child: const Icon(IconData(0x25BA), size: 14),
)
: null,
),
// Spacer
const SizedBox(width: 8.0),
// Content
Text(node.content.toString()),
]),
),
);
},
),
],
),
));
// Still collapsed from earlier
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
// Tapping on the text instead of the Icon.
await tester.tap(find.text('Root 1'));
await tester.pump();
expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
expect(toggled, isTrue);
expect(toggledNode, simpleNodeSet[1]);
});
testWidgets('AnimationStyle is piped through to node builder', (WidgetTester tester) async {
AnimationStyle? style;
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
treeNodeBuilder: (
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle toggleAnimationStyle,
) {
style ??= toggleAnimationStyle;
return Text(node.content.toString());
},
),
],
),
));
// Default
expect(style, TreeSliver.defaultToggleAnimationStyle);
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
toggleAnimationStyle: AnimationStyle.noAnimation,
treeNodeBuilder: (
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle toggleAnimationStyle,
) {
style = toggleAnimationStyle;
return Text(node.content.toString());
},
),
],
),
));
expect(style, isNotNull);
expect(style!.curve, isNull);
expect(style!.duration, Duration.zero);
style = null;
await tester.pumpWidget(MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
toggleAnimationStyle: AnimationStyle(
curve: Curves.easeIn,
duration: const Duration(milliseconds: 200),
),
treeNodeBuilder: (
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle toggleAnimationStyle,
) {
style ??= toggleAnimationStyle;
return Text(node.content.toString());
},
),
],
),
));
expect(style, isNotNull);
expect(style!.curve, Curves.easeIn);
expect(style!.duration, const Duration(milliseconds: 200));
});
testWidgets('Adding more root TreeViewNodes are reflected in the tree', (WidgetTester tester) async {
simpleNodeSet = <TreeSliverNode<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
final TreeSliverController controller = TreeSliverController();
await tester.pumpWidget(MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
simpleNodeSet.add(TreeSliverNode<String>('Added root'));
});
},
),
);
},
),
));
await tester.pump();
expect(find.text('Root 0'), findsOneWidget);
expect(find.text('Root 1'), findsOneWidget);
expect(find.text('Child 1:0'), findsOneWidget);
expect(find.text('Child 1:1'), findsOneWidget);
expect(find.text('Root 2'), findsOneWidget);
expect(find.text('Child 2:0'), findsNothing);
expect(find.text('Child 2:1'), findsNothing);
expect(find.text('Root 3'), findsOneWidget);
expect(find.text('Added root'), findsNothing);
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('Root 0'), findsOneWidget);
expect(find.text('Root 1'), findsOneWidget);
expect(find.text('Child 1:0'), findsOneWidget);
expect(find.text('Child 1:1'), findsOneWidget);
expect(find.text('Root 2'), findsOneWidget);
expect(find.text('Child 2:0'), findsNothing);
expect(find.text('Child 2:1'), findsNothing);
expect(find.text('Root 3'), findsOneWidget);
// Node was added
expect(find.text('Added root'), findsOneWidget);
});
testWidgets('Adding more TreeViewNodes below the root are reflected in the tree', (WidgetTester tester) async {
simpleNodeSet = <TreeSliverNode<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
final TreeSliverController controller = TreeSliverController();
await tester.pumpWidget(MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
simpleNodeSet[1].children.add(TreeSliverNode<String>('Added child'));
});
},
),
);
},
),
));
await tester.pump();
expect(find.text('Root 0'), findsOneWidget);
expect(find.text('Root 1'), findsOneWidget);
expect(find.text('Child 1:0'), findsOneWidget);
expect(find.text('Child 1:1'), findsOneWidget);
expect(find.text('Added child'), findsNothing);
expect(find.text('Root 2'), findsOneWidget);
expect(find.text('Child 2:0'), findsNothing);
expect(find.text('Child 2:1'), findsNothing);
expect(find.text('Root 3'), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('Root 0'), findsOneWidget);
expect(find.text('Root 1'), findsOneWidget);
expect(find.text('Child 1:0'), findsOneWidget);
expect(find.text('Child 1:1'), findsOneWidget);
// Child node was added
expect(find.text('Added child'), findsOneWidget);
expect(find.text('Root 2'), findsOneWidget);
expect(find.text('Child 2:0'), findsNothing);
expect(find.text('Child 2:1'), findsNothing);
expect(find.text('Root 3'), findsOneWidget);
});
}