diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index 62cdc53d1cd..41f116eb3d6 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 0.3.5 * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. +* Updates the simple table sample to demonstrate different types of selection: single-cell +selection, and multi-cell selection. ## 0.3.4 diff --git a/packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart b/packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart index 5e23bb20e95..1a6298f3c16 100644 --- a/packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart +++ b/packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart @@ -19,8 +19,11 @@ class TableExample extends StatefulWidget { State createState() => _TableExampleState(); } +enum _TableSelection { multiCell, singleCell, disabled } + class _TableExampleState extends State { late final ScrollController _verticalController = ScrollController(); + _TableSelection _selectionMode = _TableSelection.disabled; int _rowCount = 20; @override @@ -34,57 +37,114 @@ class _TableExampleState extends State { return Scaffold( body: Padding( padding: const EdgeInsets.symmetric(horizontal: 50.0), - child: TableView.builder( - verticalDetails: ScrollableDetails.vertical( - controller: _verticalController, - ), - cellBuilder: _buildCell, - columnCount: 20, - columnBuilder: _buildColumnSpan, - rowCount: _rowCount, - rowBuilder: _buildRowSpan, - ), + child: _selectionMode == _TableSelection.multiCell + ? SelectionArea( + child: TableView.builder( + verticalDetails: ScrollableDetails.vertical( + controller: _verticalController, + ), + cellBuilder: _buildCell, + columnCount: 20, + columnBuilder: _buildColumnSpan, + rowCount: _rowCount, + rowBuilder: _buildRowSpan, + ), + ) + : TableView.builder( + verticalDetails: ScrollableDetails.vertical( + controller: _verticalController, + ), + cellBuilder: _buildCell, + columnCount: 20, + columnBuilder: _buildColumnSpan, + rowCount: _rowCount, + rowBuilder: _buildRowSpan, + ), ), persistentFooterButtons: [ - 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'), + OverflowBar( + alignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Selection'), + SegmentedButton<_TableSelection>( + segments: const >[ + ButtonSegment<_TableSelection>( + value: _TableSelection.multiCell, + label: Text('Multi-Cell'), + icon: Icon(Icons.layers), + ), + ButtonSegment<_TableSelection>( + value: _TableSelection.singleCell, + label: Text('Single-Cell'), + icon: Icon(Icons.crop_square), + ), + ButtonSegment<_TableSelection>( + value: _TableSelection.disabled, + label: Text('Disabled'), + icon: Icon(Icons.disabled_by_default), + ), + ], + selected: <_TableSelection>{_selectionMode}, + onSelectionChanged: (Set<_TableSelection> newSelectionMode) { + setState(() { + // By default there is only a single segment that can be + // selected at one time, so its value is always the first + // item in the selected set. + _selectionMode = newSelectionMode.first; + }); + }, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + 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}'), - ), + Widget result = Center( + child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), ); + if (_selectionMode == _TableSelection.singleCell) { + result = SelectionArea(child: result); + } + return TableViewCell(child: result); } TableSpan _buildColumnSpan(int index) { const TableSpanDecoration decoration = TableSpanDecoration( - border: TableSpanBorder( - trailing: BorderSide(), - ), + border: TableSpanBorder(trailing: BorderSide()), ); switch (index % 5) { @@ -137,11 +197,7 @@ class _TableExampleState extends State { TableSpan _buildRowSpan(int index) { final TableSpanDecoration decoration = TableSpanDecoration( color: index.isEven ? Colors.purple[100] : null, - border: const TableSpanBorder( - trailing: BorderSide( - width: 3, - ), - ), + border: const TableSpanBorder(trailing: BorderSide(width: 3)), ); switch (index % 3) { diff --git a/packages/two_dimensional_scrollables/example/test/table_view/simple_table_test.dart b/packages/two_dimensional_scrollables/example/test/table_view/simple_table_test.dart index 0186c4d68ba..60cfb381f62 100644 --- a/packages/two_dimensional_scrollables/example/test/table_view/simple_table_test.dart +++ b/packages/two_dimensional_scrollables/example/test/table_view/simple_table_test.dart @@ -2,7 +2,9 @@ // 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:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:two_dimensional_examples/table_view/simple_table.dart'; @@ -54,4 +56,91 @@ void main() { await tester.pump(); expect(position.pixels, 0.0); }); + + testWidgets('Selection SegmentedButton control works', + (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: TableExample())); + await tester.pump(); + + // Find two adjacent cells. Adjust these finders as needed for your specific layout. + final Finder cell1 = find.text('Tile c: 0, r: 0'); + final Finder cell2 = find.text('Tile c: 1, r: 0'); + + final Offset cell1Center = tester.getCenter(cell1); + final Offset cell2Center = tester.getCenter(cell2); + + // Enable multi-cell selection and verify. + await tester.tap(find.textContaining('Multi-Cell')); + await tester.pumpAndSettle(); + + RenderParagraph paragraph1 = tester.renderObject( + find.descendant(of: cell1, matching: find.byType(RichText)), + ); + RenderParagraph paragraph2 = tester.renderObject( + find.descendant(of: cell2, matching: find.byType(RichText)), + ); + + // Selection starts empty. + expect(paragraph1.selections.isEmpty, isTrue); + expect(paragraph2.selections.isEmpty, isTrue); + + // Long press and drag to select multiple cells. + final TestGesture gesture = await tester.startGesture(cell1Center); + await tester.pump(kLongPressTimeout); + await gesture.moveTo(cell2Center); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(paragraph1.selections.isEmpty, isFalse); + expect(paragraph2.selections.isEmpty, isFalse); + + // Enable single-cell selection and verify. + await tester.tap(find.textContaining('Single-Cell')); + await tester.pumpAndSettle(); + + paragraph1 = tester.renderObject( + find.descendant(of: cell1, matching: find.byType(RichText)), + ); + paragraph2 = tester.renderObject( + find.descendant(of: cell2, matching: find.byType(RichText)), + ); + + // Selection has been cleared. + expect(paragraph1.selections.isEmpty, isTrue); + expect(paragraph2.selections.isEmpty, isTrue); + + // Selecting from cell1 to cell2 only selects cell1. + await gesture.down(cell1Center); + await tester.pump(kLongPressTimeout); + await gesture.moveTo(cell2Center); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(paragraph1.selections.isEmpty, isFalse); + expect(paragraph2.selections.isEmpty, isTrue); + + // Disable selection and verify. + await tester.tap(find.text('Disabled')); + await tester.pumpAndSettle(); + + paragraph1 = tester.renderObject( + find.descendant(of: cell1, matching: find.byType(RichText)), + ); + paragraph2 = tester.renderObject( + find.descendant(of: cell2, matching: find.byType(RichText)), + ); + + // Selection has been cleared. + expect(paragraph1.selections.isEmpty, isTrue); + expect(paragraph2.selections.isEmpty, isTrue); + + // Long pressing should not select anything. + await gesture.down(cell1Center); + await tester.pump(kLongPressTimeout); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(paragraph1.selections.isEmpty, isTrue); + expect(paragraph2.selections.isEmpty, isTrue); + }); } diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 8549dd85dcc..e4babe71160 100644 --- a/packages/two_dimensional_scrollables/pubspec.yaml +++ b/packages/two_dimensional_scrollables/pubspec.yaml @@ -1,6 +1,6 @@ name: two_dimensional_scrollables description: Widgets that scroll using the two dimensional scrolling foundation. -version: 0.3.4 +version: 0.3.5 repository: https://github.com/flutter/packages/tree/main/packages/two_dimensional_scrollables issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+two_dimensional_scrollables%22+