Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[two_dimensional_scrollables] TreeView #6592

Merged
merged 32 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/two_dimensional_scrollables/CHANGELOG.md
@@ -1,3 +1,8 @@
## 0.3.0

* Adds new TreeView widget and associated classes.
* New example application for exploring both Trees and Tables.

## 0.2.1

* Refactors TableSpans to use basic Span classes. Clean up for incoming TreeView.
Expand Down
35 changes: 27 additions & 8 deletions packages/two_dimensional_scrollables/README.md
Expand Up @@ -5,18 +5,30 @@ two-dimensional foundation of the Flutter framework.

## Features

This package provides support for a TableView widget that scrolls in both the
vertical and horizontal axes.
This package provides support for TableView and TreeView widgets that scroll
in both the vertical and horizontal axes.

### TableView

`TableView` is a subclass of `TwoDimensionalScrollView`, building its provided
children lazily in a `TwoDimensionalViewport`. This widget can

- Scroll diagonally, or lock axes
- Build infinite rows and columns
- Apply decorations to rows and columns
- Handle gestures & custom pointers for rows and columns
- Pin rows and columns
- Merge table cells

### TreeView

`TreeView` is a subclass of `TwoDimensionalScrollView`, building its provided
children lazily in a `TwoDimensionalViewport`. This widget can

- Scroll diagonally, or lock axes
- Apply decorations to tree rows
- Handle gestures & custom pointers for tree rows
- Animate TreeViewNodes in and out of view

## Getting started

Expand All @@ -40,12 +52,19 @@ import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

### TableView

The code in `example/` shows a `TableView` of initially 400 cells, each varying
in sizes with a few `TableSpanDecoration`s like background colors and borders. The
`builder` constructor is called on demand for the cells that are visible in the
TableView. Additional rows can be added on demand while the vertical position
can jump between the first and last row using the buttons at the bottom of the
screen.
The code in `example/lib/table_view` has three `TableView` samples, each
showcasing different features. The `TableExample` demonstrates adding and
removing rows from the table, and applying `TableSpanDecoration`s. The
`MergedTableExample` demonstrates pinned and merged `TableViewCell`s.
Lastly, the `InfiniteTableExample` demonstrates an infinite `TableView`.

### TreeView

The code in `example/lib/tree_view` has two `TreeView` samples, each
showcasing different features. The `TreeExample` demonstrates most of
the default builders and animations. The `CustomTreeExample` demonstrates
a highly customized tree, utilizing `TreeView.treeNodeBuilder`,
`TreeView.treeRowBuilder` and `TreeView.onNodeToggle`.

## Changelog

Expand Down
@@ -0,0 +1,4 @@
# TODO(Piinks): Remove once https://github.com/flutter/flutter/pull/147202 reaches stable
buildFlags:
Piinks marked this conversation as resolved.
Show resolved Hide resolved
global:
- "--no-tree-shake-icons"
4 changes: 2 additions & 2 deletions packages/two_dimensional_scrollables/example/README.md
@@ -1,3 +1,3 @@
# TableView Example
# TableView and TreeView Examples

A sample application that utilizes the TableView API.
A sample application that utilizes the TableView and TreeView APIs.
199 changes: 40 additions & 159 deletions packages/two_dimensional_scrollables/example/lib/main.dart
Expand Up @@ -2,189 +2,70 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

// Print statements are only for illustrative purposes, not recommended for
// production applications.
// ignore_for_file: avoid_print
import 'table_view/table_explorer.dart';
import 'tree_view/tree_explorer.dart';

void main() {
runApp(const TableExampleApp());
runApp(const ExampleApp());
}

/// A sample application that utilizes the TableView API.
class TableExampleApp extends StatelessWidget {
/// Creates an instance of the TableView example app.
const TableExampleApp({super.key});
/// A sample application that utilizes the TableView and TreeView APIs.
class ExampleApp extends StatelessWidget {
/// Creates an instance of the example app.
const ExampleApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Table Example',
title: '2D Scrolling Examples',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
appBarTheme: AppBarTheme(backgroundColor: Colors.purple[50]),
),
home: const TableExample(),
home: const ExampleHome(),
routes: <String, WidgetBuilder>{
'/table': (BuildContext context) => const TableExplorer(),
'/tree': (BuildContext context) => const TreeExplorer(),
},
);
}
}

/// The class containing the TableView for the sample application.
class TableExample extends StatefulWidget {
/// The home page of the application, which directs to the tree or table
/// explorer.
class ExampleHome extends StatelessWidget {
/// Creates a screen that demonstrates the TableView widget.
const TableExample({super.key});

@override
State<TableExample> createState() => _TableExampleState();
}

class _TableExampleState extends State<TableExample> {
late final ScrollController _verticalController = ScrollController();
int _rowCount = 20;
const ExampleHome({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Table Example'),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 50),
child: TableView.builder(
verticalDetails:
ScrollableDetails.vertical(controller: _verticalController),
cellBuilder: _buildCell,
columnCount: 20,
columnBuilder: _buildColumnSpan,
rowCount: _rowCount,
rowBuilder: _buildRowSpan,
),
),
persistentFooterButtons: <Widget>[
TextButton(
onPressed: () {
_verticalController.jumpTo(0);
},
child: const Text('Jump to Top'),
),
TextButton(
onPressed: () {
_verticalController
.jumpTo(_verticalController.position.maxScrollExtent);
},
child: const Text('Jump to Bottom'),
),
TextButton(
onPressed: () {
setState(() {
_rowCount += 10;
});
},
child: const Text('Add 10 Rows'),
),
],
);
}

TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
return TableViewCell(
child: Center(
child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'),
title: const Text('Tables & Trees'),
),
);
}

TableSpan _buildColumnSpan(int index) {
const TableSpanDecoration decoration = TableSpanDecoration(
border: TableSpanBorder(
trailing: BorderSide(),
body: Center(
child: Column(children: <Widget>[
const Spacer(flex: 3),
FilledButton(
onPressed: () {
// Go to table explorer
Navigator.of(context).pushNamed('/table');
},
child: const Text('TableView Explorer'),
),
const Spacer(),
FilledButton(
onPressed: () {
// Go to tree explorer
Navigator.of(context).pushNamed('/tree');
},
child: const Text('TreeView Explorer'),
),
const Spacer(flex: 3),
]),
),
);

switch (index % 5) {
case 0:
return TableSpan(
foregroundDecoration: decoration,
extent: const FixedTableSpanExtent(100),
onEnter: (_) => print('Entered column $index'),
recognizerFactories: <Type, GestureRecognizerFactory>{
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(),
(TapGestureRecognizer t) =>
t.onTap = () => print('Tap column $index'),
),
},
);
case 1:
return TableSpan(
foregroundDecoration: decoration,
extent: const FractionalTableSpanExtent(0.5),
onEnter: (_) => print('Entered column $index'),
cursor: SystemMouseCursors.contextMenu,
);
case 2:
return TableSpan(
foregroundDecoration: decoration,
extent: const FixedTableSpanExtent(120),
onEnter: (_) => print('Entered column $index'),
);
case 3:
return TableSpan(
foregroundDecoration: decoration,
extent: const FixedTableSpanExtent(145),
onEnter: (_) => print('Entered column $index'),
);
case 4:
return TableSpan(
foregroundDecoration: decoration,
extent: const FixedTableSpanExtent(200),
onEnter: (_) => print('Entered column $index'),
);
}
throw AssertionError(
'This should be unreachable, as every index is accounted for in the switch clauses.');
}

TableSpan _buildRowSpan(int index) {
final TableSpanDecoration decoration = TableSpanDecoration(
color: index.isEven ? Colors.purple[100] : null,
border: const TableSpanBorder(
trailing: BorderSide(
width: 3,
),
),
);

switch (index % 3) {
case 0:
return TableSpan(
backgroundDecoration: decoration,
extent: const FixedTableSpanExtent(50),
recognizerFactories: <Type, GestureRecognizerFactory>{
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(),
(TapGestureRecognizer t) =>
t.onTap = () => print('Tap row $index'),
),
},
);
case 1:
return TableSpan(
backgroundDecoration: decoration,
extent: const FixedTableSpanExtent(65),
cursor: SystemMouseCursors.click,
);
case 2:
return TableSpan(
backgroundDecoration: decoration,
extent: const FractionalTableSpanExtent(0.15),
);
}
throw AssertionError(
'This should be unreachable, as every index is accounted for in the switch clauses.');
}
}