diff --git a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart index ea34041e07..3ae6361548 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -326,16 +326,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix _RenderSingleChildViewport({ AxisDirection axisDirection = AxisDirection.down, required ViewportOffset offset, - double cacheExtent = RenderAbstractViewport.defaultCacheExtent, RenderBox? child, required Clip clipBehavior, }) : assert(axisDirection != null), assert(offset != null), - assert(cacheExtent != null), assert(clipBehavior != null), _axisDirection = axisDirection, _offset = offset, - _cacheExtent = cacheExtent, _clipBehavior = clipBehavior { this.child = child; } @@ -370,18 +367,6 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix 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} /// /// Defaults to [Clip.none], and must not be null. @@ -700,19 +685,34 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix @override Rect describeSemanticsClip(RenderObject child) { assert(axis != null); - switch (axis) { - case Axis.vertical: + final double remainingOffset = _maxScrollExtent - offset.pixels; + switch (axisDirection) { + case AxisDirection.up: return Rect.fromLTRB( semanticBounds.left, - semanticBounds.top - cacheExtent, + semanticBounds.top - remainingOffset, semanticBounds.right, - semanticBounds.bottom + cacheExtent, + semanticBounds.bottom + offset.pixels, ); - case Axis.horizontal: + case AxisDirection.right: return Rect.fromLTRB( - semanticBounds.left - cacheExtent, + semanticBounds.left - offset.pixels, 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, ); } diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index e6c2bb67cb..7da052c89f 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -2995,14 +2995,15 @@ void main() { 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 List hiddenFlags = [SemanticsFlag.isHidden, SemanticsFlag.isFocusable]; expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft])); expect(semantics, includesNodeWith(label: tab0title)); - expect(semantics, isNot(includesNodeWith(label: tab10title))); + expect(semantics, includesNodeWith(label: tab10title, flags: hiddenFlags)); controller.index = 10; await tester.pumpAndSettle(); - expect(semantics, isNot(includesNodeWith(label: tab0title))); + expect(semantics, includesNodeWith(label: tab0title, flags: hiddenFlags)); expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollRight])); expect(semantics, includesNodeWith(label: tab10title)); @@ -3016,7 +3017,7 @@ void main() { expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft])); expect(semantics, includesNodeWith(label: tab0title)); - expect(semantics, isNot(includesNodeWith(label: tab10title))); + expect(semantics, includesNodeWith(label: tab10title, flags: hiddenFlags)); semantics.dispose(); }); diff --git a/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart b/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart index 2e12a30e8c..ff54d1084e 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart @@ -624,6 +624,25 @@ void main() { ), ); + final List children = []; + for (int index = 0; index < 30; index += 1) { + final bool isHidden = index < 15 || index > 17; + children.add( + TestSemantics( + flags: isHidden ? [SemanticsFlag.isHidden] : 0, + label: 'Item ${index}a', + textDirection: TextDirection.ltr, + ), + ); + children.add( + TestSemantics( + flags: isHidden ? [SemanticsFlag.isHidden] : 0, + label: 'item ${index}b', + textDirection: TextDirection.ltr, + ), + ); + } + expect(semantics, hasSemantics( TestSemantics.root( children: [ @@ -638,72 +657,7 @@ void main() { SemanticsAction.scrollUp, SemanticsAction.scrollDown, ], - children: [ - TestSemantics( - flags: [SemanticsFlag.isHidden], - label: 'Item 13a', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [SemanticsFlag.isHidden], - label: 'item 13b', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [SemanticsFlag.isHidden], - label: 'Item 14a', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [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.isHidden], - label: 'Item 18a', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [SemanticsFlag.isHidden], - label: 'item 18b', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [SemanticsFlag.isHidden], - label: 'Item 19a', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [SemanticsFlag.isHidden], - label: 'item 19b', - textDirection: TextDirection.ltr, - ), - ], + children: children, ), ], ), diff --git a/packages/flutter/test/widgets/single_child_scroll_view_test.dart b/packages/flutter/test/widgets/single_child_scroll_view_test.dart index 2933af4030..1cf8516611 100644 --- a/packages/flutter/test/widgets/single_child_scroll_view_test.dart +++ b/packages/flutter/test/widgets/single_child_scroll_view_test.dart @@ -305,6 +305,19 @@ void main() { ), ); + List generateSemanticsChildren({int startHidden = -1, int endHidden = 30}) { + final List children = []; + 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.isHidden] : 0, + )); + } + return children; + } + expect(semantics, hasSemantics( TestSemantics( children: [ @@ -315,33 +328,7 @@ void main() { actions: [ SemanticsAction.scrollUp, ], - children: [ - 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.isHidden, - ], - label: r'Tile 3', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [ - SemanticsFlag.isHidden,], - label: r'Tile 4', - textDirection: TextDirection.ltr, - ), - ], + children: generateSemanticsChildren(endHidden: 3), ), ], ), @@ -362,48 +349,7 @@ void main() { SemanticsAction.scrollUp, SemanticsAction.scrollDown, ], - children: [ - TestSemantics( - flags: [ - SemanticsFlag.isHidden, - ], - label: r'Tile 13', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [ - 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.isHidden, - ], - label: r'Tile 18', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [ - SemanticsFlag.isHidden, - ], - label: r'Tile 19', - textDirection: TextDirection.ltr, - ), - ], + children: generateSemanticsChildren(startHidden: 14, endHidden: 18), ), ], ), @@ -423,34 +369,7 @@ void main() { actions: [ SemanticsAction.scrollDown, ], - children: [ - TestSemantics( - flags: [ - SemanticsFlag.isHidden, - ], - label: r'Tile 25', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [ - 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, - ), - ], + children: generateSemanticsChildren(startHidden: 26), ), ], ), @@ -460,6 +379,85 @@ void main() { 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 { List children; await tester.pumpWidget(