Reland TreeSliver (#149839)

Reland of https://github.com/flutter/flutter/pull/147171

It was reverted for a failing unit test that had passed in presubmit. Hopefully this sticks better.
Also fixed leaks in 37b10adc90
This commit is contained in:
Kate Lovett 2024-06-17 12:49:05 -07:00 committed by GitHub
parent d86448f965
commit 9b6813f4da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 3332 additions and 0 deletions

View file

@ -0,0 +1,104 @@
// 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

@ -0,0 +1,189 @@
// 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

@ -0,0 +1,21 @@
// 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

@ -0,0 +1,21 @@
// 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,6 +67,7 @@ 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,6 +8,7 @@ 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

@ -0,0 +1,404 @@
// 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` are 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 access, 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,
);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -139,6 +139,7 @@ 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_resizing_header.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

@ -0,0 +1,860 @@
// 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

@ -0,0 +1,727 @@
// 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);
});
}