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_multi_box_adaptor.dart';
|
||||||
export 'src/rendering/sliver_padding.dart';
|
export 'src/rendering/sliver_padding.dart';
|
||||||
export 'src/rendering/sliver_persistent_header.dart';
|
export 'src/rendering/sliver_persistent_header.dart';
|
||||||
export 'src/rendering/sliver_tree.dart';
|
|
||||||
export 'src/rendering/stack.dart';
|
export 'src/rendering/stack.dart';
|
||||||
export 'src/rendering/table.dart';
|
export 'src/rendering/table.dart';
|
||||||
export 'src/rendering/table_border.dart';
|
export 'src/rendering/table_border.dart';
|
||||||
|
@ -8,7 +8,6 @@ import 'package:vector_math/vector_math_64.dart';
|
|||||||
import 'box.dart';
|
import 'box.dart';
|
||||||
import 'object.dart';
|
import 'object.dart';
|
||||||
import 'sliver.dart';
|
import 'sliver.dart';
|
||||||
import 'sliver_fixed_extent_list.dart';
|
|
||||||
|
|
||||||
/// A delegate used by [RenderSliverMultiBoxAdaptor] to manage its children.
|
/// 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_layout_builder.dart';
|
||||||
export 'src/widgets/sliver_persistent_header.dart';
|
export 'src/widgets/sliver_persistent_header.dart';
|
||||||
export 'src/widgets/sliver_prototype_extent_list.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/slotted_render_object_widget.dart';
|
||||||
export 'src/widgets/snapshot_widget.dart';
|
export 'src/widgets/snapshot_widget.dart';
|
||||||
export 'src/widgets/spacer.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