Use stable IDs for TextSpan SemanticsNodes (#52769)
This commit is contained in:
parent
542feb4736
commit
1444e77205
@ -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 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment, TextHeightBehavior;
|
||||
|
||||
@ -844,6 +845,12 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
}
|
||||
|
||||
// Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
|
||||
// can be re-used when [assembleSemanticsNode] is called again. This ensures
|
||||
// stable ids for the [SemanticsNode]s of [TextSpan]s across
|
||||
// [assembleSemanticsNode] invocations.
|
||||
Queue<SemanticsNode> _cachedChildNodes;
|
||||
|
||||
@override
|
||||
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
|
||||
assert(_semanticsInfo != null && _semanticsInfo.isNotEmpty);
|
||||
@ -854,6 +861,7 @@ class RenderParagraph extends RenderBox
|
||||
int start = 0;
|
||||
int placeholderIndex = 0;
|
||||
RenderBox child = firstChild;
|
||||
final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
|
||||
for (final InlineSpanSemanticsInformation info in _combineSemanticsInfo()) {
|
||||
final TextDirection initialDirection = currentDirection;
|
||||
final TextSelection selection = TextSelection(
|
||||
@ -914,17 +922,27 @@ class RenderParagraph extends RenderBox
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
newChildren.add(
|
||||
SemanticsNode()
|
||||
..updateWith(config: configuration)
|
||||
..rect = currentRect,
|
||||
);
|
||||
final SemanticsNode newChild = (_cachedChildNodes?.isNotEmpty == true)
|
||||
? _cachedChildNodes.removeFirst()
|
||||
: SemanticsNode();
|
||||
newChild
|
||||
..updateWith(config: configuration)
|
||||
..rect = currentRect;
|
||||
newChildCache.addLast(newChild);
|
||||
newChildren.add(newChild);
|
||||
}
|
||||
start += info.text.length;
|
||||
}
|
||||
_cachedChildNodes = newChildCache;
|
||||
node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
|
||||
}
|
||||
|
||||
@override
|
||||
void clearSemantics() {
|
||||
super.clearSemantics();
|
||||
_cachedChildNodes = null;
|
||||
}
|
||||
|
||||
@override
|
||||
List<DiagnosticsNode> debugDescribeChildren() {
|
||||
return <DiagnosticsNode>[
|
||||
|
133
packages/flutter/test/widgets/text_semantics_test.dart
Normal file
133
packages/flutter/test/widgets/text_semantics_test.dart
Normal file
@ -0,0 +1,133 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('SemanticsNode ids are stable', (WidgetTester tester) async {
|
||||
// Regression test for b/151732341.
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
await tester.pumpWidget(Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: 'Hallo ',
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: 'Welt ',
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
),
|
||||
TextSpan(
|
||||
text: '!!!',
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
expect(find.text('Hallo Welt !!!'), findsOneWidget);
|
||||
final SemanticsNode node = tester.getSemantics(find.text('Hallo Welt !!!'));
|
||||
final Map<String, int> labelToNodeId = <String, int>{};
|
||||
node.visitChildren((SemanticsNode node) {
|
||||
labelToNodeId[node.label] = node.id;
|
||||
return true;
|
||||
});
|
||||
expect(node.id, 1);
|
||||
expect(labelToNodeId['Hallo '], 2);
|
||||
expect(labelToNodeId['Welt '], 3);
|
||||
expect(labelToNodeId['!!!'], 4);
|
||||
expect(labelToNodeId.length, 3);
|
||||
|
||||
// Rebuild semantics.
|
||||
tester.renderObject(find.text('Hallo Welt !!!')).markNeedsSemanticsUpdate();
|
||||
await tester.pump();
|
||||
|
||||
final SemanticsNode nodeAfterRebuild = tester.getSemantics(find.text('Hallo Welt !!!'));
|
||||
final Map<String, int> labelToNodeIdAfterRebuild = <String, int>{};
|
||||
nodeAfterRebuild.visitChildren((SemanticsNode node) {
|
||||
labelToNodeIdAfterRebuild[node.label] = node.id;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Node IDs are stable.
|
||||
expect(nodeAfterRebuild.id, node.id);
|
||||
expect(labelToNodeIdAfterRebuild['Hallo '], labelToNodeId['Hallo ']);
|
||||
expect(labelToNodeIdAfterRebuild['Welt '], labelToNodeId['Welt ']);
|
||||
expect(labelToNodeIdAfterRebuild['!!!'], labelToNodeId['!!!']);
|
||||
expect(labelToNodeIdAfterRebuild.length, 3);
|
||||
|
||||
// Remove one node.
|
||||
await tester.pumpWidget(Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: 'Hallo ',
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: 'Welt ',
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final SemanticsNode nodeAfterRemoval = tester.getSemantics(find.text('Hallo Welt '));
|
||||
final Map<String, int> labelToNodeIdAfterRemoval = <String, int>{};
|
||||
nodeAfterRemoval.visitChildren((SemanticsNode node) {
|
||||
labelToNodeIdAfterRemoval[node.label] = node.id;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Node IDs are stable.
|
||||
expect(nodeAfterRemoval.id, node.id);
|
||||
expect(labelToNodeIdAfterRemoval['Hallo '], labelToNodeId['Hallo ']);
|
||||
expect(labelToNodeIdAfterRemoval['Welt '], labelToNodeId['Welt ']);
|
||||
expect(labelToNodeIdAfterRemoval.length, 2);
|
||||
|
||||
await tester.pumpWidget(Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: 'Hallo ',
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: 'Welt ',
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
),
|
||||
TextSpan(
|
||||
text: '!!!',
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
expect(find.text('Hallo Welt !!!'), findsOneWidget);
|
||||
final SemanticsNode nodeAfterAddition = tester.getSemantics(find.text('Hallo Welt !!!'));
|
||||
final Map<String, int> labelToNodeIdAfterAddition = <String, int>{};
|
||||
nodeAfterAddition.visitChildren((SemanticsNode node) {
|
||||
labelToNodeIdAfterAddition[node.label] = node.id;
|
||||
return true;
|
||||
});
|
||||
|
||||
// New node gets a new ID.
|
||||
expect(nodeAfterAddition.id, node.id);
|
||||
expect(labelToNodeIdAfterAddition['Hallo '], labelToNodeId['Hallo ']);
|
||||
expect(labelToNodeIdAfterAddition['Welt '], labelToNodeId['Welt ']);
|
||||
expect(labelToNodeIdAfterAddition['!!!'], isNot(labelToNodeId['!!!']));
|
||||
expect(labelToNodeIdAfterAddition['!!!'], isNotNull);
|
||||
expect(labelToNodeIdAfterAddition.length, 3);
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user