SingleChildScrollView does not clip semantics child (#114194)
This commit is contained in:
parent
0dea659f98
commit
fa174b2343
@ -326,16 +326,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
|
|||||||
_RenderSingleChildViewport({
|
_RenderSingleChildViewport({
|
||||||
AxisDirection axisDirection = AxisDirection.down,
|
AxisDirection axisDirection = AxisDirection.down,
|
||||||
required ViewportOffset offset,
|
required ViewportOffset offset,
|
||||||
double cacheExtent = RenderAbstractViewport.defaultCacheExtent,
|
|
||||||
RenderBox? child,
|
RenderBox? child,
|
||||||
required Clip clipBehavior,
|
required Clip clipBehavior,
|
||||||
}) : assert(axisDirection != null),
|
}) : assert(axisDirection != null),
|
||||||
assert(offset != null),
|
assert(offset != null),
|
||||||
assert(cacheExtent != null),
|
|
||||||
assert(clipBehavior != null),
|
assert(clipBehavior != null),
|
||||||
_axisDirection = axisDirection,
|
_axisDirection = axisDirection,
|
||||||
_offset = offset,
|
_offset = offset,
|
||||||
_cacheExtent = cacheExtent,
|
|
||||||
_clipBehavior = clipBehavior {
|
_clipBehavior = clipBehavior {
|
||||||
this.child = child;
|
this.child = child;
|
||||||
}
|
}
|
||||||
@ -370,18 +367,6 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
|
|||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
|
|
||||||
double get cacheExtent => _cacheExtent;
|
|
||||||
double _cacheExtent;
|
|
||||||
set cacheExtent(double value) {
|
|
||||||
assert(value != null);
|
|
||||||
if (value == _cacheExtent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_cacheExtent = value;
|
|
||||||
markNeedsLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@macro flutter.material.Material.clipBehavior}
|
/// {@macro flutter.material.Material.clipBehavior}
|
||||||
///
|
///
|
||||||
/// Defaults to [Clip.none], and must not be null.
|
/// Defaults to [Clip.none], and must not be null.
|
||||||
@ -700,19 +685,34 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
|
|||||||
@override
|
@override
|
||||||
Rect describeSemanticsClip(RenderObject child) {
|
Rect describeSemanticsClip(RenderObject child) {
|
||||||
assert(axis != null);
|
assert(axis != null);
|
||||||
switch (axis) {
|
final double remainingOffset = _maxScrollExtent - offset.pixels;
|
||||||
case Axis.vertical:
|
switch (axisDirection) {
|
||||||
|
case AxisDirection.up:
|
||||||
return Rect.fromLTRB(
|
return Rect.fromLTRB(
|
||||||
semanticBounds.left,
|
semanticBounds.left,
|
||||||
semanticBounds.top - cacheExtent,
|
semanticBounds.top - remainingOffset,
|
||||||
semanticBounds.right,
|
semanticBounds.right,
|
||||||
semanticBounds.bottom + cacheExtent,
|
semanticBounds.bottom + offset.pixels,
|
||||||
);
|
);
|
||||||
case Axis.horizontal:
|
case AxisDirection.right:
|
||||||
return Rect.fromLTRB(
|
return Rect.fromLTRB(
|
||||||
semanticBounds.left - cacheExtent,
|
semanticBounds.left - offset.pixels,
|
||||||
semanticBounds.top,
|
semanticBounds.top,
|
||||||
semanticBounds.right + cacheExtent,
|
semanticBounds.right + remainingOffset,
|
||||||
|
semanticBounds.bottom,
|
||||||
|
);
|
||||||
|
case AxisDirection.down:
|
||||||
|
return Rect.fromLTRB(
|
||||||
|
semanticBounds.left,
|
||||||
|
semanticBounds.top - offset.pixels,
|
||||||
|
semanticBounds.right,
|
||||||
|
semanticBounds.bottom + remainingOffset,
|
||||||
|
);
|
||||||
|
case AxisDirection.left:
|
||||||
|
return Rect.fromLTRB(
|
||||||
|
semanticBounds.left - remainingOffset,
|
||||||
|
semanticBounds.top,
|
||||||
|
semanticBounds.right + offset.pixels,
|
||||||
semanticBounds.bottom,
|
semanticBounds.bottom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2995,14 +2995,15 @@ void main() {
|
|||||||
const String tab0title = 'This is a very wide tab #0\nTab 1 of 20';
|
const String tab0title = 'This is a very wide tab #0\nTab 1 of 20';
|
||||||
const String tab10title = 'This is a very wide tab #10\nTab 11 of 20';
|
const String tab10title = 'This is a very wide tab #10\nTab 11 of 20';
|
||||||
|
|
||||||
|
const List<SemanticsFlag> hiddenFlags = <SemanticsFlag>[SemanticsFlag.isHidden, SemanticsFlag.isFocusable];
|
||||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
||||||
expect(semantics, includesNodeWith(label: tab0title));
|
expect(semantics, includesNodeWith(label: tab0title));
|
||||||
expect(semantics, isNot(includesNodeWith(label: tab10title)));
|
expect(semantics, includesNodeWith(label: tab10title, flags: hiddenFlags));
|
||||||
|
|
||||||
controller.index = 10;
|
controller.index = 10;
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(semantics, isNot(includesNodeWith(label: tab0title)));
|
expect(semantics, includesNodeWith(label: tab0title, flags: hiddenFlags));
|
||||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight]));
|
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight]));
|
||||||
expect(semantics, includesNodeWith(label: tab10title));
|
expect(semantics, includesNodeWith(label: tab10title));
|
||||||
|
|
||||||
@ -3016,7 +3017,7 @@ void main() {
|
|||||||
|
|
||||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
||||||
expect(semantics, includesNodeWith(label: tab0title));
|
expect(semantics, includesNodeWith(label: tab0title));
|
||||||
expect(semantics, isNot(includesNodeWith(label: tab10title)));
|
expect(semantics, includesNodeWith(label: tab10title, flags: hiddenFlags));
|
||||||
|
|
||||||
semantics.dispose();
|
semantics.dispose();
|
||||||
});
|
});
|
||||||
|
@ -624,6 +624,25 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final List<TestSemantics> children = <TestSemantics>[];
|
||||||
|
for (int index = 0; index < 30; index += 1) {
|
||||||
|
final bool isHidden = index < 15 || index > 17;
|
||||||
|
children.add(
|
||||||
|
TestSemantics(
|
||||||
|
flags: isHidden ? <SemanticsFlag>[SemanticsFlag.isHidden] : 0,
|
||||||
|
label: 'Item ${index}a',
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
children.add(
|
||||||
|
TestSemantics(
|
||||||
|
flags: isHidden ? <SemanticsFlag>[SemanticsFlag.isHidden] : 0,
|
||||||
|
label: 'item ${index}b',
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
expect(semantics, hasSemantics(
|
expect(semantics, hasSemantics(
|
||||||
TestSemantics.root(
|
TestSemantics.root(
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
@ -638,72 +657,7 @@ void main() {
|
|||||||
SemanticsAction.scrollUp,
|
SemanticsAction.scrollUp,
|
||||||
SemanticsAction.scrollDown,
|
SemanticsAction.scrollDown,
|
||||||
],
|
],
|
||||||
children: <TestSemantics>[
|
children: children,
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
|
|
||||||
label: 'Item 13a',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
|
|
||||||
label: 'item 13b',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
|
|
||||||
label: 'Item 14a',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
|
|
||||||
label: 'item 14b',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: 'Item 15a',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: 'item 15b',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: 'Item 16a',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: 'item 16b',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: 'Item 17a',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: 'item 17b',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
|
|
||||||
label: 'Item 18a',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
|
|
||||||
label: 'item 18b',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
|
|
||||||
label: 'Item 19a',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
|
|
||||||
label: 'item 19b',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -305,6 +305,19 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
List<TestSemantics> generateSemanticsChildren({int startHidden = -1, int endHidden = 30}) {
|
||||||
|
final List<TestSemantics> children = <TestSemantics>[];
|
||||||
|
for (int index = 0; index < 30; index += 1) {
|
||||||
|
final bool isHidden = index <= startHidden || index >= endHidden;
|
||||||
|
children.add(TestSemantics(
|
||||||
|
label: 'Tile $index',
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
flags: isHidden ? const <SemanticsFlag>[SemanticsFlag.isHidden] : 0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
expect(semantics, hasSemantics(
|
expect(semantics, hasSemantics(
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
@ -315,33 +328,7 @@ void main() {
|
|||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.scrollUp,
|
SemanticsAction.scrollUp,
|
||||||
],
|
],
|
||||||
children: <TestSemantics>[
|
children: generateSemanticsChildren(endHidden: 3),
|
||||||
TestSemantics(
|
|
||||||
label: r'Tile 0',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: r'Tile 1',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: r'Tile 2',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[
|
|
||||||
SemanticsFlag.isHidden,
|
|
||||||
],
|
|
||||||
label: r'Tile 3',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[
|
|
||||||
SemanticsFlag.isHidden,],
|
|
||||||
label: r'Tile 4',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -362,48 +349,7 @@ void main() {
|
|||||||
SemanticsAction.scrollUp,
|
SemanticsAction.scrollUp,
|
||||||
SemanticsAction.scrollDown,
|
SemanticsAction.scrollDown,
|
||||||
],
|
],
|
||||||
children: <TestSemantics>[
|
children: generateSemanticsChildren(startHidden: 14, endHidden: 18),
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[
|
|
||||||
SemanticsFlag.isHidden,
|
|
||||||
],
|
|
||||||
label: r'Tile 13',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[
|
|
||||||
SemanticsFlag.isHidden,
|
|
||||||
],
|
|
||||||
label: r'Tile 14',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: r'Tile 15',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: r'Tile 16',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: r'Tile 17',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[
|
|
||||||
SemanticsFlag.isHidden,
|
|
||||||
],
|
|
||||||
label: r'Tile 18',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[
|
|
||||||
SemanticsFlag.isHidden,
|
|
||||||
],
|
|
||||||
label: r'Tile 19',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -423,34 +369,7 @@ void main() {
|
|||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.scrollDown,
|
SemanticsAction.scrollDown,
|
||||||
],
|
],
|
||||||
children: <TestSemantics>[
|
children: generateSemanticsChildren(startHidden: 26),
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[
|
|
||||||
SemanticsFlag.isHidden,
|
|
||||||
],
|
|
||||||
label: r'Tile 25',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
flags: <SemanticsFlag>[
|
|
||||||
SemanticsFlag.isHidden,
|
|
||||||
],
|
|
||||||
label: r'Tile 26',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: r'Tile 27',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: r'Tile 28',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
TestSemantics(
|
|
||||||
label: r'Tile 29',
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -460,6 +379,85 @@ void main() {
|
|||||||
semantics.dispose();
|
semantics.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('SingleChildScrollView semantics clips cover entire child vertical', (WidgetTester tester) async {
|
||||||
|
final ScrollController controller = ScrollController();
|
||||||
|
final UniqueKey scrollView = UniqueKey();
|
||||||
|
final UniqueKey childBox = UniqueKey();
|
||||||
|
const double length = 10000;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
key: scrollView,
|
||||||
|
controller: controller,
|
||||||
|
child: SizedBox(key: childBox, height: length),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final RenderObject scrollRenderObject = tester.renderObject(find.byKey(scrollView));
|
||||||
|
RenderAbstractViewport? viewport;
|
||||||
|
void findsRenderViewPort(RenderObject child) {
|
||||||
|
if (viewport != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (child is RenderAbstractViewport) {
|
||||||
|
viewport = child;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
child.visitChildren(findsRenderViewPort);
|
||||||
|
}
|
||||||
|
scrollRenderObject.visitChildren(findsRenderViewPort);
|
||||||
|
expect(viewport, isNotNull);
|
||||||
|
final RenderObject childRenderObject = tester.renderObject(find.byKey(childBox));
|
||||||
|
Rect semanticsClip = viewport!.describeSemanticsClip(childRenderObject)!;
|
||||||
|
expect(semanticsClip.size.height, length);
|
||||||
|
|
||||||
|
controller.jumpTo(2000);
|
||||||
|
await tester.pump();
|
||||||
|
semanticsClip = viewport!.describeSemanticsClip(childRenderObject)!;
|
||||||
|
expect(semanticsClip.size.height, length);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('SingleChildScrollView semantics clips cover entire child', (WidgetTester tester) async {
|
||||||
|
final ScrollController controller = ScrollController();
|
||||||
|
final UniqueKey scrollView = UniqueKey();
|
||||||
|
final UniqueKey childBox = UniqueKey();
|
||||||
|
const double length = 10000;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
key: scrollView,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
controller: controller,
|
||||||
|
child: SizedBox(key: childBox, width: length),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final RenderObject scrollRenderObject = tester.renderObject(find.byKey(scrollView));
|
||||||
|
RenderAbstractViewport? viewport;
|
||||||
|
void findsRenderViewPort(RenderObject child) {
|
||||||
|
if (viewport != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (child is RenderAbstractViewport) {
|
||||||
|
viewport = child;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
child.visitChildren(findsRenderViewPort);
|
||||||
|
}
|
||||||
|
scrollRenderObject.visitChildren(findsRenderViewPort);
|
||||||
|
expect(viewport, isNotNull);
|
||||||
|
final RenderObject childRenderObject = tester.renderObject(find.byKey(childBox));
|
||||||
|
Rect semanticsClip = viewport!.describeSemanticsClip(childRenderObject)!;
|
||||||
|
expect(semanticsClip.size.width, length);
|
||||||
|
|
||||||
|
controller.jumpTo(2000);
|
||||||
|
await tester.pump();
|
||||||
|
semanticsClip = viewport!.describeSemanticsClip(childRenderObject)!;
|
||||||
|
expect(semanticsClip.size.width, length);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('SingleChildScrollView getOffsetToReveal - down', (WidgetTester tester) async {
|
testWidgets('SingleChildScrollView getOffsetToReveal - down', (WidgetTester tester) async {
|
||||||
List<Widget> children;
|
List<Widget> children;
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user