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] Merged cells for TableView #5917

Merged
merged 22 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/two_dimensional_scrollables/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.1.0

* [Breaking change] Adds support for merged cells in the TableView.

## 0.0.6

* Fixes an error in TableSpanDecoration when one or both axes are reversed.
Expand Down
8 changes: 5 additions & 3 deletions packages/two_dimensional_scrollables/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@ class _TableExampleState extends State<TableExample> {
);
}

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

Expand Down
539 changes: 455 additions & 84 deletions packages/two_dimensional_scrollables/lib/src/table_view/table.dart

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'table.dart';
Expand Down Expand Up @@ -31,13 +32,220 @@ class TableVicinity extends ChildVicinity {
/// Equivalent to the [xIndex].
int get column => xIndex;

/// The origin vicinity of the [TableView], (0,0).
static const TableVicinity zero = TableVicinity(row: 0, column: 0);

/// Returns a new [TableVicinity], copying over the row and column fields with
/// those provided, or maintaining the original values.
TableVicinity copyWith({
int? row,
int? column,
}) {
return TableVicinity(
row: row ?? this.row,
column: column ?? this.column,
);
}

@override
String toString() => '(row: $row, column: $column)';
}

/// Parent data structure used by [RenderTableViewport].
class TableViewParentData extends TwoDimensionalViewportParentData {
// TODO(Piinks): Add back merged cells here, https://github.com/flutter/flutter/issues/131224
/// Converts the [ChildVicinity] to a [TableVicinity] for ease of use.
TableVicinity get tableVicinity => vicinity as TableVicinity;

/// Represents the row index where a merged cell in the table begins.
///
/// Defaults to null, meaning a non-merged cell. A value must be provided if
/// a value is provided for [rowMergeSpan].
int? rowMergeStart;

/// Represents the number of rows spanned by a merged cell.
///
/// Defaults to null, meaning the cell is not merged. A value must be provided
/// if a value is provided for [rowMergeStart].
int? rowMergeSpan;

/// Represents the column index where a merged cell in the table begins.
///
/// Defaults to null, meaning a non-merged cell. A value must be provided if
/// a value is provided for [columnMergeSpan].
int? columnMergeStart;

/// Represents the number of columns spanned by a merged cell.
///
/// Defaults to null, meaning the cell is not merged. A value must be provided
/// if a value is provided for [columnMergeStart].
int? columnMergeSpan;

@override
String toString() {
String mergeDetails = '';
if (rowMergeStart != null || columnMergeStart != null) {
mergeDetails += ', merged';
}
if (rowMergeStart != null) {
mergeDetails += ', rowMergeStart=$rowMergeStart, '
'rowMergeSpan=$rowMergeSpan';
}
if (columnMergeStart != null) {
mergeDetails += ', columnMergeStart=$columnMergeStart, '
'columnMergeSpan=$columnMergeSpan';
}
return super.toString() + mergeDetails;
}
}

