Skip to content

Commit

Permalink
TreeSliver & associated classes (#147171)
Browse files Browse the repository at this point in the history
**FYI for Reviewers:** Much of the API surface matches that of the 2D TreeView in flutter#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 flutter/flutter#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
  • Loading branch information
Piinks authored Jun 4, 2024
1 parent 8897555 commit b50eb97
Show file tree
Hide file tree
Showing 11 changed files with 3,326 additions and 0 deletions.
104 changes: 104 additions & 0 deletions examples/api/lib/widgets/sliver/sliver_tree.0.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 189 additions & 0 deletions examples/api/lib/widgets/sliver/sliver_tree.1.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions examples/api/test/widgets/sliver/sliver_tree.0_test.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions examples/api/test/widgets/sliver/sliver_tree.1_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
}
1 change: 1 addition & 0 deletions packages/flutter/lib/rendering.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export 'src/rendering/sliver_list.dart';
export 'src/rendering/sliver_multi_box_adaptor.dart';
export 'src/rendering/sliver_padding.dart';
export 'src/rendering/sliver_persistent_header.dart';
export 'src/rendering/sliver_tree.dart';
export 'src/rendering/stack.dart';
export 'src/rendering/table.dart';
export 'src/rendering/table_border.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'object.dart';
import 'sliver.dart';
import 'sliver_fixed_extent_list.dart';

/// A delegate used by [RenderSliverMultiBoxAdaptor] to manage its children.
///
Expand Down
Loading

0 comments on commit b50eb97

Please sign in to comment.