assembleSemanticsNode method
void
assembleSemanticsNode(
- SemanticsNode node,
- SemanticsConfiguration config,
- Iterable<
SemanticsNode> children
override
Builds the accessibility tree the paint pass would otherwise have thrown away.
A widget table gets this for free and pays for it in widgets. Here it is assembled from the same cell specs the painter reads, for the window only, and the nodes are recycled across updates — so a screen reader sees a proper table of rows and cells while the cost still tracks the viewport rather than the dataset.
Implementation
@override
void assembleSemanticsNode(
SemanticsNode node,
SemanticsConfiguration config,
Iterable<SemanticsNode> children,
) {
final rows = <SemanticsNode>[];
final liveRowKeys = <int>{};
final liveCellKeys = <int>{};
final (firstScroll, lastScroll) = _visibleScrollableRange;
final layout = _columnLayout;
for (var row = _firstVisibleRow; row < _lastVisibleRow; row++) {
if (_fullRow(row)) continue;
final cells = <SemanticsNode>[];
final rowTop = _rowMetrics.offsetOf(row) - _verticalOffset;
final rowHeight = _rowMetrics.heightOf(row);
void addColumn(int column) {
final width = layout.widths[column];
final left = _screenLeft(column);
// A cell scrolled entirely out of its band is laid out but not
// announced: it is not on screen, and a screen reader that walks it
// would read the grid in an order the eye cannot follow.
if (left + width <= 0 || left >= size.width) return;
final key = _cellKey(row, column);
liveCellKeys.add(key);
final cellNode = _cellNodes.putIfAbsent(key, SemanticsNode.new);
final spec = _cellSpec(row, column);
final cellConfig = SemanticsConfiguration()
..role = SemanticsRole.cell
..isReadOnly = true
..label = _semanticLabelFor(column, spec)
..textDirection = _textDirection;
if (_onCellActivate != null) {
cellConfig.onTap = () => _onCellActivate!(row, column);
}
if (row == _focusedRow && column == _focusedColumn) {
cellConfig.isFocused = true;
}
if (_inRange(row, column)) cellConfig.isSelected = true;
cellNode
..rect = Rect.fromLTWH(left, 0, width, rowHeight)
..updateWith(config: cellConfig, childrenInInversePaintOrder: null);
cells.add(cellNode);
}
for (var c = 0; c < layout.leadingFrozenCount; c++) {
addColumn(c);
}
for (var c = firstScroll; c < lastScroll; c++) {
addColumn(c);
}
for (var c = layout.trailingFrozenStart; c < layout.length; c++) {
addColumn(c);
}
if (cells.isEmpty) continue;
liveRowKeys.add(row);
final rowNode = _rowNodes.putIfAbsent(row, SemanticsNode.new);
final rowConfig = SemanticsConfiguration()
..role = SemanticsRole.row
..isSelected = _isRowSelected?.call(row) ?? false
..indexInParent = _rowIndexOffset + row
..sortKey = OrdinalSortKey((_rowIndexOffset + row).toDouble());
rowNode
..rect = Rect.fromLTWH(0, rowTop, size.width, rowHeight)
..updateWith(config: rowConfig, childrenInInversePaintOrder: cells);
rows.add(rowNode);
}
_rowNodes.removeWhere((key, _) => !liveRowKeys.contains(key));
_cellNodes.removeWhere((key, _) => !liveCellKeys.contains(key));
// The rows hang off a node of their own rather than off this one. A node
// with the `table` role may only have rows beneath it, and an open editor
// is a real render object with real semantics that has to go somewhere —
// so the table is a child here, and the overlay children are its siblings.
final table = _tableNode ??= SemanticsNode();
table
..rect = Offset.zero & size
..updateWith(
config: SemanticsConfiguration()..role = SemanticsRole.table,
childrenInInversePaintOrder: rows,
);
node.updateWith(
config: config,
childrenInInversePaintOrder: <SemanticsNode>[table, ...children],
);
}