/// Creates a cell of the [TableView], along with information regarding merged
/// cells and [RepaintBoundary]s.
///
/// When merging cells in a [TableView], the same child should be returned from
/// every vicinity the merged cell contains. The `build` method will only be
/// called once for a merged cell, but since the table's children are lazily
/// laid out, returning the same child ensures the merged cell can be built no
/// matter which part of it is visible.
class TableViewCell extends StatelessWidget {
/// Creates a widget that controls how a child of a [TableView] spans across
/// multiple rows or columns.
const TableViewCell({
super.key,
this.rowMergeStart,
this.rowMergeSpan,
this.columnMergeStart,
this.columnMergeSpan,
this.addRepaintBoundaries = true,
required this.child,
}) : assert(
(rowMergeStart == null && rowMergeSpan == null) ||
(rowMergeStart != null && rowMergeSpan != null),
'Row merge start and span must both be set, or both unset.',
),
assert(rowMergeStart == null || rowMergeStart >= 0),
assert(rowMergeSpan == null || rowMergeSpan > 0),
assert(
(columnMergeStart == null && columnMergeSpan == null) ||
(columnMergeStart != null && columnMergeSpan != null),
'Column merge start and span must both be set, or both unset.',
),
assert(columnMergeStart == null || columnMergeStart >= 0),
assert(columnMergeSpan == null || columnMergeSpan > 0);

/// The child contained in this cell of the [TableView].
final Widget child;

/// Represents the row index where a merged cell in the table begins.
///
/// Defaults to null, meaning a non-merged cell. A value must be provided if
/// a value is provided for [rowMergeSpan].
final int? rowMergeStart;

/// Represents the number of rows spanned by a merged cell.
///
/// Defaults to null, meaning the cell is not merged. A value must be provided
/// if a value is provided for [rowMergeStart].
final int? rowMergeSpan;

/// Represents the column index where a merged cell in the table begins.
///
/// Defaults to null, meaning a non-merged cell. A value must be provided if
/// a value is provided for [columnMergeSpan].
final int? columnMergeStart;

/// Represents the number of columns spanned by a merged cell.
///
/// Defaults to null, meaning the cell is not merged. A value must be provided
/// if a value is provided for [columnMergeStart].
final int? columnMergeSpan;

/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and instead always repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;

@override
Widget build(BuildContext context) {
Widget child = this.child;

if (addRepaintBoundaries) {
child = RepaintBoundary(child: child);
}

return _TableViewCell(
rowMergeStart: rowMergeStart,
rowMergeSpan: rowMergeSpan,
columnMergeStart: columnMergeStart,
columnMergeSpan: columnMergeSpan,
child: child,
);
}
}

class _TableViewCell extends ParentDataWidget<TableViewParentData> {
const _TableViewCell({
this.rowMergeStart,
this.rowMergeSpan,
this.columnMergeStart,
this.columnMergeSpan,
required super.child,
});

final int? rowMergeStart;
final int? rowMergeSpan;
final int? columnMergeStart;
final int? columnMergeSpan;

@override
void applyParentData(RenderObject renderObject) {
final TableViewParentData parentData =
renderObject.parentData! as TableViewParentData;
bool needsLayout = false;
if (parentData.rowMergeStart != rowMergeStart) {
assert(rowMergeStart == null || rowMergeStart! >= 0);
parentData.rowMergeStart = rowMergeStart;
needsLayout = true;
}
if (parentData.rowMergeSpan != rowMergeSpan) {
assert(rowMergeSpan == null || rowMergeSpan! > 0);
parentData.rowMergeSpan = rowMergeSpan;
needsLayout = true;
}
if (parentData.columnMergeStart != columnMergeStart) {
assert(columnMergeStart == null || columnMergeStart! >= 0);
parentData.columnMergeStart = columnMergeStart;
needsLayout = true;
}
if (parentData.columnMergeSpan != columnMergeSpan) {
assert(columnMergeSpan == null || columnMergeSpan! > 0);
parentData.columnMergeSpan = columnMergeSpan;
needsLayout = true;
}

if (needsLayout) {
renderObject.parent?.markNeedsLayout();
}
}

@override
Type get debugTypicalAncestorWidgetClass => TableViewport;

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
if (rowMergeStart != null) {
properties.add(IntProperty('rowMergeStart', rowMergeStart));
properties.add(IntProperty('rowMergeSpan', rowMergeSpan));
}
if (columnMergeStart != null) {
properties.add(IntProperty('columnMergeStart', columnMergeStart));
properties.add(IntProperty('columnMergeSpan', columnMergeSpan));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import 'table_span.dart';
/// [TableView].
typedef TableSpanBuilder = TableSpan Function(int index);

/// Signature for a function that creates a child [Widget] for a given
/// Signature for a function that creates a child [TableViewCell] for a given
/// [TableVicinity] in a [TableView], but may return null.
///
/// Used by [TableCellBuilderDelegate.builder] to build cells on demand for the
/// table.
typedef TableViewCellBuilder = Widget? Function(
typedef TableViewCellBuilder = TableViewCell Function(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't notice this before: Is this intentional that null is no longer allowed as a return value? If so, the doc above needs updating. (Remind me: what did it mean when this was previously returning null? What are we no longer allowing?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This felt like a bug I came across. Typically in Flutter scrolling land, returning null from the child delegate means we have reached the end of the children. This is not currently supported, but is part of supporting infinite children in the table: flutter/flutter#131226.

I think it was here by mistake since if you return null currently, with or without this change, it will crash! :(

BuildContext context,
TableVicinity vicinity,
);
Expand Down Expand Up @@ -116,6 +116,10 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate {

/// A delegate that supplies children for a [TableViewport] on demand using a
/// builder callback.
///
/// Unlike the base [TwoDimensionalChildBuilderDelegate] this delegate does not
/// automatically insert repaint boundaries. Instead, repaint boundaries are
/// controlled by [TableViewCell.addRepaintBoundaries].
class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate
with TableCellDelegateMixin {
/// Creates a lazy building delegate to use with a [TableView].
Expand All @@ -124,7 +128,6 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate
required int rowCount,
int pinnedColumnCount = 0,
int pinnedRowCount = 0,
super.addRepaintBoundaries,
super.addAutomaticKeepAlives,
required TableViewCellBuilder cellBuilder,
required TableSpanBuilder columnBuilder,
Expand All @@ -144,6 +147,8 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate
cellBuilder(context, vicinity as TableVicinity),
maxXIndex: columnCount - 1,
maxYIndex: rowCount - 1,
// repaintBoundaries handled by TableViewCell
addRepaintBoundaries: false,
);

@override
Expand Down Expand Up @@ -209,15 +214,18 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate
/// The [children] are accessed for each [TableVicinity.row] and
/// [TableVicinity.column] of the [TwoDimensionalViewport] as
/// `children[vicinity.row][vicinity.column]`.
///
/// Unlike the base [TwoDimensionalChildBuilderDelegate] this delegate does not
/// automatically insert repaint boundaries. Instead, repaint boundaries are
/// controlled by [TableViewCell.addRepaintBoundaries].
class TableCellListDelegate extends TwoDimensionalChildListDelegate
with TableCellDelegateMixin {
/// Creates a delegate that supplies children for a [TableView].
TableCellListDelegate({
int pinnedColumnCount = 0,
int pinnedRowCount = 0,
super.addRepaintBoundaries,
super.addAutomaticKeepAlives,
required List<List<Widget>> cells,
required List<List<TableViewCell>> cells,
required TableSpanBuilder columnBuilder,
required TableSpanBuilder rowBuilder,
}) : assert(pinnedColumnCount >= 0),
Expand All @@ -226,10 +234,16 @@ class TableCellListDelegate extends TwoDimensionalChildListDelegate
_rowBuilder = rowBuilder,
_pinnedColumnCount = pinnedColumnCount,
_pinnedRowCount = pinnedRowCount,
super(children: cells) {
super(
children: cells,
// repaintBoundaries handled by TableViewCell
addRepaintBoundaries: false,
) {
// Even if there are merged cells, they should be represented by the same
// child in each cell location. So all arrays of cells should have the same
// length.
// child in each cell location. This ensures that no matter which direction
// the merged cell scrolls into view from, we can build the correct child
// without having to explore all possible vicinities of the merged cell
// area. So all arrays of cells should have the same length.
assert(
children.map((List<Widget> array) => array.length).toSet().length == 1,
'Each list of Widgets within cells must be of the same length.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ class MinTableSpanExtent extends CombiningTableSpanExtent {
}

/// A decoration for a [TableSpan].
///
/// When decorating merged cells in the [TableView], a merged cell will take its
/// decoration from the leading cell of the merged span.
class TableSpanDecoration {
/// Creates a [TableSpanDecoration].
const TableSpanDecoration({
Expand Down
2 changes: 1 addition & 1 deletion packages/two_dimensional_scrollables/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: two_dimensional_scrollables
description: Widgets that scroll using the two dimensional scrolling foundation.
version: 0.0.6
version: 0.1.0
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+

Expand Down
Loading