Exclude SemanticNodes that are fully covered by an overlapping sliver (#11826)

* Exclude SemanticNodes that are fully covered by an overlapping sliver

* simplification

* formatting

* nits

* add test for center widget
This commit is contained in:
Michael Goderbauer 2017-08-29 14:57:35 -07:00 committed by GitHub
parent 98c8366bba
commit bfa788512e
3 changed files with 423 additions and 1 deletions

View file

@ -285,6 +285,36 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
visitor(child);
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
switch (constraints.normalizedGrowthDirection) {
case GrowthDirection.forward:
super.visitChildrenForSemantics((RenderObject child) {
// The sliver is overlapped at the leading edge.
final Offset bottomLeftInViewport = MatrixUtils.transformPoint(
child.getTransformTo(parent), child.semanticBounds.bottomLeft
);
final double endOverlap = constraints.overlap;
if ((constraints.axis == Axis.vertical && bottomLeftInViewport.dy > endOverlap) ||
(constraints.axis == Axis.horizontal && bottomLeftInViewport.dx > endOverlap))
visitor(child);
});
break;
case GrowthDirection.reverse:
super.visitChildrenForSemantics((RenderObject child) {
// The sliver is overlapped at the trailing edge.
final Offset topRightInViewport = MatrixUtils.transformPoint(
child.getTransformTo(parent), child.semanticBounds.topRight
);
final double startOverlap = constraints.remainingPaintExtent - constraints.overlap;
if ((constraints.axis == Axis.vertical && topRightInViewport.dy < startOverlap) ||
(constraints.axis == Axis.horizontal && topRightInViewport.dx < startOverlap))
visitor(child);
});
break;
}
}
/// Called during layout to create and add the child with the given index and
/// scroll offset.
///

View file

@ -97,7 +97,8 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
for (RenderSliver sliver in childrenInPaintOrder) {
if (sliver.geometry.paintExtent != 0)
if (sliver.geometry.paintExtent != 0 &&
sliver.constraints.overlap < sliver.geometry.paintOrigin + sliver.geometry.paintExtent)
visitor(sliver);
}
}

View file

