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
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:collection';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment, TextHeightBehavior;
|
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
|
@override
|
||||||
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
|
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
|
||||||
assert(_semanticsInfo != null && _semanticsInfo.isNotEmpty);
|
assert(_semanticsInfo != null && _semanticsInfo.isNotEmpty);
|
||||||
@ -854,6 +861,7 @@ class RenderParagraph extends RenderBox
|
|||||||
int start = 0;
|
int start = 0;
|
||||||
int placeholderIndex = 0;
|
int placeholderIndex = 0;
|
||||||
RenderBox child = firstChild;
|
RenderBox child = firstChild;
|
||||||
|
final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
|
||||||
for (final InlineSpanSemanticsInformation info in _combineSemanticsInfo()) {
|
for (final InlineSpanSemanticsInformation info in _combineSemanticsInfo()) {
|
||||||
final TextDirection initialDirection = currentDirection;
|
final TextDirection initialDirection = currentDirection;
|
||||||
final TextSelection selection = TextSelection(
|
final TextSelection selection = TextSelection(
|
||||||
@ -914,17 +922,27 @@ class RenderParagraph extends RenderBox
|
|||||||
assert(false);
|
assert(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newChildren.add(
|
final SemanticsNode newChild = (_cachedChildNodes?.isNotEmpty == true)
|
||||||
SemanticsNode()
|
? _cachedChildNodes.removeFirst()
|
||||||
|
: SemanticsNode();
|
||||||
|
newChild
|
||||||
..updateWith(config: configuration)
|
..updateWith(config: configuration)
|
||||||
..rect = currentRect,
|
..rect = currentRect;
|
||||||
);
|
newChildCache.addLast(newChild);
|
||||||
|
newChildren.add(newChild);
|
||||||
}
|
}
|
||||||
start += info.text.length;
|
start += info.text.length;
|
||||||
}
|
}
|
||||||
|
_cachedChildNodes = newChildCache;
|
||||||
node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
|
node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void clearSemantics() {
|
||||||
|
super.clearSemantics();
|
||||||
|
_cachedChildNodes = null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<DiagnosticsNode> debugDescribeChildren() {
|
List<DiagnosticsNode> debugDescribeChildren() {
|
||||||
return <DiagnosticsNode>[
|
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