forked from firka/flutter
TreeSliver & associated classes (#147171)
**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:
104
examples/api/lib/widgets/sliver/sliver_tree.0.dart
Normal file
104
examples/api/lib/widgets/sliver/sliver_tree.0.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [TreeSliver].
|
||||
|
||||
void main() => runApp(const TreeSliverExampleApp());
|
||||
|
||||
class TreeSliverExampleApp extends StatelessWidget {
|
||||
const TreeSliverExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: TreeSliverExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TreeSliverExample extends StatefulWidget {
|
||||
const TreeSliverExample({super.key});
|
||||
|
||||
@override
|
||||
State<TreeSliverExample> createState() => _TreeSliverExampleState();
|
||||
}
|
||||
|
||||
class _TreeSliverExampleState extends State<TreeSliverExample> {
|
||||
TreeSliverNode<String>? _selectedNode;
|
||||
final TreeSliverController controller = TreeSliverController();
|
||||
final List<TreeSliverNode<String>> _tree = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('First'),
|
||||
TreeSliverNode<String>(
|
||||
'Second',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>(
|
||||
'alpha',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('uno'),
|
||||
TreeSliverNode<String>('dos'),
|
||||
TreeSliverNode<String>('tres'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('beta'),
|
||||
TreeSliverNode<String>('kappa'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>(
|
||||
'Third',
|
||||
expanded: true,
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('gamma'),
|
||||
TreeSliverNode<String>('delta'),
|
||||
TreeSliverNode<String>('epsilon'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('Fourth'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('TreeSliver Demo'),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
TreeSliver<String>(
|
||||
tree: _tree,
|
||||
controller: controller,
|
||||
treeNodeBuilder: (
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle animationStyle,
|
||||
) {
|
||||
Widget child = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
controller.toggleNode(node);
|
||||
_selectedNode = node as TreeSliverNode<String>;
|
||||
});
|
||||
},
|
||||
child: TreeSliver.defaultTreeNodeBuilder(
|
||||
context,
|
||||
node,
|
||||
animationStyle,
|
||||
),
|
||||
);
|
||||
if (_selectedNode == node as TreeSliverNode<String>) {
|
||||
child = ColoredBox(
|
||||
color: Colors.purple[100]!,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
189
examples/api/lib/widgets/sliver/sliver_tree.1.dart
Normal file
189
examples/api/lib/widgets/sliver/sliver_tree.1.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// Flutter code sample for [TreeSliver].
|
||||
|
||||
void main() => runApp(const TreeSliverExampleApp());
|
||||
|
||||
class TreeSliverExampleApp extends StatelessWidget {
|
||||
const TreeSliverExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: TreeSliverExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TreeSliverExample extends StatefulWidget {
|
||||
const TreeSliverExample({super.key});
|
||||
|
||||
@override
|
||||
State<TreeSliverExample> createState() => _TreeSliverExampleState();
|
||||
}
|
||||
|
||||
class _TreeSliverExampleState extends State<TreeSliverExample> {
|
||||
TreeSliverNode<String>? _selectedNode;
|
||||
final List<TreeSliverNode<String>> tree = <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('README.md'),
|
||||
TreeSliverNode<String>('analysis_options.yaml'),
|
||||
TreeSliverNode<String>(
|
||||
'lib',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>(
|
||||
'src',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>(
|
||||
'widgets',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('about.dart.dart'),
|
||||
TreeSliverNode<String>('app.dart'),
|
||||
TreeSliverNode<String>('basic.dart'),
|
||||
TreeSliverNode<String>('constants.dart'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('widgets.dart'),
|
||||
],
|
||||
),
|
||||
TreeSliverNode<String>('pubspec.lock'),
|
||||
TreeSliverNode<String>('pubspec.yaml'),
|
||||
TreeSliverNode<String>(
|
||||
'test',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>(
|
||||
'widgets',
|
||||
children: <TreeSliverNode<String>>[
|
||||
TreeSliverNode<String>('about_test.dart'),
|
||||
TreeSliverNode<String>('app_test.dart'),
|
||||
TreeSliverNode<String>('basic_test.dart'),
|
||||
TreeSliverNode<String>('constants_test.dart'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
Widget _treeNodeBuilder(
|
||||
BuildContext context,
|
||||
TreeSliverNode<Object?> node,
|
||||
AnimationStyle toggleAnimationStyle,
|
||||
) {
|
||||
final bool isParentNode = node.children.isNotEmpty;
|
||||
final BorderSide border = BorderSide(
|
||||
width: 2,
|
||||
color: Colors.purple[300]!,
|
||||
);
|
||||
return TreeSliver.wrapChildToToggleNode(
|
||||
node: node,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
// Custom indentation
|
||||
SizedBox(width: 10.0 * node.depth! + 8.0),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: node.parent != null
|
||||
? Border(left: border, bottom: border)
|
||||
: null,
|
||||
),
|
||||
child: const SizedBox(height: 50.0, width: 20.0),
|
||||
),
|
||||
// Leading icon for parent nodes
|
||||
if (isParentNode)
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(border: Border.all()),
|
||||
child: SizedBox.square(
|
||||
dimension: 20.0,
|
||||
child: Icon(
|
||||
node.isExpanded ? Icons.remove : Icons.add,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Spacer
|
||||
const SizedBox(width: 8.0),
|
||||
// Content
|
||||
Text(node.content.toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getTree() {
|
||||
return DecoratedSliver(
|
||||
decoration: BoxDecoration( border: Border.all()),
|
||||
sliver: TreeSliver<String>(
|
||||
tree: tree,
|
||||
onNodeToggle: (TreeSliverNode<Object?> node) {
|
||||
setState(() {
|
||||
_selectedNode = node as TreeSliverNode<String>;
|
||||
});
|
||||
},
|
||||
treeNodeBuilder: _treeNodeBuilder,
|
||||
treeRowExtentBuilder: (
|
||||
TreeSliverNode<Object?> node,
|
||||
SliverLayoutDimensions layoutDimensions,
|
||||
) {
|
||||
// This gives more space to parent nodes.
|
||||
return node.children.isNotEmpty ? 60.0 : 50.0;
|
||||
},
|
||||
// No internal indentation, the custom treeNodeBuilder applies its
|
||||
// own indentation to decorate in the indented space.
|
||||
indentation: TreeSliverIndentationType.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This example is assumes the full screen is available.
|
||||
final Size screenSize = MediaQuery.sizeOf(context);
|
||||
final List<Widget> selectedChildren = <Widget>[];
|
||||
if (_selectedNode != null) {
|
||||
selectedChildren.addAll(<Widget>[
|
||||
const Spacer(),
|
||||
Icon(
|
||||
_selectedNode!.children.isEmpty
|
||||
? Icons.file_open_outlined
|
||||
: Icons.folder_outlined,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(_selectedNode!.content),
|
||||
const Spacer(),
|
||||
]);
|
||||
}
|
||||
return Scaffold(
|
||||
body: Row(children: <Widget>[
|
||||
SizedBox(
|
||||
width: screenSize.width / 2,
|
||||
height: double.infinity,
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
_getTree(),
|
||||
],
|
||||
),
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: screenSize.width / 2,
|
||||
height: double.infinity,
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: selectedChildren,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
21
examples/api/test/widgets/sliver/sliver_tree.0_test.dart
Normal file
21
examples/api/test/widgets/sliver/sliver_tree.0_test.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_api_samples/widgets/sliver/sliver_tree.0.dart'
|
||||
as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Can toggle nodes in TreeSliver', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.TreeSliverExampleApp(),
|
||||
);
|
||||
expect(find.text('Second'), findsOneWidget);
|
||||
expect(find.text('alpha'), findsNothing);
|
||||
// Toggle tree node.
|
||||
await tester.tap(find.text('Second'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('alpha'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
21
examples/api/test/widgets/sliver/sliver_tree.1_test.dart
Normal file
21
examples/api/test/widgets/sliver/sliver_tree.1_test.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_api_samples/widgets/sliver/sliver_tree.1.dart'
|
||||
as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Can toggle nodes in TreeSliver', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.TreeSliverExampleApp(),
|
||||
);
|
||||
expect(find.text('lib'), findsOneWidget);
|
||||
expect(find.text('src'), findsNothing);
|
||||
// Toggle tree node.
|
||||
await tester.tap(find.text('lib'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('src'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user