Skip to content

Commit da1d32f

Browse files
[two_dimensional_scrollables] Enable multi-cell/single-cell text selection in Simple Table example (flutter#8189)
This change adds a `SegmentedButton` to enable different variations of text selection for the example. * Multi-cell selection, enables selection across the entire table, allowing you to select across cells. * Single-cell selection, enables selection on individual cells, so the selection cannot escape the cell it started in. * Disabled, disables selection across the entire table. Text selection in a table is a common use-case that we should showcase.
1 parent 6e2acf7 commit da1d32f

File tree

4 files changed

+192
-45
lines changed

4 files changed

+192
-45
lines changed

packages/two_dimensional_scrollables/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
## NEXT
1+
## 0.3.5
22

33
* Updates minimum supported SDK version to Flutter 3.27/Dart 3.6.
4+
* Updates the simple table sample to demonstrate different types of selection: single-cell
5+
selection, and multi-cell selection.
46

57
## 0.3.4
68

packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart

Lines changed: 99 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ class TableExample extends StatefulWidget {
1919
State<TableExample> createState() => _TableExampleState();
2020
}
2121

22+
enum _TableSelection { multiCell, singleCell, disabled }
23+
2224
class _TableExampleState extends State<TableExample> {
2325
late final ScrollController _verticalController = ScrollController();
26+
_TableSelection _selectionMode = _TableSelection.disabled;
2427
int _rowCount = 20;
2528

2629
@override
@@ -34,57 +37,114 @@ class _TableExampleState extends State<TableExample> {
3437
return Scaffold(
3538
body: Padding(
3639
padding: const EdgeInsets.symmetric(horizontal: 50.0),
37-
child: TableView.builder(
38-
verticalDetails: ScrollableDetails.vertical(
39-
controller: _verticalController,
40-
),
41-
cellBuilder: _buildCell,
42-
columnCount: 20,
43-
columnBuilder: _buildColumnSpan,
44-
rowCount: _rowCount,
45-
rowBuilder: _buildRowSpan,
46-
),
40+
child: _selectionMode == _TableSelection.multiCell
41+
? SelectionArea(
42+
child: TableView.builder(
43+
verticalDetails: ScrollableDetails.vertical(
44+
controller: _verticalController,
45+
),
46+
cellBuilder: _buildCell,
47+
columnCount: 20,
48+
columnBuilder: _buildColumnSpan,
49+
rowCount: _rowCount,
50+
rowBuilder: _buildRowSpan,
51+
),
52+
)
53+
: TableView.builder(
54+
verticalDetails: ScrollableDetails.vertical(
55+
controller: _verticalController,
56+
),
57+
cellBuilder: _buildCell,
58+
columnCount: 20,
59+
columnBuilder: _buildColumnSpan,
60+
rowCount: _rowCount,
61+
rowBuilder: _buildRowSpan,
62+
),
4763
),
4864
persistentFooterButtons: <Widget>[
49-
TextButton(
50-
onPressed: () {
51-
_verticalController.jumpTo(0);
52-
},
53-
child: const Text('Jump to Top'),
54-
),
55-
TextButton(
56-
onPressed: () {
57-
_verticalController.jumpTo(
58-
_verticalController.position.maxScrollExtent,
59-
);
60-
},
61-
child: const Text('Jump to Bottom'),
62-
),
63-
TextButton(
64-
onPressed: () {
65-
setState(() {
66-
_rowCount += 10;
67-
});
68-
},
69-
child: const Text('Add 10 Rows'),
65+
OverflowBar(
66+
alignment: MainAxisAlignment.spaceEvenly,
67+
children: <Widget>[
68+
Column(
69+
mainAxisSize: MainAxisSize.min,
70+
children: <Widget>[
71+
const Text('Selection'),
72+
SegmentedButton<_TableSelection>(
73+
segments: const <ButtonSegment<_TableSelection>>[
74+
ButtonSegment<_TableSelection>(
75+
value: _TableSelection.multiCell,
76+
label: Text('Multi-Cell'),
77+
icon: Icon(Icons.layers),
78+
),
79+
ButtonSegment<_TableSelection>(
80+
value: _TableSelection.singleCell,
81+
label: Text('Single-Cell'),
82+
icon: Icon(Icons.crop_square),
83+
),
84+
ButtonSegment<_TableSelection>(
85+
value: _TableSelection.disabled,
86+
label: Text('Disabled'),
87+
icon: Icon(Icons.disabled_by_default),
88+
),
89+
],
90+
selected: <_TableSelection>{_selectionMode},
91+
onSelectionChanged: (Set<_TableSelection> newSelectionMode) {
92+
setState(() {
93+
// By default there is only a single segment that can be
94+
// selected at one time, so its value is always the first
95+
// item in the selected set.
96+
_selectionMode = newSelectionMode.first;
97+
});
98+
},
99+
),
100+
],
101+
),
102+
Row(
103+
mainAxisSize: MainAxisSize.min,
104+
children: <Widget>[
105+
TextButton(
106+
onPressed: () {
107+
_verticalController.jumpTo(0);
108+
},
109+
child: const Text('Jump to Top'),
110+
),
111+
TextButton(
112+
onPressed: () {
113+
_verticalController.jumpTo(
114+
_verticalController.position.maxScrollExtent,
115+
);
116+
},
117+
child: const Text('Jump to Bottom'),
118+
),
119+
TextButton(
120+
onPressed: () {
121+
setState(() {
122+
_rowCount += 10;
123+
});
124+
},
125+
child: const Text('Add 10 Rows'),
126+
),
127+
],
128+
),
129+
],
70130
),
71131
],
72132
);
73133
}
74134

75135
TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
76-
return TableViewCell(
77-
child: Center(
78-
child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'),
79-
),
136+
Widget result = Center(
137+
child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'),
80138
);
139+
if (_selectionMode == _TableSelection.singleCell) {
140+
result = SelectionArea(child: result);
141+
}
142+
return TableViewCell(child: result);
81143
}
82144

83145
TableSpan _buildColumnSpan(int index) {
84146
const TableSpanDecoration decoration = TableSpanDecoration(
85-
border: TableSpanBorder(
86-
trailing: BorderSide(),
87-
),
147+
border: TableSpanBorder(trailing: BorderSide()),
88148
);
89149

90150
switch (index % 5) {
@@ -137,11 +197,7 @@ class _TableExampleState extends State<TableExample> {
137197
TableSpan _buildRowSpan(int index) {
138198
final TableSpanDecoration decoration = TableSpanDecoration(
139199
color: index.isEven ? Colors.purple[100] : null,
140-
border: const TableSpanBorder(
141-
trailing: BorderSide(
142-
width: 3,
143-
),
144-
),
200+
border: const TableSpanBorder(trailing: BorderSide(width: 3)),
145201
);
146202

147203
switch (index % 3) {

packages/two_dimensional_scrollables/example/test/table_view/simple_table_test.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/gestures.dart';
56
import 'package:flutter/material.dart';
7+
import 'package:flutter/rendering.dart';
68
import 'package:flutter_test/flutter_test.dart';
79
import 'package:two_dimensional_examples/table_view/simple_table.dart';
810

@@ -54,4 +56,91 @@ void main() {
5456
await tester.pump();
5557
expect(position.pixels, 0.0);
5658
});
59+
60+
testWidgets('Selection SegmentedButton control works',
61+
(WidgetTester tester) async {
62+
await tester.pumpWidget(const MaterialApp(home: TableExample()));
63+
await tester.pump();
64+
65+
// Find two adjacent cells. Adjust these finders as needed for your specific layout.
66+
final Finder cell1 = find.text('Tile c: 0, r: 0');
67+
final Finder cell2 = find.text('Tile c: 1, r: 0');
68+
69+
final Offset cell1Center = tester.getCenter(cell1);
70+
final Offset cell2Center = tester.getCenter(cell2);
71+
72+
// Enable multi-cell selection and verify.
73+
await tester.tap(find.textContaining('Multi-Cell'));
74+
await tester.pumpAndSettle();
75+
76+
RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
77+
find.descendant(of: cell1, matching: find.byType(RichText)),
78+
);
79+
RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
80+
find.descendant(of: cell2, matching: find.byType(RichText)),
81+
);
82+
83+
// Selection starts empty.
84+
expect(paragraph1.selections.isEmpty, isTrue);
85+
expect(paragraph2.selections.isEmpty, isTrue);
86+
87+
// Long press and drag to select multiple cells.
88+
final TestGesture gesture = await tester.startGesture(cell1Center);
89+
await tester.pump(kLongPressTimeout);
90+
await gesture.moveTo(cell2Center);
91+
await gesture.up();
92+
await tester.pumpAndSettle();
93+
94+
expect(paragraph1.selections.isEmpty, isFalse);
95+
expect(paragraph2.selections.isEmpty, isFalse);
96+
97+
// Enable single-cell selection and verify.
98+
await tester.tap(find.textContaining('Single-Cell'));
99+
await tester.pumpAndSettle();
100+
101+
paragraph1 = tester.renderObject<RenderParagraph>(
102+
find.descendant(of: cell1, matching: find.byType(RichText)),
103+
);
104+
paragraph2 = tester.renderObject<RenderParagraph>(
105+
find.descendant(of: cell2, matching: find.byType(RichText)),
106+
);
107+
108+
// Selection has been cleared.
109+
expect(paragraph1.selections.isEmpty, isTrue);
110+
expect(paragraph2.selections.isEmpty, isTrue);
111+
112+
// Selecting from cell1 to cell2 only selects cell1.
113+
await gesture.down(cell1Center);
114+
await tester.pump(kLongPressTimeout);
115+
await gesture.moveTo(cell2Center);
116+
await gesture.up();
117+
await tester.pumpAndSettle();
118+
119+
expect(paragraph1.selections.isEmpty, isFalse);
120+
expect(paragraph2.selections.isEmpty, isTrue);
121+
122+
// Disable selection and verify.
123+
await tester.tap(find.text('Disabled'));
124+
await tester.pumpAndSettle();
125+
126+
paragraph1 = tester.renderObject<RenderParagraph>(
127+
find.descendant(of: cell1, matching: find.byType(RichText)),
128+
);
129+
paragraph2 = tester.renderObject<RenderParagraph>(
130+
find.descendant(of: cell2, matching: find.byType(RichText)),
131+
);
132+
133+
// Selection has been cleared.
134+
expect(paragraph1.selections.isEmpty, isTrue);
135+
expect(paragraph2.selections.isEmpty, isTrue);
136+
137+
// Long pressing should not select anything.
138+
await gesture.down(cell1Center);
139+
await tester.pump(kLongPressTimeout);
140+
await gesture.up();
141+
await tester.pumpAndSettle();
142+
143+
expect(paragraph1.selections.isEmpty, isTrue);
144+
expect(paragraph2.selections.isEmpty, isTrue);
145+
});
57146
}

packages/two_dimensional_scrollables/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: two_dimensional_scrollables
22
description: Widgets that scroll using the two dimensional scrolling foundation.
3-
version: 0.3.4
3+
version: 0.3.5
44
repository: https://github.com/flutter/packages/tree/main/packages/two_dimensional_scrollables
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+two_dimensional_scrollables%22+
66

0 commit comments

Comments
 (0)