fix semantics to only send relevant node update (#60925)

This commit is contained in:
chunhtai 2020-07-08 18:21:04 -07:00 committed by GitHub
parent b345ecc11b
commit 06b301cdfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 233 additions and 3 deletions

View file

@ -4,7 +4,7 @@
// @dart = 2.8
import 'dart:ui' as ui show AccessibilityFeatures;
import 'dart:ui' as ui show AccessibilityFeatures, SemanticsUpdateBuilder;
import 'package:flutter/foundation.dart';
@ -34,6 +34,16 @@ mixin SemanticsBinding on BindingBase {
_accessibilityFeatures = window.accessibilityFeatures;
}
/// Creates an empty semantics update builder.
///
/// The caller is responsible for filling out the semantics node updates.
///
/// This method is used by the [SemanticsOwner] to create builder for all its
/// semantics updates.
ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
return ui.SemanticsUpdateBuilder();
}
/// The currently active set of [AccessibilityFeatures].
///
/// This is initialized the first time [runApp] is called and updated whenever

View file

@ -2624,13 +2624,15 @@ class SemanticsOwner extends ChangeNotifier {
if (node.isPartOfNodeMerging) {
assert(node.mergeAllDescendantsIntoThisNode || node.parent != null);
// if we're merged into our parent, make sure our parent is added to the dirty list
if (node.parent != null && node.parent.isPartOfNodeMerging)
if (node.parent != null && node.parent.isPartOfNodeMerging) {
node.parent._markDirty(); // this can add the node to the dirty list
node._dirty = false; // We don't want to send update for this node.
}
}
}
}
visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
final ui.SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder();
for (final SemanticsNode node in visitedNodes) {
assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
// The _serialize() method marks the node as not dirty, and

View file

@ -0,0 +1,218 @@
// 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.
// @dart = 2.8
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
SemanticsUpdateTestBinding();
testWidgets('Semantics update does not send update for merged nodes.', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
// Pumps a placeholder to trigger the warm up frame.
await tester.pumpWidget(
const Placeholder(),
// Stops right after the warm up frame.
null,
EnginePhase.build
);
// The warm up frame will send update for an empty semantics tree. We
// ignore this one time update.
SemanticsUpdateBuilderSpy.observations.clear();
// Builds the real widget tree.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MergeSemantics(
child: Semantics(
label: 'outer',
// This semantics node should not be part of the semantics update
// because it is under another semantics container.
child: Semantics(
label: 'inner',
container: true,
child: const Text('text'),
),
),
),
),
);
expect(SemanticsUpdateBuilderSpy.observations.length, 2);
expect(SemanticsUpdateBuilderSpy.observations.containsKey(0), isTrue);
expect(SemanticsUpdateBuilderSpy.observations[0].childrenInTraversalOrder.length, 1);
expect(SemanticsUpdateBuilderSpy.observations[0].childrenInTraversalOrder[0], 1);
expect(SemanticsUpdateBuilderSpy.observations.containsKey(1), isTrue);
expect(SemanticsUpdateBuilderSpy.observations[1].childrenInTraversalOrder.length, 0);
expect(SemanticsUpdateBuilderSpy.observations[1].label, 'outer\ninner\ntext');
SemanticsUpdateBuilderSpy.observations.clear();
// Updates the inner semantics label and verifies it only sends update for
// the merged parent.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MergeSemantics(
child: Semantics(
label: 'outer',
// This semantics node should not be part of the semantics update
// because it is under another semantics container.
child: Semantics(
label: 'inner-updated',
container: true,
child: const Text('text'),
),
),
),
),
);
expect(SemanticsUpdateBuilderSpy.observations.length, 1);
expect(SemanticsUpdateBuilderSpy.observations.containsKey(1), isTrue);
expect(SemanticsUpdateBuilderSpy.observations[1].childrenInTraversalOrder.length, 0);
expect(SemanticsUpdateBuilderSpy.observations[1].label, 'outer\ninner-updated\ntext');
SemanticsUpdateBuilderSpy.observations.clear();
handle.dispose();
});
}
class SemanticsUpdateTestBinding extends AutomatedTestWidgetsFlutterBinding {
@override
ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
return SemanticsUpdateBuilderSpy();
}
}
class SemanticsUpdateBuilderSpy extends ui.SemanticsUpdateBuilder {
static Map<int, SemanticsNodeUpdateObservation> observations = <int, SemanticsNodeUpdateObservation>{};
@override
void updateNode({
@required int id,
@required int flags,
@required int actions,
@required int maxValueLength,
@required int currentValueLength,
@required int textSelectionBase,
@required int textSelectionExtent,
@required int platformViewId,
@required int scrollChildren,
@required int scrollIndex,
@required double scrollPosition,
@required double scrollExtentMax,
@required double scrollExtentMin,
@required double elevation,
@required double thickness,
@required Rect rect,
@required String label,
@required String hint,
@required String value,
@required String increasedValue,
@required String decreasedValue,
TextDirection textDirection,
@required Float64List transform,
@required Int32List childrenInTraversalOrder,
@required Int32List childrenInHitTestOrder,
@required Int32List additionalActions,
}) {
// Makes sure we don't send the same id twice.
assert(!observations.containsKey(id));
observations[id] = SemanticsNodeUpdateObservation(
id: id,
flags: flags,
actions: actions,
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
textSelectionBase: textSelectionBase,
textSelectionExtent: textSelectionExtent,
platformViewId: platformViewId,
scrollChildren: scrollChildren,
scrollIndex: scrollIndex,
scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin,
elevation: elevation,
thickness: thickness,
rect: rect,
label: label,
hint: hint,
value: value,
increasedValue: increasedValue,
decreasedValue: decreasedValue,
textDirection: textDirection,
transform: transform,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,
);
}
}
class SemanticsNodeUpdateObservation {
const SemanticsNodeUpdateObservation({
@required this.id,
@required this.flags,
@required this.actions,
@required this.maxValueLength,
@required this.currentValueLength,
@required this.textSelectionBase,
@required this.textSelectionExtent,
@required this.platformViewId,
@required this.scrollChildren,
@required this.scrollIndex,
@required this.scrollPosition,
@required this.scrollExtentMax,
@required this.scrollExtentMin,
@required this.elevation,
@required this.thickness,
@required this.rect,
@required this.label,
@required this.hint,
@required this.value,
@required this.increasedValue,
@required this.decreasedValue,
this.textDirection,
@required this.transform,
@required this.childrenInTraversalOrder,
@required this.childrenInHitTestOrder,
@required this.additionalActions,
});
final int id;
final int flags;
final int actions;
final int maxValueLength;
final int currentValueLength;
final int textSelectionBase;
final int textSelectionExtent;
final int platformViewId;
final int scrollChildren;
final int scrollIndex;
final double scrollPosition;
final double scrollExtentMax;
final double scrollExtentMin;
final double elevation;
final double thickness;
final Rect rect;
final String label;
final String hint;
final String value;
final String increasedValue;
final String decreasedValue;
final TextDirection textDirection;
final Float64List transform;
final Int32List childrenInTraversalOrder;
final Int32List childrenInHitTestOrder;
final Int32List additionalActions;
}