Commit 49499457 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Drop invisible SemanticsNodes from tree (#12358)

* Drop invisible SemanticsNodes from tree

A node is invisible if it is outside of the bounds of the screen and if it is not merged into its (partially) visible parent.

Also in this PR: only set `wasAffectedByClip` to true if the nodes has actually been clipped.

* Fix other failing tests

* renaming

* review feedback

* more doc
parent 6128f48c
...@@ -603,12 +603,14 @@ class _SemanticsGeometry { ...@@ -603,12 +603,14 @@ class _SemanticsGeometry {
assert(parentSemantics != null); assert(parentSemantics != null);
assert(parentSemantics.wasAffectedByClip != null); assert(parentSemantics.wasAffectedByClip != null);
semantics.transform = _transform; semantics.transform = _transform;
final Rect semanticBounds = rendering.semanticBounds;
if (_clipRect != null) { if (_clipRect != null) {
semantics.rect = _clipRect.intersect(rendering.semanticBounds); final Rect rect = _clipRect.intersect(semanticBounds);
semantics.wasAffectedByClip = true; semantics.rect = rect;
semantics.wasAffectedByClip = rect != semanticBounds;
} else { } else {
semantics.rect = rendering.semanticBounds; semantics.rect = semanticBounds;
semantics.wasAffectedByClip = parentSemantics?.wasAffectedByClip ?? false; semantics.wasAffectedByClip = false;
} }
} }
} }
...@@ -697,6 +699,8 @@ class _CleanSemanticsFragment extends _SemanticsFragment { ...@@ -697,6 +699,8 @@ class _CleanSemanticsFragment extends _SemanticsFragment {
if (geometry != null) { if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain); geometry.applyAncestorChain(_ancestorChain);
geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics); geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics);
if (node.isInvisible)
return; // drop the node
} else { } else {
assert(_ancestorChain.length == 1); assert(_ancestorChain.length == 1);
} }
...@@ -722,6 +726,8 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment { ...@@ -722,6 +726,8 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
assert(!_debugCompiled); assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; }()); assert(() { _debugCompiled = true; return true; }());
final SemanticsNode node = establishSemanticsNode(geometry, currentSemantics, parentSemantics); final SemanticsNode node = establishSemanticsNode(geometry, currentSemantics, parentSemantics);
if (node.isInvisible)
return; // drop the node
final List<SemanticsNode> children = <SemanticsNode>[]; final List<SemanticsNode> children = <SemanticsNode>[];
for (_SemanticsFragment child in _children) { for (_SemanticsFragment child in _children) {
assert(child._ancestorChain.last == renderObjectOwner); assert(child._ancestorChain.last == renderObjectOwner);
...@@ -2714,6 +2720,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2714,6 +2720,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(fragment is _InterestingSemanticsFragment); assert(fragment is _InterestingSemanticsFragment);
final SemanticsNode node = fragment.compile(parentSemantics: _semantics?.parent).single; final SemanticsNode node = fragment.compile(parentSemantics: _semantics?.parent).single;
assert(node != null); assert(node != null);
assert(!node.isInvisible);
assert(node == _semantics); assert(node == _semantics);
} catch (e, stack) { } catch (e, stack) {
_debugReportException('_updateSemantics', e, stack); _debugReportException('_updateSemantics', e, stack);
......
...@@ -285,9 +285,24 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -285,9 +285,24 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
} }
} }
/// Whether [rect] might have been influenced by clips applied by ancestors. /// Whether [rect] was clipped by ancestors.
///
/// This is only true if the [rect] of this [SemanticsNode] has been altered
/// due to clipping by an ancestor. If ancestors have been clipped, but the
/// [rect] of this node was unaffected it will be false.
bool wasAffectedByClip = false; bool wasAffectedByClip = false;
/// Whether the node is invisible.
///
/// A node whose [rect] is outside of the bounds of the screen and hence not
/// reachable for users is considered invisible if its semantic information
/// is not merged into a (partially) visible parent as indicated by
/// [isMergedIntoParent].
///
/// An invisible node can be safely dropped from the semantic tree without
/// loosing semantic information that is relevant for describing the content
/// currently shown on screen.
bool get isInvisible => !isMergedIntoParent && rect.isEmpty;
// FLAGS AND LABELS // FLAGS AND LABELS
// These are supposed to be set by SemanticsAnnotator obtained from getSemanticsAnnotators // These are supposed to be set by SemanticsAnnotator obtained from getSemanticsAnnotators
...@@ -519,8 +534,10 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -519,8 +534,10 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
child._dead = true; child._dead = true;
} }
if (_newChildren != null) { if (_newChildren != null) {
for (SemanticsNode child in _newChildren) for (SemanticsNode child in _newChildren) {
assert(!child.isInvisible, 'Child with id ${child.id} is invisible and should not be added to tree.');
child._dead = false; child._dead = false;
}
} }
bool sawChange = false; bool sawChange = false;
if (_children != null) { if (_children != null) {
......
...@@ -501,11 +501,11 @@ void main() { ...@@ -501,11 +501,11 @@ void main() {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(new MaterialApp(home: new Scaffold( await tester.pumpWidget(new MaterialApp(home: new Scaffold(
body: new Semantics(label: bodyLabel, child: new Container()), body: const Text(bodyLabel),
persistentFooterButtons: <Widget>[new Semantics(label: persistentFooterButtonLabel, child: new Container())], persistentFooterButtons: <Widget>[const Text(persistentFooterButtonLabel)],
bottomNavigationBar: new Semantics(label: bottomNavigationBarLabel, child: new Container()), bottomNavigationBar: const Text(bottomNavigationBarLabel),
floatingActionButton: new Semantics(label: floatingActionButtonLabel, child: new Container()), floatingActionButton: const Text(floatingActionButtonLabel),
drawer: new Drawer(child:new Semantics(label: drawerLabel, child: new Container())), drawer: const Drawer(child:const Text(drawerLabel)),
))); )));
expect(semantics, includesNodeWith(label: bodyLabel)); expect(semantics, includesNodeWith(label: bodyLabel));
......
...@@ -32,6 +32,7 @@ void main() { ...@@ -32,6 +32,7 @@ void main() {
test('getSemanticsData includes tags', () { test('getSemanticsData includes tags', () {
final SemanticsNode node = new SemanticsNode() final SemanticsNode node = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0)
..addTag(tag1) ..addTag(tag1)
..addTag(tag2); ..addTag(tag2);
...@@ -43,7 +44,9 @@ void main() { ...@@ -43,7 +44,9 @@ void main() {
node.mergeAllDescendantsIntoThisNode = true; node.mergeAllDescendantsIntoThisNode = true;
node.addChildren(<SemanticsNode>[ node.addChildren(<SemanticsNode>[
new SemanticsNode()..addTag(tag3) new SemanticsNode()
..rect = new Rect.fromLTRB(5.0, 5.0, 10.0, 10.0)
..addTag(tag3),
]); ]);
node.finalizeChildren(); node.finalizeChildren();
...@@ -95,9 +98,12 @@ void main() { ...@@ -95,9 +98,12 @@ void main() {
}); });
test('toStringDeep() does not throw with transform == null', () { test('toStringDeep() does not throw with transform == null', () {
final SemanticsNode child1 = new SemanticsNode(); final SemanticsNode child1 = new SemanticsNode()
final SemanticsNode child2 = new SemanticsNode(); ..rect = new Rect.fromLTRB(0.0, 0.0, 5.0, 5.0);
final SemanticsNode root = new SemanticsNode(); final SemanticsNode child2 = new SemanticsNode()
..rect = new Rect.fromLTRB(5.0, 0.0, 10.0, 5.0);
final SemanticsNode root = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
root.addChildren(<SemanticsNode>[child1, child2]); root.addChildren(<SemanticsNode>[child1, child2]);
root.finalizeChildren(); root.finalizeChildren();
...@@ -107,64 +113,68 @@ void main() { ...@@ -107,64 +113,68 @@ void main() {
expect( expect(
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal), root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
'SemanticsNode#8(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n' 'SemanticsNode#8(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n'
'├SemanticsNode#6(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n' '├SemanticsNode#6(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n'
'└SemanticsNode#7(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n', '└SemanticsNode#7(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n',
); );
}); });
test('toStringDeep respects childOrder parameter', () { test('toStringDeep respects childOrder parameter', () {
final SemanticsNode child1 = new SemanticsNode() final SemanticsNode child1 = new SemanticsNode()
..rect = new Rect.fromLTRB(20.0, 20.0, 20.0, 20.0); ..rect = new Rect.fromLTRB(15.0, 0.0, 20.0, 5.0);
final SemanticsNode child2 = new SemanticsNode() final SemanticsNode child2 = new SemanticsNode()
..rect = new Rect.fromLTRB(10.0, 10.0, 10.0, 10.0); ..rect = new Rect.fromLTRB(10.0, 0.0, 15.0, 5.0);
final SemanticsNode root = new SemanticsNode(); final SemanticsNode root = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 20.0, 5.0);
root.addChildren(<SemanticsNode>[child1, child2]); root.addChildren(<SemanticsNode>[child1, child2]);
root.finalizeChildren(); root.finalizeChildren();
expect( expect(
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal), root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
'SemanticsNode#11(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n' 'SemanticsNode#11(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 20.0, 5.0))\n'
'├SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 10.0, 10.0, 10.0))\n' '├SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n'
'└SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(20.0, 20.0, 20.0, 20.0))\n', '└SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n',
); );
expect( expect(
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest), root.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
'SemanticsNode#11(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n' 'SemanticsNode#11(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 20.0, 5.0))\n'
'├SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(20.0, 20.0, 20.0, 20.0))\n' '├SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n'
'└SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 10.0, 10.0, 10.0))\n', '└SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n',
); );
final SemanticsNode child3 = new SemanticsNode() final SemanticsNode child3 = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 0.0, 0.0); ..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
child3.addChildren(<SemanticsNode>[ child3.addChildren(<SemanticsNode>[
new SemanticsNode()..rect = new Rect.fromLTRB(20.0, 0.0, 20.0, 0.0), new SemanticsNode()
new SemanticsNode(), ..rect = new Rect.fromLTRB(5.0, 0.0, 10.0, 5.0),
new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 5.0, 5.0),
]); ]);
child3.finalizeChildren(); child3.finalizeChildren();
final SemanticsNode rootComplex = new SemanticsNode(); final SemanticsNode rootComplex = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 25.0, 5.0);
rootComplex.addChildren(<SemanticsNode>[child1, child2, child3]); rootComplex.addChildren(<SemanticsNode>[child1, child2, child3]);
rootComplex.finalizeChildren(); rootComplex.finalizeChildren();
expect( expect(
rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal), rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
'SemanticsNode#15(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n' 'SemanticsNode#15(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 25.0, 5.0))\n'
'├SemanticsNode#12(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n' '├SemanticsNode#12(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n'
'│├SemanticsNode#14(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n' '│├SemanticsNode#14(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n'
'│└SemanticsNode#13(STALE, owner: null, Rect.fromLTRB(20.0, 0.0, 20.0, 0.0))\n' '│└SemanticsNode#13(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n'
'├SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 10.0, 10.0, 10.0))\n' '├SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n'
'└SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(20.0, 20.0, 20.0, 20.0))\n', '└SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n',
); );
expect( expect(
rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest), rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
'SemanticsNode#15(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n' 'SemanticsNode#15(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 25.0, 5.0))\n'
'├SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(20.0, 20.0, 20.0, 20.0))\n' '├SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n'
'├SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 10.0, 10.0, 10.0))\n' '├SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n'
'└SemanticsNode#12(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n' '└SemanticsNode#12(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n'
' ├SemanticsNode#13(STALE, owner: null, Rect.fromLTRB(20.0, 0.0, 20.0, 0.0))\n' ' ├SemanticsNode#13(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n'
' └SemanticsNode#14(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n', ' └SemanticsNode#14(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n',
); );
}); });
......
// Copyright 2017 The Chromium 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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('SemanticNode.rect is clipped', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: new Container(
width: 100.0,
child: new Flex(
direction: Axis.horizontal,
children: <Widget>[
new Container(
width: 75.0,
child: const Text('1'),
),
new Container(
width: 75.0,
child: const Text('2'),
),
new Container(
width: 75.0,
child: const Text('3'),
),
],
),
),
),
));
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics(
id: 1,
label: '1',
rect: new Rect.fromLTRB(0.0, 0.0, 75.0, 14.0),
),
new TestSemantics(
id: 2,
label: '2',
rect: new Rect.fromLTRB(0.0, 0.0, 25.0, 14.0), // clipped form original 75.0 to 25.0
),
// node with Text 3 not present.
],
),
ignoreTransform: true,
));
final SemanticsNode node1 = tester.renderObject(find.byWidget(const Text('1'))).debugSemantics;
final SemanticsNode node2 = tester.renderObject(find.byWidget(const Text('2'))).debugSemantics;
final SemanticsNode node3 = tester.renderObject(find.byWidget(const Text('3'))).debugSemantics;
expect(node1.wasAffectedByClip, false);
expect(node2.wasAffectedByClip, true);
expect(node3.wasAffectedByClip, true);
expect(node1.isInvisible, isFalse);
expect(node2.isInvisible, isFalse);
expect(node3.isInvisible, isTrue);
semantics.dispose();
});
testWidgets('SemanticsNode is not removed if out of bounds and merged into something within bounds', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: new Container(
width: 100.0,
child: new Flex(
direction: Axis.horizontal,
children: <Widget>[
new Container(
width: 75.0,
child: const Text('1'),
),
new MergeSemantics(
child: new Flex(
direction: Axis.horizontal,
children: <Widget>[
new Container(
width: 75.0,
child: const Text('2'),
),
new Container(
width: 75.0,
child: const Text('3'),
),
]
)
),
],
),
),
),
));
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics(
id: 4,
label: '1',
rect: new Rect.fromLTRB(0.0, 0.0, 75.0, 14.0),
),
new TestSemantics(
id: 5,
label: '2\n3',
rect: new Rect.fromLTRB(0.0, 0.0, 25.0, 14.0), // clipped form original 75.0 to 25.0
),
],
),
ignoreTransform: true,
));
semantics.dispose();
});
}
...@@ -19,16 +19,8 @@ void main() { ...@@ -19,16 +19,8 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
new Semantics( const Text('test1'),
label: 'test1', const Text('test2'),
textDirection: TextDirection.ltr,
child: new Container()
),
new Semantics(
label: 'test2',
textDirection: TextDirection.ltr,
child: new Container()
)
], ],
), ),
), ),
...@@ -52,16 +44,8 @@ void main() { ...@@ -52,16 +44,8 @@ void main() {
child: new MergeSemantics( child: new MergeSemantics(
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
new Semantics( const Text('test1'),
label: 'test1', const Text('test2'),
textDirection: TextDirection.ltr,
child: new Container()
),
new Semantics(
label: 'test2',
textDirection: TextDirection.ltr,
child: new Container()
)
], ],
), ),
), ),
...@@ -80,16 +64,8 @@ void main() { ...@@ -80,16 +64,8 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
new Semantics( const Text('test1'),
label: 'test1', const Text('test2'),
textDirection: TextDirection.ltr,
child: new Container()
),
new Semantics(
label: 'test2',
textDirection: TextDirection.ltr,
child: new Container()
)
], ],
), ),
), ),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment