mirror of
https://github.com/flutter/flutter
synced 2024-09-12 21:01:59 +00:00
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:
parent
d86448f965
commit
9b6813f4da
104
examples/api/lib/widgets/sliver/sliver_tree.0.dart
Normal file
104
examples/api/lib/widgets/sliver/sliver_tree.0.dart
Normal 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;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
189
examples/api/lib/widgets/sliver/sliver_tree.1.dart
Normal file
189
examples/api/lib/widgets/sliver/sliver_tree.1.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
21
examples/api/test/widgets/sliver/sliver_tree.0_test.dart
Normal file
21
examples/api/test/widgets/sliver/sliver_tree.0_test.dart
Normal 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);
|
||||
});
|
||||
}
|
21
examples/api/test/widgets/sliver/sliver_tree.1_test.dart
Normal file
21
examples/api/test/widgets/sliver/sliver_tree.1_test.dart
Normal 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);
|
||||
});
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
404
packages/flutter/lib/src/rendering/sliver_tree.dart
Normal file
404
packages/flutter/lib/src/rendering/sliver_tree.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
1003
packages/flutter/lib/src/widgets/sliver_tree.dart
Normal file
1003
packages/flutter/lib/src/widgets/sliver_tree.dart
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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';
|
||||
|
|
860
packages/flutter/test/rendering/sliver_tree_test.dart
Normal file
860
packages/flutter/test/rendering/sliver_tree_test.dart
Normal 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
|
||||
});
|
||||
}
|
727
packages/flutter/test/widgets/sliver_tree_test.dart
Normal file
727
packages/flutter/test/widgets/sliver_tree_test.dart
Normal 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);
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue