sendSemanticsUpdate method
Update the semantics using onSemanticsUpdate.
Implementation
void sendSemanticsUpdate() {
// Once the tree is up-to-date, verify that every node is visible.
assert(() {
final invisibleNodes = <SemanticsNode>[];
// Finds the invisible nodes in the tree rooted at `node` and adds them to
// the invisibleNodes list. If a node is itself invisible, all its
// descendants will be skipped.
bool findInvisibleNodes(SemanticsNode node) {
if (node.rect.isEmpty) {
invisibleNodes.add(node);
} else if (!node.mergeAllDescendantsIntoThisNode) {
node.visitChildren(findInvisibleNodes);
}
return true;
}
final SemanticsNode? rootSemanticsNode = this.rootSemanticsNode;
if (rootSemanticsNode != null) {
// The root node is allowed to be invisible when it has no children.
if (rootSemanticsNode.childrenCount > 0 && rootSemanticsNode.rect.isEmpty) {
invisibleNodes.add(rootSemanticsNode);
} else if (!rootSemanticsNode.mergeAllDescendantsIntoThisNode) {
rootSemanticsNode.visitChildren(findInvisibleNodes);
}
}
if (invisibleNodes.isEmpty) {
return true;
}
List<DiagnosticsNode> nodeToMessage(SemanticsNode invisibleNode) {
final SemanticsNode? parent = invisibleNode.parent;
return <DiagnosticsNode>[
invisibleNode.toDiagnosticsNode(style: DiagnosticsTreeStyle.errorProperty),
parent?.toDiagnosticsNode(
name: 'which was added as a child of',
style: DiagnosticsTreeStyle.errorProperty,
) ??
ErrorDescription('which was added as the root SemanticsNode'),
];
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Invisible SemanticsNodes should not be added to the tree.'),
ErrorDescription('The following invisible SemanticsNodes were added to the tree:'),
...invisibleNodes.expand(nodeToMessage),
ErrorHint(
'An invisible SemanticsNode is one whose rect is not on screen hence not reachable for users, '
'and its semantic information is not merged into a visible parent.',
),
ErrorHint(
'An invisible SemanticsNode makes the accessibility experience confusing, '
'as it does not provide any visual indication when the user selects it '
'via accessibility technologies.',
),
ErrorHint(
'Consider removing the above invisible SemanticsNodes if they were added by your '
'RenderObject.assembleSemanticsNode implementation, or filing a bug on GitHub:\n'
' https://github.com/flutter/flutter/issues/new?template=02_bug.yml',
),
]);
}());
if (_dirtyNodes.isEmpty) {
return;
}
final customSemanticsActionIds = <int>{};
final visitedNodes = <SemanticsNode>[];
while (_dirtyNodes.isNotEmpty) {
final List<SemanticsNode> localDirtyNodes = _dirtyNodes
.where((SemanticsNode node) => !_detachedNodes.contains(node))
.toList();
_dirtyNodes.clear();
_detachedNodes.clear();
localDirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
visitedNodes.addAll(localDirtyNodes);
for (final node in localDirtyNodes) {
assert(node._dirty);
assert(node.parent == null || !node.parent!.isPartOfNodeMerging || node.isMergedIntoParent);
if (node.isPartOfNodeMerging) {
assert(node.mergeAllDescendantsIntoThisNode || node.parent != null);
// If child node is merged into its parent, make sure the parent is marked as dirty
if (node.parent != null && node.parent!.isPartOfNodeMerging) {
node.parent!._markDirty(); // this can add the node to the dirty list
node._dirty = false; // Do not send update for this node, as it's now part of its parent
}
}
// Clean up the dirty entry in owner._traversalParentNodes map because it
// will be updated later.
_traversalParentNodes.removeWhere((Object key, SemanticsNode oldNode) => node == oldNode);
// Clean up the node from the value set in owner._traversalChildNodes.
for (final Set<SemanticsNode> childSet in _traversalChildNodes.values) {
childSet.removeWhere((SemanticsNode oldNode) => node == oldNode);
}
}
}
visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
final SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder();
final updatedVisitedNodes = <SemanticsNode>[];
for (final node in visitedNodes) {
final bool isTraversalParent = node._isTraversalParent;
final bool isTraversalChild = node._isTraversalChild;
if (kIsWeb) {
updatedVisitedNodes.add(node);
} else {
if (!isTraversalParent && !isTraversalChild) {
updatedVisitedNodes.add(node);
continue;
}
if (isTraversalChild) {
// If the node has a non-null `_traversalChildIdentifier`, it indicates
// that its hit-test parent and traversal parent are different, and
// its traversal parent should update its children to include this node.
// Therefore, its traversal parent node should be added to the
// `updatedVisitedNodes` list for later grafting, in order to generate
// a correct `childrenIntraversalOrder`. This is typically used in
// `OverlayPortal` widget.
final SemanticsNode? parentNode = _traversalParentNodes[node.traversalChildIdentifier];
if (parentNode != null && !updatedVisitedNodes.contains(parentNode)) {
updatedVisitedNodes.add(parentNode);
}
}
updatedVisitedNodes.add(node);
}
// If the node is a traversal parent, then add it to the
// _traversalParentNodes map for later grafting. Similarly, add the node
// to the _traversalChildNodes map if it is a traversal child.
if (isTraversalParent) {
assert(
!_traversalParentNodes.containsKey(node._traversalParentIdentifier) ||
_traversalParentNodes[node.traversalParentIdentifier!] == node,
'The traversalParentIdentifier must be unique. No two semantics nodes can share the same traversalParentIdentifier.',
);
_traversalParentNodes[node.traversalParentIdentifier!] = node;
} else if (isTraversalChild) {
_traversalChildNodes[node.traversalChildIdentifier!] ??= <SemanticsNode>{};
_traversalChildNodes[node.traversalChildIdentifier!]!.add(node);
}
}
for (final node in updatedVisitedNodes) {
assert(
node.parent?._dirty != true || node._isTraversalParent,
); // could be null (no parent) or false (not dirty)
// The traversalParentNode is added to updatedVisitedNodes for later
// grafting; its traversalChildren should be grafted to its children in
// the traversal order. This grafting process is skipped on web because
// the traversal order will be handled in the web engine.
final bool needUpdateTraversalParent = !kIsWeb && node._isTraversalParent;
// The _serialize() method marks the node as not dirty, and
// recurses through the tree to do a deep serialization of all
// contiguous dirty nodes. This means that when we return here,
// it's quite possible that subsequent nodes are no longer
// dirty. We skip these here.
// We also skip any nodes that were reset and subsequently
// dropped entirely (RenderObject.markNeedsSemanticsUpdate()
// calls reset() on its SemanticsNode if onlyChanges isn't set,
// which happens e.g. when the node is no longer contributing
// semantics).
if ((node._dirty || needUpdateTraversalParent) && node.attached) {
node._addToUpdate(builder, customSemanticsActionIds);
}
}
_dirtyNodes.clear();
for (final actionId in customSemanticsActionIds) {
final CustomSemanticsAction action = CustomSemanticsAction.getAction(actionId)!;
builder.updateCustomAction(
id: actionId,
label: action.label,
hint: action.hint,
overrideId: action.action?.index ?? -1,
);
}
onSemanticsUpdate(builder.build());
notifyListeners();
}