@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart';
import 'semantics_tester.dart';
@ -281,4 +282,394 @@ void main() {
semantics.dispose();
});
testWidgets('SemanticsNodes of a sliver fully covered by another overlapping sliver are excluded', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final List<Widget> listChildren = new List<Widget>.generate(10, (int i) {
return new Container(
height: 200.0,
child: new Text('Item $i'),
);
});
final ScrollController controller = new ScrollController(initialScrollOffset: 280.0);
await tester.pumpWidget(
new MediaQuery(
data: new MediaQueryData(),
child: new CustomScrollView(
slivers: <Widget>[
new SliverAppBar(
pinned: true,
expandedHeight: 100.0,
),
new SliverList(
delegate: new SliverChildListDelegate(listChildren),
),
],
controller: controller,
),
),
);
// 'Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Item 0')));
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 20,
rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[
new TestSemantics(
id: 25,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
// Item 0 is missing because its covered by the app bar.
new TestSemantics(
id: 21,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
// Item 1 starts 20.0dp below edge, so there would be room for Item 0.
transform: new Matrix4.translation(new Vector3(0.0, 20.0, 0.0)),
label: 'Item 1',
),
new TestSemantics(
id: 22,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 220.0, 0.0)),
label: 'Item 2',
),
new TestSemantics(
id: 23,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 420.0, 0.0)),
label: 'Item 3',
),
],
),
new TestSemantics(
id: 24,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
),
],
)
],
),
));
semantics.dispose();
});
testWidgets('Slivers fully covered by another overlapping sliver are excluded', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final ScrollController controller = new ScrollController(initialScrollOffset: 280.0);
final List<Widget> slivers = new List<Widget>.generate(10, (int i) {
return new SliverToBoxAdapter(
child: new Container(
height: 200.0,
child: new Text('Item $i'),
),
);
});
await tester.pumpWidget(
new MediaQuery(
data: new MediaQueryData(),
child: new CustomScrollView(
controller: controller,
slivers: <Widget>[
new SliverAppBar(
pinned: true,
expandedHeight: 100.0,
),
]..addAll(slivers),
),
),
);
// 'Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Item 0')));
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 26,
rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[
new TestSemantics(
id: 31,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 27,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 420.0, 0.0)),
label: 'Item 3',
),
new TestSemantics(
id: 28,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 220.0, 0.0)),
label: 'Item 2',
),
new TestSemantics(
id: 29,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
// Item 1 starts 20.0dp below edge, so there would be room for Item 0.
transform: new Matrix4.translation(new Vector3(0.0, 20.0, 0.0)),
label: 'Item 1',
),
// Item 0 is missing because its covered by the app bar.
],
),
new TestSemantics(
id: 30,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
),
],
)
],
),
));
semantics.dispose();
});
testWidgets('SemanticsNodes of a sliver fully covered by another overlapping sliver are excluded (reverse)', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final List<Widget> listChildren = new List<Widget>.generate(10, (int i) {
return new Container(
height: 200.0,
child: new Text('Item $i'),
);
});
final ScrollController controller = new ScrollController(initialScrollOffset: 280.0);
await tester.pumpWidget(
new MediaQuery(
data: new MediaQueryData(),
child: new CustomScrollView(
reverse: true, // This is the important setting for this test.
slivers: <Widget>[
new SliverAppBar(
pinned: true,
expandedHeight: 100.0,
),
new SliverList(
delegate: new SliverChildListDelegate(listChildren),
),
],
controller: controller,
),
),
);
// 'Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Item 0')));
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 32,
rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[
new TestSemantics(
id: 37,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
// Item 0 is missing because its covered by the app bar.
new TestSemantics(
id: 33,
// Item 1 ends at 580dp, so there would be 20dp space for Item 0.
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 380.0, 0.0)),
label: 'Item 1',
),
new TestSemantics(
id: 34,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 180.0, 0.0)),
label: 'Item 2',
),
new TestSemantics(
id: 35,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, -20.0, 0.0)),
label: 'Item 3',
),
],
),
new TestSemantics(
id: 36,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0),
transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
),
],
)
],
),
));
semantics.dispose();
});
testWidgets('Slivers fully covered by another overlapping sliver are excluded (reverse)', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final ScrollController controller = new ScrollController(initialScrollOffset: 280.0);
final List<Widget> slivers = new List<Widget>.generate(10, (int i) {
return new SliverToBoxAdapter(
child: new Container(
height: 200.0,
child: new Text('Item $i'),
),
);
});
await tester.pumpWidget(
new MediaQuery(
data: new MediaQueryData(),
child: new CustomScrollView(
reverse: true, // This is the important setting for this test.
controller: controller,
slivers: <Widget>[
new SliverAppBar(
pinned: true,
expandedHeight: 100.0,
),
]..addAll(slivers),
),
),
);
// 'Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Item 0')));
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 38,
rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[
new TestSemantics(
id: 43,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 39,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, -20.0, 0.0)),
label: 'Item 3',
),
new TestSemantics(
id: 40,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 180.0, 0.0)),
label: 'Item 2',
),
new TestSemantics(
id: 41,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
// Item 1 ends at 580dp, so there would be 20dp space for Item 0.
transform: new Matrix4.translation(new Vector3(0.0, 380.0, 0.0)),
label: 'Item 1',
),
// Item 0 is missing because its covered by the app bar.
],
),
new TestSemantics(
id: 42,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0),
transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
),
],
)
],
),
));
semantics.dispose();
});
testWidgets('Slivers fully covered by another overlapping sliver are excluded (with center sliver)', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final ScrollController controller = new ScrollController(initialScrollOffset: 280.0);
final GlobalKey forwardAppBarKey = new GlobalKey(debugLabel: 'forward app bar');
final List<Widget> forwardChildren = new List<Widget>.generate(10, (int i) {
return new Container(
height: 200.0,
child: new Text('Forward Item $i'),
);
});
final List<Widget> backwardChildren = new List<Widget>.generate(10, (int i) {
return new Container(
height: 200.0,
child: new Text('Backward Item $i'),
);
});
await tester.pumpWidget(
new MediaQuery(
data: new MediaQueryData(),
child: new Scrollable(
controller: controller,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new Viewport(
offset: offset,
center: forwardAppBarKey,
slivers: <Widget>[
new SliverList(
delegate: new SliverChildListDelegate(backwardChildren),
),
new SliverAppBar(
pinned: true,
expandedHeight: 100.0,
flexibleSpace: const FlexibleSpaceBar(
title: const Text('Backward app bar'),
),
),
new SliverAppBar(
pinned: true,
key: forwardAppBarKey,
expandedHeight: 100.0,
flexibleSpace: const FlexibleSpaceBar(
title: const Text('Forward app bar'),
),
),
new SliverList(
delegate: new SliverChildListDelegate(forwardChildren),
),
],
);
},
),
),
);
// 'Forward Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Forward Item 0')));
expect(semantics, includesNodeWith(label: 'Forward Item 1'));
controller.jumpTo(-880.0);
await tester.pumpAndSettle();
expect(semantics, isNot(includesNodeWith(label: 'Backward Item 0')));
expect(semantics, includesNodeWith(label: 'Backward Item 1'));
semantics.dispose();
});
}