mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Reverts: flutter/flutter#147171 Initiated by: QuncCccccc Reason for reverting: tree is red due to a test in this PR Original PR Author: Piinks Reviewed By: {QuncCccccc, TahaTesser, bleroux} This change reverts the following previous change: **FYI for Reviewers:** Much of the API surface matches that of the 2D TreeView in https://github.com/flutter/packages/pull/6592. If it changes here, it should change there, and vice versa. 📜 [Design Document](https://docs.google.com/document/d/1-aFI7VjkF9yMkWpP94J8T_JREDS-M3bOak26PVehUYg/edit?usp=sharing) This adds classes and associated callbacks and controllers for TreeSliver. Core components: - TreeSliver - RenderTreeSliver - TreeSliverNode - TreeSliverController - TreeSliverStateMixin - TreeSliverIndentationType Fixes https://github.com/flutter/flutter/issues/114299 https://github.com/flutter/flutter/assets/16964204/3facd095-7262-4068-aa33-d713e2deca99 https://github.com/flutter/flutter/assets/16964204/f851ae30-8e71-45c7-82a4-9606986a5872
This commit is contained in:
parent
39472d9b61
commit
27e06569a1
@ -1,104 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [TreeSliver].
|
||||
|
||||
void main() => runApp(const TreeSliverExampleApp());
|
||||
|
||||
class TreeSliverExampleApp extends StatelessWidget {
|
||||
const TreeSliverExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: TreeSliverExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TreeSliverExample extends StatefulWidget {
|
||||
const TreeSliverExample({super.key});
|
||||
|
||||
@override
|
||||
State<TreeSliverExample> createState() => _TreeSliverExampleState();
|
||||
}
|
||||
|
||||
class _TreeSliverExampleState extends State<TreeSliverExample> {
|
||||
TreeSliverNode<String>? _selectedNode;
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
final List<TreeSliverNode<String>> _tree = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('First'),
|
||||
TreeSliverNode<String>(
|
||||
'Second',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>(
|
||||
'alpha',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('uno'),
|
||||
TreeSliverNode<String>('dos'),
|
||||
TreeSliverNode<String>('tres'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('beta'),
|
||||
TreeSliverNode<String>('kappa'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>(
|
||||
'Third',
|
||||
expanded: true,
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('gamma'),
|
||||
TreeSliverNode<String>('delta'),
|
||||
TreeSliverNode<String>('epsilon'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('Fourth'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('TreeSliver Demo'),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: _tree,
|
||||
controller: controller,
|
||||
treeNodeBuilder: (
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle animationStyle,
|
||||
) {
|
||||
Widget child = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
controller.toggleNode(node);
|
||||
_selectedNode = node as TreeSliverNode<String>;
|
||||
});
|
||||
},
|
||||
child: TreeSliver.defaultTreeNodeBuilder(
|
||||
context,
|
||||
node,
|
||||
animationStyle,
|
||||
),
|
||||
);
|
||||
if (_selectedNode == node as TreeSliverNode<String>) {
|
||||
child = ColoredBox(
|
||||
color: Colors.purple[100]!,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// Flutter code sample for [TreeSliver].
|
||||
|
||||
void main() => runApp(const TreeSliverExampleApp());
|
||||
|
||||
class TreeSliverExampleApp extends StatelessWidget {
|
||||
const TreeSliverExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: TreeSliverExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TreeSliverExample extends StatefulWidget {
|
||||
const TreeSliverExample({super.key});
|
||||
|
||||
@override
|
||||
State<TreeSliverExample> createState() => _TreeSliverExampleState();
|
||||
}
|
||||
|
||||
class _TreeSliverExampleState extends State<TreeSliverExample> {
|
||||
TreeSliverNode<String>? _selectedNode;
|
||||
final List<TreeSliverNode<String>> tree = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('README.md'),
|
||||
TreeSliverNode<String>('analysis_options.yaml'),
|
||||
TreeSliverNode<String>(
|
||||
'lib',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>(
|
||||
'src',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>(
|
||||
'widgets',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('about.dart.dart'),
|
||||
TreeSliverNode<String>('app.dart'),
|
||||
TreeSliverNode<String>('basic.dart'),
|
||||
TreeSliverNode<String>('constants.dart'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('widgets.dart'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('pubspec.lock'),
|
||||
TreeSliverNode<String>('pubspec.yaml'),
|
||||
TreeSliverNode<String>(
|
||||
'test',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>(
|
||||
'widgets',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('about_test.dart'),
|
||||
TreeSliverNode<String>('app_test.dart'),
|
||||
TreeSliverNode<String>('basic_test.dart'),
|
||||
TreeSliverNode<String>('constants_test.dart'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
Widget _treeNodeBuilder(
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle toggleAnimationStyle,
|
||||
) {
|
||||
final bool isParentNode = node.children.isNotEmpty;
|
||||
final BorderSide border = BorderSide(
|
||||
width: 2,
|
||||
color: Colors.purple[300]!,
|
||||
);
|
||||
return TreeSliver.wrapChildToToggleNode(
|
||||
node: node,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
// Custom indentation
|
||||
SizedBox(width: 10.0 * node.depth! + 8.0),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: node.parent != null
|
||||
? Border(left: border, bottom: border)
|
||||
: null,
|
||||
),
|
||||
child: const SizedBox(height: 50.0, width: 20.0),
|
||||
),
|
||||
// Leading icon for parent nodes
|
||||
if (isParentNode)
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(border: Border.all()),
|
||||
child: SizedBox.square(
|
||||
dimension: 20.0,
|
||||
child: Icon(
|
||||
node.isExpanded ? Icons.remove : Icons.add,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Spacer
|
||||
const SizedBox(width: 8.0),
|
||||
// Content
|
||||
Text(node.content.toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getTree() {
|
||||
return DecoratedSliver(
|
||||
decoration: BoxDecoration( border: Border.all()),
|
||||
sliver: TreeSliver<String>(
|
||||
tree: tree,
|
||||
onNodeToggle: (TreeSliverNode<Object?> node) {
|
||||
setState(() {
|
||||
_selectedNode = node as TreeSliverNode<String>;
|
||||
});
|
||||
},
|
||||
treeNodeBuilder: _treeNodeBuilder,
|
||||
treeRowExtentBuilder: (
|
||||
TreeSliverNode<Object?> node,
|
||||
SliverLayoutDimensions layoutDimensions,
|
||||
) {
|
||||
// This gives more space to parent nodes.
|
||||
return node.children.isNotEmpty ? 60.0 : 50.0;
|
||||
},
|
||||
// No internal indentation, the custom treeNodeBuilder applies its
|
||||
// own indentation to decorate in the indented space.
|
||||
indentation: TreeSliverIndentationType.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This example is assumes the full screen is available.
|
||||
final Size screenSize = MediaQuery.sizeOf(context);
|
||||
final List<Widget> selectedChildren = <Widget>[];
|
||||
if (_selectedNode != null) {
|
||||
selectedChildren.addAll(<Widget>[
|
||||
const Spacer(),
|
||||
Icon(
|
||||
_selectedNode!.children.isEmpty
|
||||
? Icons.file_open_outlined
|
||||
: Icons.folder_outlined,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(_selectedNode!.content),
|
||||
const Spacer(),
|
||||
]);
|
||||
}
|
||||
return Scaffold(
|
||||
body: Row(children: <Widget>[
|
||||
SizedBox(
|
||||
width: screenSize.width / 2,
|
||||
height: double.infinity,
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
_getTree(),
|
||||
],
|
||||
),
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: screenSize.width / 2,
|
||||
height: double.infinity,
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: selectedChildren,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_api_samples/widgets/sliver/sliver_tree.0.dart'
|
||||
as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Can toggle nodes in TreeSliver', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.TreeSliverExampleApp(),
|
||||
);
|
||||
expect(find.text('Second'), findsOneWidget);
|
||||
expect(find.text('alpha'), findsNothing);
|
||||
// Toggle tree node.
|
||||
await tester.tap(find.text('Second'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('alpha'), findsOneWidget);
|
||||
});
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_api_samples/widgets/sliver/sliver_tree.1.dart'
|
||||
as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Can toggle nodes in TreeSliver', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.TreeSliverExampleApp(),
|
||||
);
|
||||
expect(find.text('lib'), findsOneWidget);
|
||||
expect(find.text('src'), findsNothing);
|
||||
// Toggle tree node.
|
||||
await tester.tap(find.text('lib'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('src'), findsOneWidget);
|
||||
});
|
||||
}
|
@ -67,7 +67,6 @@ export 'src/rendering/sliver_list.dart';
|
||||
export 'src/rendering/sliver_multi_box_adaptor.dart';
|
||||
export 'src/rendering/sliver_padding.dart';
|
||||
export 'src/rendering/sliver_persistent_header.dart';
|
||||
export 'src/rendering/sliver_tree.dart';
|
||||
export 'src/rendering/stack.dart';
|
||||
export 'src/rendering/table.dart';
|
||||
export 'src/rendering/table_border.dart';
|
||||
|
@ -8,7 +8,6 @@ import 'package:vector_math/vector_math_64.dart';
|
||||
import 'box.dart';
|
||||
import 'object.dart';
|
||||
import 'sliver.dart';
|
||||
import 'sliver_fixed_extent_list.dart';
|
||||
|
||||
/// A delegate used by [RenderSliverMultiBoxAdaptor] to manage its children.
|
||||
///
|
||||
|
@ -1,404 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'box.dart';
|
||||
import 'layer.dart';
|
||||
import 'object.dart';
|
||||
import 'sliver.dart';
|
||||
import 'sliver_fixed_extent_list.dart';
|
||||
import 'sliver_multi_box_adaptor.dart';
|
||||
|
||||
/// Represents the animation of the children of a parent [TreeSliverNode] that
|
||||
/// are animating into or out of view.
|
||||
///
|
||||
/// The `fromIndex` and `toIndex` identify the animating children following
|
||||
/// the parent, with the `value` representing the status of the current
|
||||
/// animation. The value of `toIndex` is inclusive, meaning the child at that
|
||||
/// index is included in the animating segment.
|
||||
///
|
||||
/// Provided to [RenderTreeSliver] as part of
|
||||
/// [RenderTreeSliver.activeAnimations] by [TreeSliver] to properly offset
|
||||
/// animating children.
|
||||
typedef TreeSliverNodesAnimation = ({
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
double value,
|
||||
});
|
||||
|
||||
/// Used to pass information down to [RenderTreeSliver].
|
||||
class TreeSliverNodeParentData extends SliverMultiBoxAdaptorParentData {
|
||||
/// The depth of the node, used by [RenderTreeSliver] to offset children by
|
||||
/// by the [TreeSliverIndentationType].
|
||||
int depth = 0;
|
||||
}
|
||||
|
||||
/// The style of indentation for [TreeSliverNode]s in a [TreeSliver], as
|
||||
/// handled by [RenderTreeSliver].
|
||||
///
|
||||
/// {@template flutter.rendering.TreeSliverIndentationType}
|
||||
/// By default, the indentation is handled by [RenderTreeSliver]. Child nodes
|
||||
/// are offset by the indentation specified by
|
||||
/// [TreeSliverIndentationType.value] in the cross axis of the viewport. This
|
||||
/// means the space allotted to the indentation will not be part of the space
|
||||
/// made available to the widget returned by [TreeSliver.treeNodeBuilder].
|
||||
///
|
||||
/// Alternatively, the indentation can be implemented in
|
||||
/// [TreeSliver.treeNodeBuilder], with the depth of the given tree row
|
||||
/// accessed by [TreeSliverNode.depth]. This allows for more customization in
|
||||
/// building tree rows, such as filling the indented area with decorations or
|
||||
/// ink effects.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows a highly customized [TreeSliver] configured to
|
||||
/// [TreeSliverIndentationType.none]. This allows the indentation to be handled
|
||||
/// by the developer in [TreeSliver.treeNodeBuilder], where a decoration is
|
||||
/// used to fill the indented space.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class TreeSliverIndentationType {
|
||||
const TreeSliverIndentationType._internal(double value) : _value = value;
|
||||
|
||||
/// The number of pixels by which [TreeSliverNode]s will be offset according
|
||||
/// to their [TreeSliverNode.depth].
|
||||
double get value => _value;
|
||||
final double _value;
|
||||
|
||||
/// The default indentation of child [TreeSliverNode]s in a [TreeSliver].
|
||||
///
|
||||
/// Child nodes will be offset by 10 pixels for each level in the tree.
|
||||
static const TreeSliverIndentationType standard = TreeSliverIndentationType._internal(10.0);
|
||||
|
||||
/// Configures no offsetting of child nodes in a [TreeSliver].
|
||||
///
|
||||
/// Useful if the indentation is implemented in the
|
||||
/// [TreeSliver.treeNodeBuilder] instead for more customization options.
|
||||
///
|
||||
/// Child nodes will not be offset in the tree.
|
||||
static const TreeSliverIndentationType none = TreeSliverIndentationType._internal(0.0);
|
||||
|
||||
/// Configures a custom offset for indenting child nodes in a
|
||||
/// [TreeSliver].
|
||||
///
|
||||
/// Child nodes will be offset by the provided number of pixels in the tree.
|
||||
/// The [value] must be a non negative number.
|
||||
static TreeSliverIndentationType custom(double value) {
|
||||
assert(value >= 0.0);
|
||||
return TreeSliverIndentationType._internal(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Used during paint to delineate animating portions of the tree.
|
||||
typedef _PaintSegment = ({int leadingIndex, int trailingIndex});
|
||||
|
||||
/// A sliver that places multiple [TreeSliverNode]s in a linear array along the
|
||||
/// main axis, while staggering nodes that are animating into and out of view.
|
||||
///
|
||||
/// The extent of each child node is determined by the [itemExtentBuilder].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TreeSliver], the widget that creates and manages this render
|
||||
/// object.
|
||||
class RenderTreeSliver extends RenderSliverVariedExtentList {
|
||||
/// Creates the render object that lays out the [TreeSliverNode]s of a
|
||||
/// [TreeSliver].
|
||||
RenderTreeSliver({
|
||||
required super.childManager,
|
||||
required super.itemExtentBuilder,
|
||||
required Map<UniqueKey, TreeSliverNodesAnimation> activeAnimations,
|
||||
required double indentation,
|
||||
}) : _activeAnimations = activeAnimations,
|
||||
_indentation = indentation;
|
||||
|
||||
// TODO(Piinks): There are some opportunities to cache even further as far as
|
||||
// extents and layout offsets when using itemExtentBuilder from the super
|
||||
// class as we do here. I want to yak shave that in a separate change.
|
||||
|
||||
/// The currently active [TreeSliverNode] animations.
|
||||
///
|
||||
/// Since the index of animating nodes can change at any time, the unique key
|
||||
/// is used to track an animation of nodes across frames.
|
||||
Map<UniqueKey, TreeSliverNodesAnimation> get activeAnimations => _activeAnimations;
|
||||
Map<UniqueKey, TreeSliverNodesAnimation> _activeAnimations;
|
||||
set activeAnimations(Map<UniqueKey, TreeSliverNodesAnimation> value) {
|
||||
if (_activeAnimations == value) {
|
||||
return;
|
||||
}
|
||||
_activeAnimations = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
/// The number of pixels by which child nodes will be offset in the cross axis
|
||||
/// based on their [TreeSliverNodeParentData.depth].
|
||||
///
|
||||
/// If zero, can alternatively offset children in
|
||||
/// [TreeSliver.treeNodeBuilder] for more options to customize the
|
||||
/// indented space.
|
||||
double get indentation => _indentation;
|
||||
double _indentation;
|
||||
set indentation(double value) {
|
||||
if (_indentation == value) {
|
||||
return;
|
||||
}
|
||||
assert(indentation >= 0.0);
|
||||
_indentation = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
// Maps the index of parents to the animation key of their children.
|
||||
final Map<int, UniqueKey> _animationLeadingIndices = <int, UniqueKey>{};
|
||||
// Maps the key of child node animations to the fixed distance they are
|
||||
// traversing during the animation. Determined at the start of the animation.
|
||||
final Map<UniqueKey, double> _animationOffsets = <UniqueKey, double>{};
|
||||
void _updateAnimationCache() {
|
||||
_animationLeadingIndices.clear();
|
||||
_activeAnimations.forEach((UniqueKey key, TreeSliverNodesAnimation animation) {
|
||||
_animationLeadingIndices[animation.fromIndex - 1] = key;
|
||||
});
|
||||
// Remove any stored offsets or clip layers that are no longer actively
|
||||
// animating.
|
||||
_animationOffsets.removeWhere((UniqueKey key, _) => !_activeAnimations.keys.contains(key));
|
||||
_clipHandles.removeWhere((UniqueKey key, LayerHandle<ClipRectLayer> handle) {
|
||||
if (!_activeAnimations.keys.contains(key)) {
|
||||
handle.layer = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! TreeSliverNodeParentData) {
|
||||
child.parentData = TreeSliverNodeParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_clipHandles.removeWhere((UniqueKey key, LayerHandle<ClipRectLayer> handle) {
|
||||
handle.layer = null;
|
||||
return true;
|
||||
});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// TODO(Piinks): This should be made a public getter on the super class.
|
||||
// Multiple subclasses are making use of it now, yak shave that refactor
|
||||
// separately.
|
||||
late SliverLayoutDimensions _currentLayoutDimensions;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
assert(
|
||||
constraints.axisDirection == AxisDirection.down,
|
||||
'TreeSliver is only supported in Viewports with an AxisDirection.down. '
|
||||
'The current axis direction is: ${constraints.axisDirection}.',
|
||||
);
|
||||
_updateAnimationCache();
|
||||
_currentLayoutDimensions = SliverLayoutDimensions(
|
||||
scrollOffset: constraints.scrollOffset,
|
||||
precedingScrollExtent: constraints.precedingScrollExtent,
|
||||
viewportMainAxisExtent: constraints.viewportMainAxisExtent,
|
||||
crossAxisExtent: constraints.crossAxisExtent,
|
||||
);
|
||||
super.performLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
|
||||
// itemExtent is deprecated in the super class, we ignore it because we use
|
||||
// the builder anyways.
|
||||
return _getChildIndexForScrollOffset(scrollOffset);
|
||||
}
|
||||
|
||||
@override
|
||||
int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
|
||||
// itemExtent is deprecated in the super class, we ignore it because we use
|
||||
// the builder anyways.
|
||||
return _getChildIndexForScrollOffset(scrollOffset);
|
||||
}
|
||||
|
||||
int _getChildIndexForScrollOffset(double scrollOffset) {
|
||||
if (scrollOffset == 0.0) {
|
||||
return 0;
|
||||
}
|
||||
double position = 0.0;
|
||||
int index = 0;
|
||||
double totalAnimationOffset = 0.0;
|
||||
double? itemExtent;
|
||||
final int? childCount = childManager.estimatedChildCount;
|
||||
while (position < scrollOffset) {
|
||||
if (childCount != null && index > childCount - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
itemExtent = itemExtentBuilder(index, _currentLayoutDimensions);
|
||||
if (itemExtent == null) {
|
||||
break;
|
||||
}
|
||||
if (_animationLeadingIndices.keys.contains(index)) {
|
||||
final UniqueKey animationKey = _animationLeadingIndices[index]!;
|
||||
if (_animationOffsets[animationKey] == null) {
|
||||
// We have not computed the distance this block is traversing over the
|
||||
// lifetime of the animation.
|
||||
_computeAnimationOffsetFor(animationKey, position);
|
||||
}
|
||||
// We add the offset accounting for the animation value.
|
||||
totalAnimationOffset += _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value);
|
||||
}
|
||||
position += itemExtent - totalAnimationOffset;
|
||||
++index;
|
||||
}
|
||||
return index - 1;
|
||||
}
|
||||
|
||||
void _computeAnimationOffsetFor(UniqueKey key, double position) {
|
||||
assert(_activeAnimations[key] != null);
|
||||
final double targetPosition = constraints.scrollOffset + constraints.remainingCacheExtent;
|
||||
double currentPosition = position;
|
||||
final int startingIndex = _activeAnimations[key]!.fromIndex;
|
||||
final int lastIndex = _activeAnimations[key]!.toIndex;
|
||||
int currentIndex = startingIndex;
|
||||
double totalAnimatingOffset = 0.0;
|
||||
// We animate only a portion of children that would be visible/in the cache
|
||||
// extent, unless all children would fit on the screen.
|
||||
while (currentIndex <= lastIndex && currentPosition < targetPosition) {
|
||||
final double itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions)!;
|
||||
totalAnimatingOffset += itemExtent;
|
||||
currentPosition += itemExtent;
|
||||
currentIndex++;
|
||||
}
|
||||
// For the life of this animation, which affects all children following
|
||||
// startingIndex (regardless of if they are a child of the triggering
|
||||
// parent), they will be offset by totalAnimatingOffset * the
|
||||
// animation value. This is because even though more children can be
|
||||
// scrolled into view, the same distance must be maintained for a smooth
|
||||
// animation.
|
||||
_animationOffsets[key] = totalAnimatingOffset;
|
||||
}
|
||||
|
||||
@override
|
||||
double indexToLayoutOffset(double itemExtent, int index) {
|
||||
// itemExtent is deprecated in the super class, we ignore it because we use
|
||||
// the builder anyways.
|
||||
double position = 0.0;
|
||||
int currentIndex = 0;
|
||||
double totalAnimationOffset = 0.0;
|
||||
double? itemExtent;
|
||||
final int? childCount = childManager.estimatedChildCount;
|
||||
while (currentIndex < index) {
|
||||
if (childCount != null && currentIndex > childCount - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions);
|
||||
if (itemExtent == null) {
|
||||
break;
|
||||
}
|
||||
if (_animationLeadingIndices.keys.contains(currentIndex)) {
|
||||
final UniqueKey animationKey = _animationLeadingIndices[currentIndex]!;
|
||||
assert(_animationOffsets[animationKey] != null);
|
||||
// We add the offset accounting for the animation value.
|
||||
totalAnimationOffset += _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value);
|
||||
}
|
||||
position += itemExtent;
|
||||
currentIndex++;
|
||||
}
|
||||
return position - totalAnimationOffset;
|
||||
}
|
||||
|
||||
final Map<UniqueKey, LayerHandle<ClipRectLayer>> _clipHandles = <UniqueKey, LayerHandle<ClipRectLayer>>{};
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (firstChild == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
RenderBox? nextChild = firstChild;
|
||||
void paintUpTo(
|
||||
int index,
|
||||
RenderBox? startWith,
|
||||
PaintingContext context,
|
||||
Offset offset,
|
||||
) {
|
||||
RenderBox? child = startWith;
|
||||
while (child != null && indexOf(child) <= index) {
|
||||
final double mainAxisDelta = childMainAxisPosition(child);
|
||||
final TreeSliverNodeParentData parentData = child.parentData! as TreeSliverNodeParentData;
|
||||
final Offset childOffset = Offset(
|
||||
parentData.depth * indentation,
|
||||
parentData.layoutOffset!,
|
||||
);
|
||||
|
||||
// If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child))
|
||||
// does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden.
|
||||
if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0) {
|
||||
context.paintChild(child, childOffset);
|
||||
}
|
||||
child = childAfter(child);
|
||||
}
|
||||
nextChild = child;
|
||||
}
|
||||
if (_animationLeadingIndices.isEmpty) {
|
||||
// There are no animations running.
|
||||
paintUpTo(indexOf(lastChild!), firstChild, context, offset);
|
||||
return;
|
||||
}
|
||||
|
||||
// We are animating.
|
||||
// Separate animating segments to clip for any overlap.
|
||||
int leadingIndex = indexOf(firstChild!);
|
||||
final List<int> animationIndices = _animationLeadingIndices.keys.toList()..sort();
|
||||
final List<_PaintSegment> paintSegments = <_PaintSegment>[];
|
||||
while (animationIndices.isNotEmpty) {
|
||||
final int trailingIndex = animationIndices.removeAt(0);
|
||||
paintSegments.add((leadingIndex: leadingIndex, trailingIndex: trailingIndex));
|
||||
leadingIndex = trailingIndex + 1;
|
||||
}
|
||||
paintSegments.add((leadingIndex: leadingIndex, trailingIndex: indexOf(lastChild!)));
|
||||
|
||||
// Paint, clipping for all but the first segment.
|
||||
paintUpTo(paintSegments.removeAt(0).trailingIndex, nextChild, context, offset);
|
||||
// Paint the rest with clip layers.
|
||||
while (paintSegments.isNotEmpty) {
|
||||
final _PaintSegment segment = paintSegments.removeAt(0);
|
||||
|
||||
// Rect is calculated by the trailing edge of the parent (preceding
|
||||
// leadingIndex), and the trailing edge of the trailing index. We cannot
|
||||
// rely on the leading edge of the leading index, because it is currently
|
||||
// moving.
|
||||
final int parentIndex = math.max(segment.leadingIndex - 1, 0);
|
||||
final double leadingOffset = indexToLayoutOffset(0.0, parentIndex)
|
||||
+ (parentIndex == 0 ? 0.0 : itemExtentBuilder(parentIndex, _currentLayoutDimensions)!);
|
||||
final double trailingOffset = indexToLayoutOffset(0.0, segment.trailingIndex)
|
||||
+ itemExtentBuilder(segment.trailingIndex, _currentLayoutDimensions)!;
|
||||
final Rect rect = Rect.fromPoints(
|
||||
Offset(0.0, leadingOffset),
|
||||
Offset(constraints.crossAxisExtent, trailingOffset),
|
||||
);
|
||||
// We use the same animation key to keep track of the clip layer, unless
|
||||
// this is the odd man out segment.
|
||||
final UniqueKey key = _animationLeadingIndices[parentIndex]!;
|
||||
_clipHandles[key] ??= LayerHandle<ClipRectLayer>();
|
||||
_clipHandles[key]!.layer = context.pushClipRect(
|
||||
needsCompositing,
|
||||
offset,
|
||||
rect,
|
||||
(PaintingContext context, Offset offset) {
|
||||
paintUpTo(segment.trailingIndex, nextChild, context, offset);
|
||||
},
|
||||
oldLayer: _clipHandles[key]!.layer,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,997 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
import 'icon.dart';
|
||||
import 'icon_data.dart';
|
||||
import 'implicit_animations.dart';
|
||||
import 'scroll_delegate.dart';
|
||||
import 'sliver.dart';
|
||||
import 'text.dart';
|
||||
import 'ticker_provider.dart';
|
||||
|
||||
const double _kDefaultRowExtent = 40.0;
|
||||
|
||||
/// A data structure for configuring children of a [TreeSliver].
|
||||
///
|
||||
/// A [TreeSliverNode.content] can be of any type [T], but must correspond with
|
||||
/// the same type of the [TreeSliver].
|
||||
///
|
||||
/// The values returned by [depth], [parent] and [isExpanded] getters are
|
||||
/// managed by the [TreeSliver]'s state.
|
||||
class TreeSliverNode<T> {
|
||||
/// Creates a [TreeSliverNode] instance for use in a [TreeSliver].
|
||||
TreeSliverNode(
|
||||
T content, {
|
||||
List<TreeSliverNode<T>>? children,
|
||||
bool expanded = false,
|
||||
}) : _expanded = children != null && children.isNotEmpty && expanded,
|
||||
_content = content,
|
||||
_children = children ?? <TreeSliverNode<T>>[];
|
||||
|
||||
/// The subject matter of the node.
|
||||
///
|
||||
/// Must correspond with the type of [TreeSliver].
|
||||
T get content => _content;
|
||||
final T _content;
|
||||
|
||||
/// Other [TreeSliverNode]s that this node will be [parent] to.
|
||||
///
|
||||
/// Modifying the children of nodes in a [TreeSliver] will cause the tree to be
|
||||
/// rebuilt so that newly added active nodes are reflected in the tree.
|
||||
List<TreeSliverNode<T>> get children => _children;
|
||||
final List<TreeSliverNode<T>> _children;
|
||||
|
||||
/// Whether or not this node is expanded in the tree.
|
||||
///
|
||||
/// Cannot be expanded if there are no children.
|
||||
bool get isExpanded => _expanded;
|
||||
bool _expanded;
|
||||
|
||||
/// The number of parent nodes between this node and the root of the tree.
|
||||
int? get depth => _depth;
|
||||
int? _depth;
|
||||
|
||||
/// The parent [TreeSliverNode] of this node.
|
||||
TreeSliverNode<T>? get parent => _parent;
|
||||
TreeSliverNode<T>? _parent;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TreeSliverNode: $content, depth: ${depth == 0 ? 'root' : depth}, '
|
||||
'${children.isEmpty ? 'leaf' : 'parent, expanded: $isExpanded'}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature for a function that creates a [Widget] to represent the given
|
||||
/// [TreeSliverNode] in the [TreeSliver].
|
||||
///
|
||||
/// Used by [TreeSliver.treeNodeBuilder] to build rows on demand for the
|
||||
/// tree.
|
||||
typedef TreeSliverNodeBuilder<T> = Widget Function(
|
||||
BuildContext context,
|
||||
TreeSliverNode<T> node,
|
||||
AnimationStyle animationStyle,
|
||||
);
|
||||
|
||||
/// Signature for a function that returns an extent for the given
|
||||
/// [TreeSliverNode] in the [TreeSliver].
|
||||
///
|
||||
/// Used by [TreeSliver.treeRowExtentBuilder] to size rows on demand in the
|
||||
/// tree. The provided [SliverLayoutDimensions] provide information about the
|
||||
/// current scroll state and [Viewport] dimensions.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverVariedExtentList], which uses a similar item extent builder for
|
||||
/// dynamic child sizing in the list.
|
||||
typedef TreeSliverRowExtentBuilder<T> = double Function(
|
||||
TreeSliverNode<T> node,
|
||||
SliverLayoutDimensions dimensions,
|
||||
);
|
||||
|
||||
/// Signature for a function that is called when a [TreeSliverNode] is toggled,
|
||||
/// changing its expanded state.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TreeSliver.onNodeToggle], for controlling node expansion
|
||||
/// programmatically.
|
||||
typedef TreeSliverNodeCallback<T> = void Function(TreeSliverNode<T> node);
|
||||
|
||||
/// A mixin for classes implementing a tree structure as expected by a
|
||||
/// [TreeSliverController].
|
||||
///
|
||||
/// Used by [TreeSliver] to implement an interface for the
|
||||
/// [TreeSliverController].
|
||||
///
|
||||
/// This allows the [TreeSliverController] to be used in other widgets that
|
||||
/// implement this interface.
|
||||
///
|
||||
/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode],
|
||||
/// representing the type of [TreeSliverNode.content].
|
||||
mixin TreeSliverStateMixin<T> {
|
||||
/// Returns whether or not the given [TreeSliverNode] is expanded.
|
||||
bool isExpanded(TreeSliverNode<T> node);
|
||||
|
||||
/// Returns whether or not the given [TreeSliverNode] is enclosed within its
|
||||
/// parent [TreeSliverNode].
|
||||
///
|
||||
/// If the [TreeSliverNode.parent] [isExpanded] (and all its parents are
|
||||
/// expanded), or this is a root node, the given node is active and this
|
||||
/// method will return true. This does not reflect whether or not the node is
|
||||
/// visible in the [Viewport].
|
||||
bool isActive(TreeSliverNode<T> node);
|
||||
|
||||
/// Switches the given [TreeSliverNode]s expanded state.
|
||||
///
|
||||
/// May trigger an animation to reveal or hide the node's children based on
|
||||
/// the [TreeSliver.toggleAnimationStyle].
|
||||
///
|
||||
/// If the node does not have any children, nothing will happen.
|
||||
void toggleNode(TreeSliverNode<T> node);
|
||||
|
||||
/// Closes all parent [TreeSliverNode]s in the tree.
|
||||
void collapseAll();
|
||||
|
||||
/// Expands all parent [TreeSliverNode]s in the tree.
|
||||
void expandAll();
|
||||
|
||||
/// Retrieves the [TreeSliverNode] containing the associated content, if it
|
||||
/// exists.
|
||||
///
|
||||
/// If no node exists, this will return null. This does not reflect whether
|
||||
/// or not a node [isActive], or if it is visible in the viewport.
|
||||
TreeSliverNode<T>? getNodeFor(T content);
|
||||
|
||||
/// Returns the current row index of the given [TreeSliverNode].
|
||||
///
|
||||
/// If the node is not currently active in the tree, meaning its parent is
|
||||
/// collapsed, this will return null.
|
||||
int? getActiveIndexFor(TreeSliverNode<T> node);
|
||||
}
|
||||
|
||||
/// Enables control over the [TreeSliverNode]s of a [TreeSliver].
|
||||
///
|
||||
/// It can be useful to expand or collapse nodes of the tree
|
||||
/// programmatically, for example to reconfigure an existing node
|
||||
/// based on a system event. To do so, create a [TreeSliver]
|
||||
/// with a [TreeSliverController] that's owned by a stateful widget
|
||||
/// or look up the tree's automatically created [TreeSliverController]
|
||||
/// with [TreeSliverController.of]
|
||||
///
|
||||
/// The controller's methods to expand or collapse nodes cause the
|
||||
/// the [TreeSliver] to rebuild, so they may not be called from
|
||||
/// a build method.
|
||||
class TreeSliverController {
|
||||
/// Create a controller to be used with [TreeSliver.controller].
|
||||
TreeSliverController();
|
||||
|
||||
TreeSliverStateMixin<Object?>? _state;
|
||||
|
||||
/// Whether the given [TreeSliverNode] built with this controller is in an
|
||||
/// expanded state.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [expandNode], which expands a given [TreeSliverNode].
|
||||
/// * [collapseNode], which collapses a given [TreeSliverNode].
|
||||
/// * [TreeSliver.controller] to create a TreeSliver with a controller.
|
||||
bool isExpanded(TreeSliverNode<Object?> node) {
|
||||
assert(_state != null);
|
||||
return _state!.isExpanded(node);
|
||||
}
|
||||
|
||||
/// Whether or not the given [TreeSliverNode] is enclosed within its parent
|
||||
/// [TreeSliverNode].
|
||||
///
|
||||
/// If the [TreeSliverNode.parent] [isExpanded], or this is a root node, the
|
||||
/// given node is active and this method will return true. This does not
|
||||
/// reflect whether or not the node is visible in the [Viewport].
|
||||
bool isActive(TreeSliverNode<Object?> node) {
|
||||
assert(_state != null);
|
||||
return _state!.isActive(node);
|
||||
}
|
||||
|
||||
/// Returns the [TreeSliverNode] containing the associated content, if it
|
||||
/// exists.
|
||||
///
|
||||
/// If no node exists, this will return null. This does not reflect whether
|
||||
/// or not a node [isActive], or if it is currently visible in the viewport.
|
||||
TreeSliverNode<Object?>? getNodeFor(Object? content) {
|
||||
assert(_state != null);
|
||||
return _state!.getNodeFor(content);
|
||||
}
|
||||
|
||||
/// Switches the given [TreeSliverNode]s expanded state.
|
||||
///
|
||||
/// May trigger an animation to reveal or hide the node's children based on
|
||||
/// the [TreeSliver.toggleAnimationStyle].
|
||||
///
|
||||
/// If the node does not have any children, nothing will happen.
|
||||
void toggleNode(TreeSliverNode<Object?> node) {
|
||||
assert(_state != null);
|
||||
return _state!.toggleNode(node);
|
||||
}
|
||||
|
||||
/// Expands the [TreeSliverNode] that was built with this controller.
|
||||
///
|
||||
/// If the node is already in the expanded state (see [isExpanded]), calling
|
||||
/// this method has no effect.
|
||||
///
|
||||
/// Calling this method may cause the [TreeSliver] to rebuild, so it may
|
||||
/// not be called from a build method.
|
||||
///
|
||||
/// Calling this method will trigger the [TreeSliver.onNodeToggle]
|
||||
/// callback.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [collapseNode], which collapses the [TreeSliverNode].
|
||||
/// * [isExpanded] to check whether the tile is expanded.
|
||||
/// * [TreeSliver.controller] to create a TreeSliver with a controller.
|
||||
void expandNode(TreeSliverNode<Object?> node) {
|
||||
assert(_state != null);
|
||||
if (!node.isExpanded) {
|
||||
_state!.toggleNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands all parent [TreeSliverNode]s in the tree.
|
||||
void expandAll() {
|
||||
assert(_state != null);
|
||||
_state!.expandAll();
|
||||
}
|
||||
|
||||
/// Closes all parent [TreeSliverNode]s in the tree.
|
||||
void collapseAll() {
|
||||
assert(_state != null);
|
||||
_state!.collapseAll();
|
||||
}
|
||||
|
||||
/// Collapses the [TreeSliverNode] that was built with this controller.
|
||||
///
|
||||
/// If the node is already in the collapsed state (see [isExpanded]), calling
|
||||
/// this method has no effect.
|
||||
///
|
||||
/// Calling this method may cause the [TreeSliver] to rebuild, so it may
|
||||
/// not be called from a build method.
|
||||
///
|
||||
/// Calling this method will trigger the [TreeSliver.onNodeToggle]
|
||||
/// callback.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [expandNode], which expands the tile.
|
||||
/// * [isExpanded] to check whether the tile is expanded.
|
||||
/// * [TreeSliver.controller] to create a TreeSliver with a controller.
|
||||
void collapseNode(TreeSliverNode<Object?> node) {
|
||||
assert(_state != null);
|
||||
if (node.isExpanded) {
|
||||
_state!.toggleNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current row index of the given [TreeSliverNode].
|
||||
///
|
||||
/// If the node is not currently active in the tree, meaning its parent is
|
||||
/// collapsed, this will return null.
|
||||
int? getActiveIndexFor(TreeSliverNode<Object?> node) {
|
||||
assert(_state != null);
|
||||
return _state!.getActiveIndexFor(node);
|
||||
}
|
||||
|
||||
/// Finds the [TreeSliverController] for the closest [TreeSliver] instance
|
||||
/// that encloses the given context.
|
||||
///
|
||||
/// If no [TreeSliver] encloses the given context, calling this
|
||||
/// method will cause an assert in debug mode, and throw an
|
||||
/// exception in release mode.
|
||||
///
|
||||
/// To return null if there is no [TreeSliver] use [maybeOf] instead.
|
||||
///
|
||||
/// Typical usage of the [TreeSliverController.of] function is to call it
|
||||
/// from within the `build` method of a descendant of a [TreeSliver].
|
||||
///
|
||||
/// When the [TreeSliver] is actually created in the same `build`
|
||||
/// function as the callback that refers to the controller, then the
|
||||
/// `context` argument to the `build` function can't be used to find
|
||||
/// the [TreeSliverController] (since it's "above" the widget
|
||||
/// being returned in the widget tree). In cases like that you can
|
||||
/// add a [Builder] widget, which provides a new scope with a
|
||||
/// [BuildContext] that is "under" the [TreeSliver].
|
||||
static TreeSliverController of(BuildContext context) {
|
||||
final _TreeSliverState<Object?>? result =
|
||||
context.findAncestorStateOfType<_TreeSliverState<Object?>>();
|
||||
if (result != null) {
|
||||
return result.controller;
|
||||
}
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'TreeController.of() called with a context that does not contain a '
|
||||
'TreeSliver.',
|
||||
),
|
||||
ErrorDescription(
|
||||
'No TreeSliver ancestor could be found starting from the context that '
|
||||
'was passed to TreeController.of(). '
|
||||
'This usually happens when the context provided is from the same '
|
||||
'StatefulWidget as that whose build function actually creates the '
|
||||
'TreeSliver widget being sought.',
|
||||
),
|
||||
ErrorHint(
|
||||
'There are several ways to avoid this problem. The simplest is to use '
|
||||
'a Builder to get a context that is "under" the TreeSliver.',
|
||||
),
|
||||
ErrorHint(
|
||||
'A more efficient solution is to split your build function into '
|
||||
'several widgets. This introduces a new context from which you can '
|
||||
'obtain the TreeSliver. In this solution, you would have an outer '
|
||||
'widget that creates the TreeSliver populated by instances of your new '
|
||||
'inner widgets, and then in these inner widgets you would use '
|
||||
'TreeController.of().',
|
||||
),
|
||||
context.describeElement('The context used was'),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Finds the [TreeSliver] from the closest instance of this class that
|
||||
/// encloses the given context and returns its [TreeSliverController].
|
||||
///
|
||||
/// If no [TreeSliver] encloses the given context then return null.
|
||||
/// To throw an exception instead, use [of] instead of this function.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [of], a similar function to this one that throws if no [TreeSliver]
|
||||
/// encloses the given context. Also includes some sample code in its
|
||||
/// documentation.
|
||||
static TreeSliverController? maybeOf(BuildContext context) {
|
||||
return context.findAncestorStateOfType<_TreeSliverState<Object?>>()?.controller;
|
||||
}
|
||||
}
|
||||
|
||||
int _kDefaultSemanticIndexCallback(Widget _, int localIndex) => localIndex;
|
||||
|
||||
/// A widget that displays [TreeSliverNode]s that expand and collapse in a
|
||||
/// vertically and horizontally scrolling [Viewport].
|
||||
///
|
||||
/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode],
|
||||
/// representing the type of [TreeSliverNode.content].
|
||||
///
|
||||
/// The rows of the tree are laid out on demand by the [Viewport]'s render
|
||||
/// object, using [TreeSliver.treeNodeBuilder]. This will only be called for the
|
||||
/// nodes that are visible, or within the [Viewport.cacheExtent].
|
||||
///
|
||||
/// The [TreeSliver.treeNodeBuilder] returns the [Widget] that represents the
|
||||
/// given [TreeSliverNode].
|
||||
///
|
||||
/// The [TreeSliver.treeRowExtentBuilder] returns a double representing the
|
||||
/// extent of a given node in the main axis.
|
||||
///
|
||||
/// Providing a [TreeSliverController] will enable querying and controlling the
|
||||
/// state of nodes in the tree.
|
||||
///
|
||||
/// A [TreeSliver] only supports a vertical axis direction of
|
||||
/// [AxisDirection.down] and a horizontal axis direction of
|
||||
/// [AxisDirection.right].
|
||||
///
|
||||
///{@tool dartpad}
|
||||
/// This example uses a [TreeSliver] to display nodes, highlighting nodes as
|
||||
/// they are selected.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows a highly customized [TreeSliver] configured to
|
||||
/// [TreeSliverIndentationType.none]. This allows the indentation to be handled
|
||||
/// by the developer in [TreeSliver.treeNodeBuilder], where a decoration is
|
||||
/// used to fill the indented space.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.1.dart **
|
||||
/// {@end-tool}
|
||||
class TreeSliver<T> extends StatefulWidget {
|
||||
/// Creates an instance of a [TreeSliver] for displaying [TreeSliverNode]s
|
||||
/// that animate expanding and collapsing of nodes.
|
||||
const TreeSliver({
|
||||
super.key,
|
||||
required this.tree,
|
||||
this.treeNodeBuilder = TreeSliver.defaultTreeNodeBuilder,
|
||||
this.treeRowExtentBuilder = TreeSliver.defaultTreeRowExtentBuilder,
|
||||
this.controller,
|
||||
this.onNodeToggle,
|
||||
this.toggleAnimationStyle,
|
||||
this.indentation = TreeSliverIndentationType.standard,
|
||||
this.addAutomaticKeepAlives = true,
|
||||
this.addRepaintBoundaries = true,
|
||||
this.addSemanticIndexes = true,
|
||||
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
|
||||
this.semanticIndexOffset = 0,
|
||||
this.findChildIndexCallback,
|
||||
});
|
||||
|
||||
/// The list of [TreeSliverNode]s that may be displayed in the [TreeSliver].
|
||||
///
|
||||
/// Beyond root nodes, whether or not a given [TreeSliverNode] is displayed
|
||||
/// depends on the [TreeSliverNode.isExpanded] value of its parent. The
|
||||
/// [TreeSliver] will set the [TreeSliverNode.parent] and
|
||||
/// [TreeSliverNode.depth] as nodes are built on demand to ensure the
|
||||
/// integrity of the tree.
|
||||
final List<TreeSliverNode<T>> tree;
|
||||
|
||||
/// Called to build and entry of the [TreeSliver] for the given node.
|
||||
///
|
||||
/// By default, if this is unset, the [TreeSliver.defaultTreeNodeBuilder]
|
||||
/// is used.
|
||||
final TreeSliverNodeBuilder<T> treeNodeBuilder;
|
||||
|
||||
/// Called to calculate the extent of the widget built for the given
|
||||
/// [TreeSliverNode].
|
||||
///
|
||||
/// By default, if this is unset, the
|
||||
/// [TreeSliver.defaultTreeRowExtentBuilder] is used.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverVariedExtentList.itemExtentBuilder], a very similar method that
|
||||
/// allows users to dynamically compute extents on demand.
|
||||
final TreeSliverRowExtentBuilder<T> treeRowExtentBuilder;
|
||||
|
||||
/// If provided, the controller can be used to expand and collapse
|
||||
/// [TreeSliverNode]s, or lookup information about the current state of the
|
||||
/// [TreeSliver].
|
||||
final TreeSliverController? controller;
|
||||
|
||||
/// Called when a [TreeSliverNode] expands or collapses.
|
||||
///
|
||||
/// This will not be called if a [TreeSliverNode] does not have any children.
|
||||
final TreeSliverNodeCallback<T>? onNodeToggle;
|
||||
|
||||
/// The default [AnimationStyle] for expanding and collapsing nodes in the
|
||||
/// [TreeSliver].
|
||||
///
|
||||
/// The default [AnimationStyle.duration] uses
|
||||
/// [TreeSliver.defaultAnimationDuration], which is 150 milliseconds.
|
||||
///
|
||||
/// The default [AnimationStyle.curve] uses [TreeSliver.defaultAnimationCurve],
|
||||
/// which is [Curves.linear].
|
||||
///
|
||||
/// To disable the tree animation, use [AnimationStyle.noAnimation].
|
||||
final AnimationStyle? toggleAnimationStyle;
|
||||
|
||||
/// The number of pixels children will be offset by in the cross axis based on
|
||||
/// their [TreeSliverNode.depth].
|
||||
///
|
||||
/// {@macro flutter.rendering.TreeSliverIndentationType}
|
||||
final TreeSliverIndentationType indentation;
|
||||
|
||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
|
||||
final bool addAutomaticKeepAlives;
|
||||
|
||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
|
||||
final bool addRepaintBoundaries;
|
||||
|
||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addSemanticIndexes}
|
||||
final bool addSemanticIndexes;
|
||||
|
||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexCallback}
|
||||
final SemanticIndexCallback semanticIndexCallback;
|
||||
|
||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexOffset}
|
||||
final int semanticIndexOffset;
|
||||
|
||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
|
||||
final int? Function(Key)? findChildIndexCallback;
|
||||
|
||||
/// The default [AnimationStyle] used for node expand and collapse animations,
|
||||
/// when one has not been provided in [toggleAnimationStyle].
|
||||
static AnimationStyle defaultToggleAnimationStyle = AnimationStyle(
|
||||
curve: defaultAnimationCurve,
|
||||
duration: defaultAnimationDuration,
|
||||
);
|
||||
|
||||
/// A default of [Curves.linear], which is used in the tree's expanding and
|
||||
/// collapsing node animation.
|
||||
static const Curve defaultAnimationCurve = Curves.linear;
|
||||
|
||||
/// A default [Duration] of 150 milliseconds, which is used in the tree's
|
||||
/// expanding and collapsing node animation.
|
||||
static const Duration defaultAnimationDuration = Duration(milliseconds: 150);
|
||||
|
||||
/// A wrapper method for triggering the expansion or collapse of a
|
||||
/// [TreeSliverNode].
|
||||
///
|
||||
/// Used as part of [TreeSliver.defaultTreeNodeBuilder] to wrap the leading
|
||||
/// icon of parent [TreeSliverNode]s such that tapping on it triggers the
|
||||
/// animation.
|
||||
///
|
||||
/// If defining your own [TreeSliver.treeNodeBuilder], this method can be used
|
||||
/// to wrap any part, or all, of the returned widget in order to trigger the
|
||||
/// change in state for the node.
|
||||
static Widget wrapChildToToggleNode({
|
||||
required TreeSliverNode<Object?> node,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
TreeSliverController.of(context).toggleNode(node);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the fixed default extent for rows in the tree, which is 40 pixels.
|
||||
///
|
||||
/// Used by [TreeSliver.treeRowExtentBuilder].
|
||||
static double defaultTreeRowExtentBuilder(
|
||||
TreeSliverNode<Object?> node,
|
||||
SliverLayoutDimensions dimensions,
|
||||
) {
|
||||
return _kDefaultRowExtent;
|
||||
}
|
||||
|
||||
/// Returns the default tree row for a given [TreeSliverNode].
|
||||
///
|
||||
/// Used by [TreeSliver.treeNodeBuilder].
|
||||
///
|
||||
/// This will return a [Row] containing the [toString] of
|
||||
/// [TreeSliverNode.content]. If the [TreeSliverNode] is a parent of
|
||||
/// additional nodes, a arrow icon will precede the content, and will trigger
|
||||
/// an expand and collapse animation when tapped.
|
||||
static Widget defaultTreeNodeBuilder(
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle toggleAnimationStyle
|
||||
) {
|
||||
final Duration animationDuration = toggleAnimationStyle.duration
|
||||
?? TreeSliver.defaultAnimationDuration;
|
||||
final Curve animationCurve = toggleAnimationStyle.curve
|
||||
?? TreeSliver.defaultAnimationCurve;
|
||||
final int index = TreeSliverController.of(context).getActiveIndexFor(node)!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(children: <Widget>[
|
||||
// Icon for parent nodes
|
||||
TreeSliver.wrapChildToToggleNode(
|
||||
node: node,
|
||||
child: SizedBox.square(
|
||||
dimension: 30.0,
|
||||
child: node.children.isNotEmpty
|
||||
? AnimatedRotation(
|
||||
key: ValueKey<int>(index),
|
||||
turns: node.isExpanded ? 0.25 : 0.0,
|
||||
duration: animationDuration,
|
||||
curve: animationCurve,
|
||||
// Renders a unicode right-facing arrow. >
|
||||
child: const Icon(IconData(0x25BA), size: 14),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
// Spacer
|
||||
const SizedBox(width: 8.0),
|
||||
// Content
|
||||
Text(node.content.toString()),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<TreeSliver<T>> createState() => _TreeSliverState<T>();
|
||||
}
|
||||
|
||||
// Used in _SliverTreeState for code simplicity.
|
||||
typedef _AnimationRecord = ({
|
||||
AnimationController controller,
|
||||
CurvedAnimation animation,
|
||||
UniqueKey key,
|
||||
});
|
||||
|
||||
class _TreeSliverState<T> extends State<TreeSliver<T>> with TickerProviderStateMixin, TreeSliverStateMixin<T> {
|
||||
TreeSliverController get controller => _treeController!;
|
||||
TreeSliverController? _treeController;
|
||||
|
||||
final List<TreeSliverNode<T>> _activeNodes = <TreeSliverNode<T>>[];
|
||||
bool _shouldUnpackNode(TreeSliverNode<T> node) {
|
||||
if (node.children.isEmpty) {
|
||||
// No children to unpack.
|
||||
return false;
|
||||
}
|
||||
if (_currentAnimationForParent[node] != null) {
|
||||
// Whether expanding or collapsing, the child nodes are still active, so
|
||||
// unpack.
|
||||
return true;
|
||||
}
|
||||
// If we are not animating, respect node.isExpanded.
|
||||
return node.isExpanded;
|
||||
}
|
||||
void _unpackActiveNodes({
|
||||
int depth = 0,
|
||||
List<TreeSliverNode<T>>? nodes,
|
||||
TreeSliverNode<T>? parent,
|
||||
}) {
|
||||
if (nodes == null) {
|
||||
_activeNodes.clear();
|
||||
nodes = widget.tree;
|
||||
}
|
||||
for (final TreeSliverNode<T> node in nodes) {
|
||||
node._depth = depth;
|
||||
node._parent = parent;
|
||||
_activeNodes.add(node);
|
||||
if (_shouldUnpackNode(node)) {
|
||||
_unpackActiveNodes(
|
||||
depth: depth + 1,
|
||||
nodes: node.children,
|
||||
parent: node,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Map<TreeSliverNode<T>, _AnimationRecord> _currentAnimationForParent = <TreeSliverNode<T>, _AnimationRecord>{};
|
||||
final Map<UniqueKey, TreeSliverNodesAnimation> _activeAnimations = <UniqueKey, TreeSliverNodesAnimation>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_unpackActiveNodes();
|
||||
assert(
|
||||
widget.controller?._state == null,
|
||||
'The provided TreeSliverController is already associated with another '
|
||||
'TreeSliver. A TreeSliverController can only be associated with one '
|
||||
'TreeSliver.',
|
||||
);
|
||||
_treeController = widget.controller ?? TreeSliverController();
|
||||
_treeController!._state = this;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TreeSliver<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Internal or provided, there is always a tree controller.
|
||||
assert(_treeController != null);
|
||||
if (oldWidget.controller == null && widget.controller != null) {
|
||||
// A new tree controller has been provided, update and dispose of the
|
||||
// internally generated one.
|
||||
_treeController!._state = null;
|
||||
_treeController = widget.controller;
|
||||
_treeController!._state = this;
|
||||
} else if (oldWidget.controller != null && widget.controller == null) {
|
||||
// A tree controller had been provided, but was removed. We need to create
|
||||
// one internally.
|
||||
assert(oldWidget.controller == _treeController);
|
||||
oldWidget.controller!._state = null;
|
||||
_treeController = TreeSliverController();
|
||||
_treeController!._state = this;
|
||||
} else if (oldWidget.controller != widget.controller) {
|
||||
assert(oldWidget.controller != null);
|
||||
assert(widget.controller != null);
|
||||
assert(oldWidget.controller == _treeController);
|
||||
// The tree is still being provided a controller, but it has changed. Just
|
||||
// update it.
|
||||
_treeController!._state = null;
|
||||
_treeController = widget.controller;
|
||||
_treeController!._state = this;
|
||||
}
|
||||
// Internal or provided, there is always a tree controller.
|
||||
assert(_treeController != null);
|
||||
assert(_treeController!._state != null);
|
||||
_unpackActiveNodes();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_treeController!._state = null;
|
||||
for (final _AnimationRecord record in _currentAnimationForParent.values) {
|
||||
record.animation.dispose();
|
||||
record.controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _SliverTree(
|
||||
itemCount: _activeNodes.length,
|
||||
activeAnimations: _activeAnimations,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final TreeSliverNode<T> node = _activeNodes[index];
|
||||
Widget child = widget.treeNodeBuilder(
|
||||
context,
|
||||
node,
|
||||
widget.toggleAnimationStyle ?? TreeSliver.defaultToggleAnimationStyle,
|
||||
);
|
||||
|
||||
if (widget.addRepaintBoundaries) {
|
||||
child = RepaintBoundary(child: child);
|
||||
}
|
||||
if (widget.addSemanticIndexes) {
|
||||
final int? semanticIndex = widget.semanticIndexCallback(child, index);
|
||||
if (semanticIndex != null) {
|
||||
child = IndexedSemantics(
|
||||
index: semanticIndex + widget.semanticIndexOffset,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return _TreeNodeParentDataWidget(
|
||||
depth: node.depth!,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
|
||||
return widget.treeRowExtentBuilder(_activeNodes[index], dimensions);
|
||||
},
|
||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||
findChildIndexCallback: widget.findChildIndexCallback,
|
||||
indentation: widget.indentation.value,
|
||||
);
|
||||
}
|
||||
|
||||
// TreeStateMixin Implementation
|
||||
|
||||
@override
|
||||
bool isExpanded(TreeSliverNode<T> node) {
|
||||
return _getNode(node.content, widget.tree)?.isExpanded ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isActive(TreeSliverNode<T> node) => _activeNodes.contains(node);
|
||||
|
||||
@override
|
||||
TreeSliverNode<T>? getNodeFor(T content) => _getNode(content, widget.tree);
|
||||
TreeSliverNode<T>? _getNode(T content, List<TreeSliverNode<T>> tree) {
|
||||
final List<TreeSliverNode<T>> nextDepth = <TreeSliverNode<T>>[];
|
||||
for (final TreeSliverNode<T> node in tree) {
|
||||
if (node.content == content) {
|
||||
return node;
|
||||
}
|
||||
if (node.children.isNotEmpty) {
|
||||
nextDepth.addAll(node.children);
|
||||
}
|
||||
}
|
||||
if (nextDepth.isNotEmpty) {
|
||||
return _getNode(content, nextDepth);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
int? getActiveIndexFor(TreeSliverNode<T> node) {
|
||||
if (_activeNodes.contains(node)) {
|
||||
return _activeNodes.indexOf(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void expandAll() {
|
||||
final List<TreeSliverNode<T>> activeNodesToExpand = <TreeSliverNode<T>>[];
|
||||
_expandAll(widget.tree, activeNodesToExpand);
|
||||
activeNodesToExpand.reversed.forEach(toggleNode);
|
||||
}
|
||||
void _expandAll(
|
||||
List<TreeSliverNode<T>> tree,
|
||||
List<TreeSliverNode<T>> activeNodesToExpand,
|
||||
) {
|
||||
for (final TreeSliverNode<T> node in tree) {
|
||||
if (node.children.isNotEmpty) {
|
||||
// This is a parent node.
|
||||
// Expand all the children, and their children.
|
||||
_expandAll(node.children, activeNodesToExpand);
|
||||
if (!node.isExpanded) {
|
||||
// The node itself needs to be expanded.
|
||||
if (_activeNodes.contains(node)) {
|
||||
// This is an active node in the tree, add to
|
||||
// the list to toggle once all hidden nodes
|
||||
// have been handled.
|
||||
activeNodesToExpand.add(node);
|
||||
} else {
|
||||
// This is a hidden node. Update its expanded state.
|
||||
node._expanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void collapseAll() {
|
||||
final List<TreeSliverNode<T>> activeNodesToCollapse = <TreeSliverNode<T>>[];
|
||||
_collapseAll(widget.tree, activeNodesToCollapse);
|
||||
activeNodesToCollapse.reversed.forEach(toggleNode);
|
||||
}
|
||||
void _collapseAll(
|
||||
List<TreeSliverNode<T>> tree,
|
||||
List<TreeSliverNode<T>> activeNodesToCollapse,
|
||||
) {
|
||||
for (final TreeSliverNode<T> node in tree) {
|
||||
if (node.children.isNotEmpty) {
|
||||
// This is a parent node.
|
||||
// Collapse all the children, and their children.
|
||||
_collapseAll(node.children, activeNodesToCollapse);
|
||||
if (node.isExpanded) {
|
||||
// The node itself needs to be collapsed.
|
||||
if (_activeNodes.contains(node)) {
|
||||
// This is an active node in the tree, add to
|
||||
// the list to toggle once all hidden nodes
|
||||
// have been handled.
|
||||
activeNodesToCollapse.add(node);
|
||||
} else {
|
||||
// This is a hidden node. Update its expanded state.
|
||||
node._expanded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateActiveAnimations() {
|
||||
// The indexes of various child node animations can change constantly based
|
||||
// on more nodes being expanded or collapsed. Compile the indexes and their
|
||||
// animations keys each time we build with an updated active node list.
|
||||
_activeAnimations.clear();
|
||||
for (final TreeSliverNode<T> node in _currentAnimationForParent.keys) {
|
||||
final _AnimationRecord animationRecord = _currentAnimationForParent[node]!;
|
||||
final int leadingChildIndex = _activeNodes.indexOf(node) + 1;
|
||||
final TreeSliverNodesAnimation animatingChildren = (
|
||||
fromIndex: leadingChildIndex,
|
||||
toIndex: leadingChildIndex + node.children.length - 1,
|
||||
value: animationRecord.animation.value,
|
||||
);
|
||||
_activeAnimations[animationRecord.key] = animatingChildren;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void toggleNode(TreeSliverNode<T> node) {
|
||||
assert(_activeNodes.contains(node));
|
||||
if (node.children.isEmpty) {
|
||||
// No state to change.
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
node._expanded = !node._expanded;
|
||||
if (widget.onNodeToggle != null) {
|
||||
widget.onNodeToggle!(node);
|
||||
}
|
||||
final AnimationController controller = _currentAnimationForParent[node]?.controller
|
||||
?? AnimationController(
|
||||
value: node._expanded ? 0.0 : 1.0,
|
||||
vsync: this,
|
||||
duration: widget.toggleAnimationStyle?.duration
|
||||
?? TreeSliver.defaultAnimationDuration,
|
||||
)..addStatusListener((AnimationStatus status) {
|
||||
switch (status) {
|
||||
case AnimationStatus.dismissed:
|
||||
case AnimationStatus.completed:
|
||||
_currentAnimationForParent[node]!.controller.dispose();
|
||||
_currentAnimationForParent.remove(node);
|
||||
_updateActiveAnimations();
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
}
|
||||
})..addListener(() {
|
||||
setState((){
|
||||
_updateActiveAnimations();
|
||||
});
|
||||
});
|
||||
|
||||
switch (controller.status) {
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
// We're interrupting an animation already in progress.
|
||||
controller.stop();
|
||||
case AnimationStatus.dismissed:
|
||||
case AnimationStatus.completed:
|
||||
}
|
||||
|
||||
final CurvedAnimation newAnimation = CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: widget.toggleAnimationStyle?.curve ?? TreeSliver.defaultAnimationCurve,
|
||||
);
|
||||
_currentAnimationForParent[node] = (
|
||||
controller: controller,
|
||||
animation: newAnimation,
|
||||
// This key helps us keep track of the lifetime of this animation in the
|
||||
// render object, since the indexes can change at any time.
|
||||
key: UniqueKey(),
|
||||
);
|
||||
switch (node._expanded) {
|
||||
case true:
|
||||
// Expanding
|
||||
_unpackActiveNodes();
|
||||
controller.forward();
|
||||
case false:
|
||||
// Collapsing
|
||||
controller.reverse().then((_) {
|
||||
_unpackActiveNodes();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _TreeNodeParentDataWidget extends ParentDataWidget<TreeSliverNodeParentData> {
|
||||
const _TreeNodeParentDataWidget({
|
||||
required this.depth,
|
||||
required super.child,
|
||||
}) : assert(depth >= 0);
|
||||
|
||||
final int depth;
|
||||
|
||||
@override
|
||||
void applyParentData(RenderObject renderObject) {
|
||||
final TreeSliverNodeParentData parentData = renderObject.parentData! as TreeSliverNodeParentData;
|
||||
bool needsLayout = false;
|
||||
|
||||
if (parentData.depth != depth) {
|
||||
assert(depth >= 0);
|
||||
parentData.depth = depth;
|
||||
needsLayout = true;
|
||||
}
|
||||
|
||||
if (needsLayout) {
|
||||
renderObject.parent?.markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Type get debugTypicalAncestorWidgetClass => _SliverTree;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IntProperty('depth', depth));
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverTree extends SliverVariedExtentList {
|
||||
_SliverTree({
|
||||
required NullableIndexedWidgetBuilder itemBuilder,
|
||||
required super.itemExtentBuilder,
|
||||
required this.activeAnimations,
|
||||
required this.indentation,
|
||||
ChildIndexGetter? findChildIndexCallback,
|
||||
required int itemCount,
|
||||
bool addAutomaticKeepAlives = true,
|
||||
}) : super(delegate: SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
findChildIndexCallback: findChildIndexCallback,
|
||||
childCount: itemCount,
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: false, // Added in the _SliverTreeState
|
||||
addSemanticIndexes: false, // Added in the _SliverTreeState
|
||||
));
|
||||
|
||||
final Map<UniqueKey, TreeSliverNodesAnimation> activeAnimations;
|
||||
final double indentation;
|
||||
|
||||
@override
|
||||
RenderTreeSliver createRenderObject(BuildContext context) {
|
||||
final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
|
||||
return RenderTreeSliver(
|
||||
itemExtentBuilder: itemExtentBuilder,
|
||||
activeAnimations: activeAnimations,
|
||||
indentation: indentation,
|
||||
childManager: element,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderTreeSliver renderObject) {
|
||||
renderObject
|
||||
..itemExtentBuilder = itemExtentBuilder
|
||||
..activeAnimations = activeAnimations
|
||||
..indentation = indentation;
|
||||
}
|
||||
}
|
@ -137,7 +137,6 @@ export 'src/widgets/sliver_fill.dart';
|
||||
export 'src/widgets/sliver_layout_builder.dart';
|
||||
export 'src/widgets/sliver_persistent_header.dart';
|
||||
export 'src/widgets/sliver_prototype_extent_list.dart';
|
||||
export 'src/widgets/sliver_tree.dart';
|
||||
export 'src/widgets/slotted_render_object_widget.dart';
|
||||
export 'src/widgets/snapshot_widget.dart';
|
||||
export 'src/widgets/spacer.dart';
|
||||
|
@ -1,860 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
List<TreeSliverNode<String>> _setUpNodes() {
|
||||
return <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('First'),
|
||||
TreeSliverNode<String>(
|
||||
'Second',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>(
|
||||
'alpha',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('uno'),
|
||||
TreeSliverNode<String>('dos'),
|
||||
TreeSliverNode<String>('tres'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('beta'),
|
||||
TreeSliverNode<String>('kappa'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>(
|
||||
'Third',
|
||||
expanded: true,
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('gamma'),
|
||||
TreeSliverNode<String>('delta'),
|
||||
TreeSliverNode<String>('epsilon'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('Fourth'),
|
||||
];
|
||||
}
|
||||
|
||||
List<TreeSliverNode<String>> treeNodes = _setUpNodes();
|
||||
|
||||
void main() {
|
||||
testWidgets('asserts proper axis directions', (WidgetTester tester) async {
|
||||
final List<Object?> exceptions = <Object?>[];
|
||||
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
exceptions.add(details.exception);
|
||||
};
|
||||
addTearDown(() {
|
||||
FlutterError.onError = oldHandler;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
reverse: true,
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(tree: treeNodes),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
FlutterError.onError = oldHandler;
|
||||
expect(exceptions.isNotEmpty, isTrue);
|
||||
expect(
|
||||
exceptions[0].toString(),
|
||||
contains('TreeSliver is only supported in Viewports with an AxisDirection.down.'),
|
||||
);
|
||||
|
||||
exceptions.clear();
|
||||
await tester.pumpWidget(Container());
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
exceptions.add(details.exception);
|
||||
};
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
reverse: true,
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(tree: treeNodes),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
FlutterError.onError = oldHandler;
|
||||
expect(exceptions.isNotEmpty, isTrue);
|
||||
expect(
|
||||
exceptions[0].toString(),
|
||||
contains('TreeSliver is only supported in Viewports with an AxisDirection.down.'),
|
||||
);
|
||||
|
||||
exceptions.clear();
|
||||
await tester.pumpWidget(Container());
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
exceptions.add(details.exception);
|
||||
};
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(tree: treeNodes),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
FlutterError.onError = oldHandler;
|
||||
expect(exceptions.isNotEmpty, isTrue);
|
||||
expect(
|
||||
exceptions[0].toString(),
|
||||
contains('TreeSliver is only supported in Viewports with an AxisDirection.down.'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Basic layout', (WidgetTester tester) async {
|
||||
treeNodes = _setUpNodes();
|
||||
// Default layout, custom indentation values, row extents.
|
||||
TreeSliver<String> treeSliver = TreeSliver<String>(
|
||||
tree: treeNodes,
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[ treeSliver ],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 88.0))
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
expect(find.text('First'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('First')),
|
||||
const Rect.fromLTRB(46.0, 8.0, 286.0, 32.0),
|
||||
);
|
||||
expect(find.text('Second'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Second')),
|
||||
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
|
||||
);
|
||||
expect(find.text('Third'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Third')),
|
||||
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
|
||||
);
|
||||
expect(find.text('gamma'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('gamma')),
|
||||
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
|
||||
);
|
||||
expect(find.text('delta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('delta')),
|
||||
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
|
||||
);
|
||||
expect(find.text('epsilon'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('epsilon')),
|
||||
const Rect.fromLTRB(46.0, 208.0, 382.0, 232.0),
|
||||
);
|
||||
expect(find.text('Fourth'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Fourth')),
|
||||
const Rect.fromLTRB(46.0, 248.0, 334.0, 272.0),
|
||||
);
|
||||
|
||||
treeSliver = TreeSliver<String>(
|
||||
tree: treeNodes,
|
||||
indentation: TreeSliverIndentationType.none,
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[ treeSliver ],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 88.0))
|
||||
..paragraph(offset: const Offset(46.0, 128.0))
|
||||
..paragraph(offset: const Offset(46.0, 168.0))
|
||||
..paragraph(offset: const Offset(46.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
expect(find.text('First'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('First')),
|
||||
const Rect.fromLTRB(46.0, 8.0, 286.0, 32.0),
|
||||
);
|
||||
expect(find.text('Second'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Second')),
|
||||
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
|
||||
);
|
||||
expect(find.text('Third'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Third')),
|
||||
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
|
||||
);
|
||||
expect(find.text('gamma'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('gamma')),
|
||||
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
|
||||
);
|
||||
expect(find.text('delta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('delta')),
|
||||
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
|
||||
);
|
||||
expect(find.text('epsilon'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('epsilon')),
|
||||
const Rect.fromLTRB(46.0, 208.0, 382.0, 232.0),
|
||||
);
|
||||
expect(find.text('Fourth'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Fourth')),
|
||||
const Rect.fromLTRB(46.0, 248.0, 334.0, 272.0),
|
||||
);
|
||||
|
||||
treeSliver = TreeSliver<String>(
|
||||
tree: treeNodes,
|
||||
indentation: TreeSliverIndentationType.custom(50.0),
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[ treeSliver ],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 88.0))
|
||||
..paragraph(offset: const Offset(96.0, 128.0))
|
||||
..paragraph(offset: const Offset(96.0, 168.0))
|
||||
..paragraph(offset: const Offset(96.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
expect(find.text('First'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('First')),
|
||||
const Rect.fromLTRB(46.0, 8.0, 286.0, 32.0),
|
||||
);
|
||||
expect(find.text('Second'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Second')),
|
||||
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
|
||||
);
|
||||
expect(find.text('Third'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Third')),
|
||||
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
|
||||
);
|
||||
expect(find.text('gamma'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('gamma')),
|
||||
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
|
||||
);
|
||||
expect(find.text('delta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('delta')),
|
||||
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
|
||||
);
|
||||
expect(find.text('epsilon'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('epsilon')),
|
||||
const Rect.fromLTRB(46.0, 208.0, 382.0, 232.0),
|
||||
);
|
||||
expect(find.text('Fourth'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Fourth')),
|
||||
const Rect.fromLTRB(46.0, 248.0, 334.0, 272.0),
|
||||
);
|
||||
|
||||
treeSliver = TreeSliver<String>(
|
||||
tree: treeNodes,
|
||||
treeRowExtentBuilder: (_, __) => 100,
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[ treeSliver ],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 26.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 126.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 226.0))
|
||||
..paragraph(offset: const Offset(56.0, 326.0))
|
||||
..paragraph(offset: const Offset(56.0, 426.0))
|
||||
..paragraph(offset: const Offset(56.0, 526.0))
|
||||
);
|
||||
expect(find.text('First'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('First')),
|
||||
const Rect.fromLTRB(46.0, 26.0, 286.0, 74.0),
|
||||
);
|
||||
expect(find.text('Second'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Second')),
|
||||
const Rect.fromLTRB(46.0, 126.0, 334.0, 174.0),
|
||||
);
|
||||
expect(find.text('Third'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('Third')),
|
||||
const Rect.fromLTRB(46.0, 226.0, 286.0, 274.0),
|
||||
);
|
||||
expect(find.text('gamma'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('gamma')),
|
||||
const Rect.fromLTRB(46.0, 326.0, 286.0, 374.0),
|
||||
);
|
||||
expect(find.text('delta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('delta')),
|
||||
const Rect.fromLTRB(46.0, 426.0, 286.0, 474.0),
|
||||
);
|
||||
expect(find.text('epsilon'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('epsilon')),
|
||||
const Rect.fromLTRB(46.0, 526.0, 382.0, 574.0),
|
||||
);
|
||||
expect(find.text('Fourth'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Animating node segment', (WidgetTester tester) async {
|
||||
treeNodes = _setUpNodes();
|
||||
TreeSliver<String> treeSliver = TreeSliver<String>(tree: treeNodes);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[ treeSliver ],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 88.0))
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
expect(find.text('alpha'), findsNothing);
|
||||
await tester.tap(find.byType(Icon).first);
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph(offset: const Offset(56.0, 8.0)) // beta animating in
|
||||
..paragraph(offset: const Offset(56.0, 48.0)) // kappa animating in
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 88.0))
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
// New nodes have been inserted into the tree, alpha
|
||||
// is not visible yet.
|
||||
expect(find.text('alpha'), findsNothing);
|
||||
expect(find.text('beta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('beta')),
|
||||
const Rect.fromLTRB(46.0, 8.0, 238.0, 32.0),
|
||||
);
|
||||
expect(find.text('kappa'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('kappa')),
|
||||
const Rect.fromLTRB(46.0, 48.0, 286.0, 72.0),
|
||||
);
|
||||
// Progress the animation.
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // alpha icon
|
||||
..paragraph(offset: const Offset(56.0, 8.0)) // alpha animating in
|
||||
..paragraph(offset: const Offset(56.0, 48.0)) // beta animating in
|
||||
..paragraph(offset: const Offset(56.0, 88.0)) // kappa animating in
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(56.0, 248.0))
|
||||
..paragraph(offset: const Offset(46.0, 288.0))
|
||||
);
|
||||
expect(
|
||||
tester.getRect(find.text('alpha')).top.floor(),
|
||||
8.0,
|
||||
);
|
||||
expect(find.text('beta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('beta')).top.floor(),
|
||||
48.0,
|
||||
);
|
||||
expect(find.text('kappa'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('kappa')).top.floor(),
|
||||
88.0,
|
||||
);
|
||||
// Complete the animation
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0)) // First
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0)) // Second
|
||||
..paragraph() // alpha icon
|
||||
..paragraph(offset: const Offset(56.0, 88.0)) // alpha
|
||||
..paragraph(offset: const Offset(56.0, 128.0)) // beta
|
||||
..paragraph(offset: const Offset(56.0, 168.0)) // kappa
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 208.0)) // Third
|
||||
..paragraph(offset: const Offset(56.0, 248.0)) // gamma
|
||||
..paragraph(offset: const Offset(56.0, 288.0)) // delta
|
||||
..paragraph(offset: const Offset(56.0, 328.0)) // epsilon
|
||||
..paragraph(offset: const Offset(46.0, 368.0)) // Fourth
|
||||
);
|
||||
expect(find.text('alpha'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('alpha')),
|
||||
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
|
||||
);
|
||||
expect(find.text('beta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('beta')),
|
||||
const Rect.fromLTRB(46.0, 128.0, 238.0, 152.0),
|
||||
);
|
||||
expect(find.text('kappa'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('kappa')),
|
||||
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
|
||||
);
|
||||
|
||||
// Customize the animation
|
||||
treeSliver = TreeSliver<String>(
|
||||
tree: treeNodes,
|
||||
toggleAnimationStyle: AnimationStyle(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.bounceIn,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[ treeSliver ],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0)) // First
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0)) // Second
|
||||
..paragraph() // alpha icon
|
||||
..paragraph(offset: const Offset(56.0, 88.0)) // alpha
|
||||
..paragraph(offset: const Offset(56.0, 128.0)) // beta
|
||||
..paragraph(offset: const Offset(56.0, 168.0)) // kappa
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 208.0)) // Third
|
||||
..paragraph(offset: const Offset(56.0, 248.0)) // gamma
|
||||
..paragraph(offset: const Offset(56.0, 288.0)) // delta
|
||||
..paragraph(offset: const Offset(56.0, 328.0)) // epsilon
|
||||
..paragraph(offset: const Offset(46.0, 368.0)) // Fourth
|
||||
);
|
||||
// Still visible from earlier.
|
||||
expect(find.text('alpha'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('alpha')),
|
||||
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
|
||||
);
|
||||
// Collapse the node now
|
||||
await tester.tap(find.byType(Icon).first);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(find.text('alpha'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('alpha')).top.floor(),
|
||||
-22,
|
||||
);
|
||||
expect(find.text('beta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('beta')).top.floor(),
|
||||
18,
|
||||
);
|
||||
expect(find.text('kappa'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('kappa')).top.floor(),
|
||||
58,
|
||||
);
|
||||
// Progress the animation.
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(find.text('alpha'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('alpha')).top.floor(),
|
||||
-25,
|
||||
);
|
||||
expect(find.text('beta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('beta')).top.floor(),
|
||||
15,
|
||||
);
|
||||
expect(find.text('kappa'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('kappa')).top.floor(),
|
||||
55.0,
|
||||
);
|
||||
// Complete the animation
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 88.0))
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
expect(find.text('alpha'), findsNothing);
|
||||
|
||||
// Disable the animation
|
||||
treeSliver = TreeSliver<String>(
|
||||
tree: treeNodes,
|
||||
toggleAnimationStyle: AnimationStyle.noAnimation,
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[ treeSliver ],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 88.0))
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
// Not in the tree.
|
||||
expect(find.text('alpha'), findsNothing);
|
||||
// Collapse the node now
|
||||
await tester.tap(find.byType(Icon).first);
|
||||
await tester.pump();
|
||||
// No animating, straight to positions.
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0)) // First
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0)) // Second
|
||||
..paragraph() // alpha icon
|
||||
..paragraph(offset: const Offset(56.0, 88.0)) // alpha
|
||||
..paragraph(offset: const Offset(56.0, 128.0)) // beta
|
||||
..paragraph(offset: const Offset(56.0, 168.0)) // kappa
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 208.0)) // Third
|
||||
..paragraph(offset: const Offset(56.0, 248.0)) // gamma
|
||||
..paragraph(offset: const Offset(56.0, 288.0)) // delta
|
||||
..paragraph(offset: const Offset(56.0, 328.0)) // epsilon
|
||||
..paragraph(offset: const Offset(46.0, 368.0)) // Fourth
|
||||
);
|
||||
expect(find.text('alpha'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('alpha')),
|
||||
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
|
||||
);
|
||||
expect(find.text('beta'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('beta')),
|
||||
const Rect.fromLTRB(46.0, 128.0, 238.0, 152.0),
|
||||
);
|
||||
expect(find.text('kappa'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.text('kappa')),
|
||||
const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Multiple animating node segments', (WidgetTester tester) async {
|
||||
treeNodes = _setUpNodes();
|
||||
final TreeSliver<String> treeSliver = TreeSliver<String>(tree: treeNodes);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[ treeSliver ],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 88.0))
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
expect(find.text('Second'), findsOneWidget);
|
||||
expect(find.text('alpha'), findsNothing); // Second is collapsed
|
||||
expect(find.text('Third'), findsOneWidget);
|
||||
expect(find.text('gamma'), findsOneWidget); // Third is expanded
|
||||
|
||||
expect(
|
||||
tester.getRect(find.text('Second')),
|
||||
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
|
||||
);
|
||||
expect(
|
||||
tester.getRect(find.text('Third')),
|
||||
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
|
||||
);
|
||||
expect(
|
||||
tester.getRect(find.text('gamma')),
|
||||
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
|
||||
);
|
||||
|
||||
// Trigger two animations to run together.
|
||||
// Collapse Third
|
||||
await tester.tap(find.byType(Icon).last);
|
||||
// Expand Second
|
||||
await tester.tap(find.byType(Icon).first);
|
||||
await tester.pump(const Duration(milliseconds: 15));
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph(offset: const Offset(56.0, 8.0)) // beta entering
|
||||
..paragraph(offset: const Offset(56.0, 48.0)) // kappa entering
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 88.0))
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
// Third is collapsing
|
||||
expect(
|
||||
tester.getRect(find.text('Third')),
|
||||
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
|
||||
);
|
||||
expect(
|
||||
tester.getRect(find.text('gamma')),
|
||||
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
|
||||
);
|
||||
// Second is expanding
|
||||
expect(
|
||||
tester.getRect(find.text('Second')),
|
||||
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
|
||||
);
|
||||
// beta has been added and is animating into view.
|
||||
expect(
|
||||
tester.getRect(find.text('beta')).top.floor(),
|
||||
8.0,
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 15));
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // alpha icon animating
|
||||
..paragraph(offset: const Offset(56.0, -20.0)) // alpha naimating
|
||||
..paragraph(offset: const Offset(56.0, 20.0)) // beta
|
||||
..paragraph(offset: const Offset(56.0, 60.0)) // kappa
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 100.0)) // Third
|
||||
// Children of Third are animating, but the expand and
|
||||
// collapse counter each other, so their position is unchanged.
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
// Third is still collapsing. Third is sliding down
|
||||
// as Seconds's children slide in, gamma is still exiting.
|
||||
expect(
|
||||
tester.getRect(find.text('Third')).top.floor(),
|
||||
100.0,
|
||||
);
|
||||
// gamma appears to not have moved, this is because it is
|
||||
// intersecting both animations, the positive offset of
|
||||
// Second animation == the negative offset of Third
|
||||
expect(
|
||||
tester.getRect(find.text('gamma')),
|
||||
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
|
||||
);
|
||||
// Second is still expanding
|
||||
expect(
|
||||
tester.getRect(find.text('Second')),
|
||||
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
|
||||
);
|
||||
// alpha is still animating into view.
|
||||
expect(
|
||||
tester.getRect(find.text('alpha')).top.floor(),
|
||||
-20.0,
|
||||
);
|
||||
// Progress the animation further
|
||||
await tester.pump(const Duration(milliseconds: 15));
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // alpha icon animating
|
||||
..paragraph(offset: const Offset(56.0, -8.0)) // alpha animating
|
||||
..paragraph(offset: const Offset(56.0, 32.0)) // beta
|
||||
..paragraph(offset: const Offset(56.0, 72.0)) // kappa
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 112.0)) // Third
|
||||
// Children of Third are animating, but the expand and
|
||||
// collapse counter each other, so their position is unchanged.
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph(offset: const Offset(56.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
// Third is still collapsing. Third is sliding down
|
||||
// as Seconds's children slide in, gamma is still exiting.
|
||||
expect(
|
||||
tester.getRect(find.text('Third')).top.floor(),
|
||||
112.0,
|
||||
);
|
||||
// gamma appears to not have moved, this is because it is
|
||||
// intersecting both animations, the positive offset of
|
||||
// Second animation == the negative offset of Third
|
||||
expect(
|
||||
tester.getRect(find.text('gamma')),
|
||||
const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0),
|
||||
);
|
||||
// Second is still expanding
|
||||
expect(
|
||||
tester.getRect(find.text('Second')),
|
||||
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
|
||||
);
|
||||
// alpha is still animating into view.
|
||||
expect(
|
||||
tester.getRect(find.text('alpha')).top.floor(),
|
||||
-8.0,
|
||||
);
|
||||
// Complete the animations
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.byType(TreeSliver<String>),
|
||||
paints
|
||||
..paragraph(offset: const Offset(46.0, 8.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 48.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(56.0, 88.0))
|
||||
..paragraph(offset: const Offset(56.0, 128.0))
|
||||
..paragraph(offset: const Offset(56.0, 168.0))
|
||||
..paragraph() // Icon
|
||||
..paragraph(offset: const Offset(46.0, 208.0))
|
||||
..paragraph(offset: const Offset(46.0, 248.0))
|
||||
);
|
||||
expect(
|
||||
tester.getRect(find.text('Third')),
|
||||
const Rect.fromLTRB(46.0, 208.0, 286.0, 232.0),
|
||||
);
|
||||
// gamma has left the building
|
||||
expect(find.text('gamma'), findsNothing);
|
||||
expect(
|
||||
tester.getRect(find.text('Second')),
|
||||
const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0),
|
||||
);
|
||||
// alpha is in place.
|
||||
expect(
|
||||
tester.getRect(find.text('alpha')),
|
||||
const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('only paints visible rows', (WidgetTester tester) async {
|
||||
treeNodes = _setUpNodes();
|
||||
final ScrollController scrollController = ScrollController();
|
||||
addTearDown(scrollController.dispose);
|
||||
treeNodes = _setUpNodes();
|
||||
final TreeSliver<String> treeSliver = TreeSliver<String>(
|
||||
treeRowExtentBuilder: (_, __) => 200,
|
||||
tree: treeNodes,
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: <Widget>[ treeSliver ],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(scrollController.position.pixels, 0.0);
|
||||
expect(scrollController.position.maxScrollExtent, 800.0);
|
||||
bool rowNeedsPaint(String row) {
|
||||
return find.text(row).evaluate().first.renderObject!.debugNeedsPaint;
|
||||
}
|
||||
|
||||
expect(rowNeedsPaint('First'), isFalse);
|
||||
expect(rowNeedsPaint('Second'), isFalse);
|
||||
expect(rowNeedsPaint('Third'), isFalse);
|
||||
expect(find.text('gamma'), findsNothing); // Not visible
|
||||
|
||||
// Change the scroll offset
|
||||
scrollController.jumpTo(200);
|
||||
await tester.pump();
|
||||
expect(find.text('First'), findsNothing);
|
||||
expect(rowNeedsPaint('Second'), isFalse);
|
||||
expect(rowNeedsPaint('Third'), isFalse);
|
||||
expect(rowNeedsPaint('gamma'), isFalse); // Now visible
|
||||
});
|
||||
}
|
@ -1,727 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
List<TreeSliverNode<String>> simpleNodeSet = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Root 0'),
|
||||
TreeSliverNode<String>(
|
||||
'Root 1',
|
||||
expanded: true,
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Child 1:0'),
|
||||
TreeSliverNode<String>('Child 1:1'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>(
|
||||
'Root 2',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Child 2:0'),
|
||||
TreeSliverNode<String>('Child 2:1'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('Root 3'),
|
||||
];
|
||||
|
||||
void main() {
|
||||
group('TreeSliverNode', () {
|
||||
test('getters, toString', () {
|
||||
final List<TreeSliverNode<String>> children = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('child'),
|
||||
];
|
||||
final TreeSliverNode<String> node = TreeSliverNode<String>(
|
||||
'parent',
|
||||
children: children,
|
||||
expanded: true,
|
||||
);
|
||||
expect(node.content, 'parent');
|
||||
expect(node.children, children);
|
||||
expect(node.isExpanded, isTrue);
|
||||
expect(node.children.first.content, 'child');
|
||||
expect(node.children.first.children.isEmpty, isTrue);
|
||||
expect(node.children.first.isExpanded, isFalse);
|
||||
// Set by TreeSliver when built for tree integrity
|
||||
expect(node.depth, isNull);
|
||||
expect(node.parent, isNull);
|
||||
expect(node.children.first.depth, isNull);
|
||||
expect(node.children.first.parent, isNull);
|
||||
|
||||
expect(
|
||||
node.toString(),
|
||||
'TreeSliverNode: parent, depth: null, parent, expanded: true',
|
||||
);
|
||||
expect(
|
||||
node.children.first.toString(),
|
||||
'TreeSliverNode: child, depth: null, leaf',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('TreeSliverNode sets ups parent and depth properties', (WidgetTester tester) async {
|
||||
final List<TreeSliverNode<String>> children = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('child'),
|
||||
];
|
||||
final TreeSliverNode<String> node = TreeSliverNode<String>(
|
||||
'parent',
|
||||
children: children,
|
||||
expanded: true,
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: <TreeSliverNode<String>>[node],
|
||||
),
|
||||
],
|
||||
)
|
||||
));
|
||||
expect(node.content, 'parent');
|
||||
expect(node.children, children);
|
||||
expect(node.isExpanded, isTrue);
|
||||
expect(node.children.first.content, 'child');
|
||||
expect(node.children.first.children.isEmpty, isTrue);
|
||||
expect(node.children.first.isExpanded, isFalse);
|
||||
// Set by TreeSliver when built for tree integrity
|
||||
expect(node.depth, 0);
|
||||
expect(node.parent, isNull);
|
||||
expect(node.children.first.depth, 1);
|
||||
expect(node.children.first.parent, node);
|
||||
|
||||
expect(
|
||||
node.toString(),
|
||||
'TreeSliverNode: parent, depth: root, parent, expanded: true',
|
||||
);
|
||||
expect(
|
||||
node.children.first.toString(),
|
||||
'TreeSliverNode: child, depth: 1, leaf',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('TreeController', () {
|
||||
setUp(() {
|
||||
// Reset node conditions for each test.
|
||||
simpleNodeSet = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Root 0'),
|
||||
TreeSliverNode<String>(
|
||||
'Root 1',
|
||||
expanded: true,
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Child 1:0'),
|
||||
TreeSliverNode<String>('Child 1:1'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>(
|
||||
'Root 2',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Child 2:0'),
|
||||
TreeSliverNode<String>('Child 2:1'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('Root 3'),
|
||||
];
|
||||
});
|
||||
testWidgets('Can set controller on TreeSliver', (WidgetTester tester) async {
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
TreeSliverController? returnedController;
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
treeNodeBuilder: (
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle toggleAnimationStyle,
|
||||
) {
|
||||
returnedController ??= TreeSliverController.of(context);
|
||||
return TreeSliver.defaultTreeNodeBuilder(
|
||||
context,
|
||||
node,
|
||||
toggleAnimationStyle,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
expect(controller, returnedController);
|
||||
});
|
||||
|
||||
testWidgets('Can get default controller on TreeSliver', (WidgetTester tester) async {
|
||||
TreeSliverController? returnedController;
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
treeNodeBuilder: (
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle toggleAnimationStyle,
|
||||
) {
|
||||
returnedController ??= TreeSliverController.maybeOf(context);
|
||||
return TreeSliver.defaultTreeNodeBuilder(
|
||||
context,
|
||||
node,
|
||||
toggleAnimationStyle,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
expect(returnedController, isNotNull);
|
||||
});
|
||||
|
||||
testWidgets('Can get node for TreeSliverNode.content', (WidgetTester tester) async {
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
expect(controller.getNodeFor('Root 0'), simpleNodeSet[0]);
|
||||
});
|
||||
|
||||
testWidgets('Can get isExpanded for a node', (WidgetTester tester) async {
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[0]),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[1]),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can get isActive for a node', (WidgetTester tester) async {
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[0]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[1]),
|
||||
isTrue,
|
||||
);
|
||||
// The parent 'Root 2' is not expanded, so its children are not active.
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[2]),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[2].children[0]),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can toggleNode, to collapse or expand', (WidgetTester tester) async {
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
// The parent 'Root 2' is not expanded, so its children are not active.
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[2]),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[2].children[0]),
|
||||
isFalse,
|
||||
);
|
||||
// Toggle 'Root 2' to expand it
|
||||
controller.toggleNode(simpleNodeSet[2]);
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[2]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[2].children[0]),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
// The parent 'Root 1' is expanded, so its children are active.
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[1]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[1].children[0]),
|
||||
isTrue,
|
||||
);
|
||||
// Collapse 'Root 1'
|
||||
controller.toggleNode(simpleNodeSet[1]);
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[1]),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[1].children[0]),
|
||||
isTrue,
|
||||
);
|
||||
// Nodes are not removed from the active list until the collapse animation
|
||||
// completes. The parent expansion state also updates.
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[1]),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[1].children[0]),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can expandNode, then collapseAll',
|
||||
(WidgetTester tester) async {
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
// The parent 'Root 2' is not expanded, so its children are not active.
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[2]),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[2].children[0]),
|
||||
isFalse,
|
||||
);
|
||||
// Expand 'Root 2'
|
||||
controller.expandNode(simpleNodeSet[2]);
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[2]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[2].children[0]),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
// Both parents from our simple node set are expanded.
|
||||
// 'Root 1'
|
||||
expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
|
||||
// 'Root 2'
|
||||
expect(controller.isExpanded(simpleNodeSet[2]), isTrue);
|
||||
// Collapse both.
|
||||
controller.collapseAll();
|
||||
await tester.pumpAndSettle();
|
||||
// Both parents from our simple node set have collapsed.
|
||||
// 'Root 1'
|
||||
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
|
||||
// 'Root 2'
|
||||
expect(controller.isExpanded(simpleNodeSet[2]), isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Can collapseNode, then expandAll', (WidgetTester tester) async {
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
// The parent 'Root 1' is expanded, so its children are active.
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[1]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[1].children[0]),
|
||||
isTrue,
|
||||
);
|
||||
// Collapse 'Root 1'
|
||||
controller.collapseNode(simpleNodeSet[1]);
|
||||
expect(
|
||||
controller.isExpanded(simpleNodeSet[1]),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[1].children[0]),
|
||||
isTrue,
|
||||
);
|
||||
// Nodes are not removed from the active list until the collapse animation
|
||||
// completes.
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
controller.isActive(simpleNodeSet[1].children[0]),
|
||||
isFalse,
|
||||
);
|
||||
|
||||
// Both parents from our simple node set are collapsed.
|
||||
// 'Root 1'
|
||||
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
|
||||
// 'Root 2'
|
||||
expect(controller.isExpanded(simpleNodeSet[2]), isFalse);
|
||||
// Expand both.
|
||||
controller.expandAll();
|
||||
// Both parents from our simple node set are expanded.
|
||||
// 'Root 1'
|
||||
expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
|
||||
// 'Root 2'
|
||||
expect(controller.isExpanded(simpleNodeSet[2]), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
test('TreeSliverIndentationType values are properly reflected', () {
|
||||
double value = TreeSliverIndentationType.standard.value;
|
||||
expect(value, 10.0);
|
||||
|
||||
value = TreeSliverIndentationType.none.value;
|
||||
expect(value, 0.0);
|
||||
|
||||
value = TreeSliverIndentationType.custom(50.0).value;
|
||||
expect(value, 50.0);
|
||||
});
|
||||
|
||||
testWidgets('.toggleNodeWith, onNodeToggle', (WidgetTester tester) async {
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
// The default node builder wraps the leading icon with toggleNodeWith.
|
||||
bool toggled = false;
|
||||
TreeSliverNode<String>? toggledNode;
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
onNodeToggle: (TreeSliverNode<Object?> node) {
|
||||
toggled = true;
|
||||
toggledNode = node as TreeSliverNode<String>;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
|
||||
await tester.tap(find.byType(Icon).first);
|
||||
await tester.pump();
|
||||
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
|
||||
expect(toggled, isTrue);
|
||||
expect(toggledNode, simpleNodeSet[1]);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
|
||||
toggled = false;
|
||||
toggledNode = null;
|
||||
// Use toggleNodeWith to make the whole row trigger the node state.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
onNodeToggle: (TreeSliverNode<Object?> node) {
|
||||
toggled = true;
|
||||
toggledNode = node as TreeSliverNode<String>;
|
||||
},
|
||||
treeNodeBuilder: (
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle toggleAnimationStyle,
|
||||
) {
|
||||
final Duration animationDuration =
|
||||
toggleAnimationStyle.duration ?? TreeSliver.defaultAnimationDuration;
|
||||
final Curve animationCurve =
|
||||
toggleAnimationStyle.curve ?? TreeSliver.defaultAnimationCurve;
|
||||
// This makes the whole row trigger toggling.
|
||||
return TreeSliver.wrapChildToToggleNode(
|
||||
node: node,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(children: <Widget>[
|
||||
// Icon for parent nodes
|
||||
SizedBox.square(
|
||||
dimension: 30.0,
|
||||
child: node.children.isNotEmpty
|
||||
? AnimatedRotation(
|
||||
turns: node.isExpanded ? 0.25 : 0.0,
|
||||
duration: animationDuration,
|
||||
curve: animationCurve,
|
||||
child: const Icon(IconData(0x25BA), size: 14),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
// Spacer
|
||||
const SizedBox(width: 8.0),
|
||||
// Content
|
||||
Text(node.content.toString()),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
// Still collapsed from earlier
|
||||
expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
|
||||
// Tapping on the text instead of the Icon.
|
||||
await tester.tap(find.text('Root 1'));
|
||||
await tester.pump();
|
||||
expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
|
||||
expect(toggled, isTrue);
|
||||
expect(toggledNode, simpleNodeSet[1]);
|
||||
});
|
||||
|
||||
testWidgets('AnimationStyle is piped through to node builder', (WidgetTester tester) async {
|
||||
AnimationStyle? style;
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
treeNodeBuilder: (
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle toggleAnimationStyle,
|
||||
) {
|
||||
style ??= toggleAnimationStyle;
|
||||
return Text(node.content.toString());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
// Default
|
||||
expect(style, TreeSliver.defaultToggleAnimationStyle);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
toggleAnimationStyle: AnimationStyle.noAnimation,
|
||||
treeNodeBuilder: (
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle toggleAnimationStyle,
|
||||
) {
|
||||
style = toggleAnimationStyle;
|
||||
return Text(node.content.toString());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
expect(style, isNotNull);
|
||||
expect(style!.curve, isNull);
|
||||
expect(style!.duration, Duration.zero);
|
||||
style = null;
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
toggleAnimationStyle: AnimationStyle(
|
||||
curve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
treeNodeBuilder: (
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle toggleAnimationStyle,
|
||||
) {
|
||||
style ??= toggleAnimationStyle;
|
||||
return Text(node.content.toString());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
expect(style, isNotNull);
|
||||
expect(style!.curve, Curves.easeIn);
|
||||
expect(style!.duration, const Duration(milliseconds: 200));
|
||||
});
|
||||
|
||||
testWidgets('Adding more root TreeViewNodes are reflected in the tree', (WidgetTester tester) async {
|
||||
simpleNodeSet = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Root 0'),
|
||||
TreeSliverNode<String>(
|
||||
'Root 1',
|
||||
expanded: true,
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Child 1:0'),
|
||||
TreeSliverNode<String>('Child 1:1'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>(
|
||||
'Root 2',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Child 2:0'),
|
||||
TreeSliverNode<String>('Child 2:1'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('Root 3'),
|
||||
];
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
simpleNodeSet.add(TreeSliverNode<String>('Added root'));
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Root 0'), findsOneWidget);
|
||||
expect(find.text('Root 1'), findsOneWidget);
|
||||
expect(find.text('Child 1:0'), findsOneWidget);
|
||||
expect(find.text('Child 1:1'), findsOneWidget);
|
||||
expect(find.text('Root 2'), findsOneWidget);
|
||||
expect(find.text('Child 2:0'), findsNothing);
|
||||
expect(find.text('Child 2:1'), findsNothing);
|
||||
expect(find.text('Root 3'), findsOneWidget);
|
||||
expect(find.text('Added root'), findsNothing);
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Root 0'), findsOneWidget);
|
||||
expect(find.text('Root 1'), findsOneWidget);
|
||||
expect(find.text('Child 1:0'), findsOneWidget);
|
||||
expect(find.text('Child 1:1'), findsOneWidget);
|
||||
expect(find.text('Root 2'), findsOneWidget);
|
||||
expect(find.text('Child 2:0'), findsNothing);
|
||||
expect(find.text('Child 2:1'), findsNothing);
|
||||
expect(find.text('Root 3'), findsOneWidget);
|
||||
// Node was added
|
||||
expect(find.text('Added root'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Adding more TreeViewNodes below the root are reflected in the tree', (WidgetTester tester) async {
|
||||
simpleNodeSet = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Root 0'),
|
||||
TreeSliverNode<String>(
|
||||
'Root 1',
|
||||
expanded: true,
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Child 1:0'),
|
||||
TreeSliverNode<String>('Child 1:1'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>(
|
||||
'Root 2',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('Child 2:0'),
|
||||
TreeSliverNode<String>('Child 2:1'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('Root 3'),
|
||||
];
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: simpleNodeSet,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
simpleNodeSet[1].children.add(TreeSliverNode<String>('Added child'));
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(find.text('Root 0'), findsOneWidget);
|
||||
expect(find.text('Root 1'), findsOneWidget);
|
||||
expect(find.text('Child 1:0'), findsOneWidget);
|
||||
expect(find.text('Child 1:1'), findsOneWidget);
|
||||
expect(find.text('Added child'), findsNothing);
|
||||
expect(find.text('Root 2'), findsOneWidget);
|
||||
expect(find.text('Child 2:0'), findsNothing);
|
||||
expect(find.text('Child 2:1'), findsNothing);
|
||||
expect(find.text('Root 3'), findsOneWidget);
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pump();
|
||||
expect(find.text('Root 0'), findsOneWidget);
|
||||
expect(find.text('Root 1'), findsOneWidget);
|
||||
expect(find.text('Child 1:0'), findsOneWidget);
|
||||
expect(find.text('Child 1:1'), findsOneWidget);
|
||||
// Child node was added
|
||||
expect(find.text('Added child'), findsOneWidget);
|
||||
expect(find.text('Root 2'), findsOneWidget);
|
||||
expect(find.text('Child 2:0'), findsNothing);
|
||||
expect(find.text('Child 2:1'), findsNothing);
|
||||
expect(find.text('Root 3'), findsOneWidget);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user