Unverified Commit ce4d635a authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Fix visual overflow when overscrolling RenderShrinkWrappingViewport (#91620)

parent 42eb9032
...@@ -1919,7 +1919,10 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta ...@@ -1919,7 +1919,10 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta
assert(correctedOffset.isFinite); assert(correctedOffset.isFinite);
_maxScrollExtent = 0.0; _maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0; _shrinkWrapExtent = 0.0;
_hasVisualOverflow = false; // Since the viewport is shrinkwrapped, we know that any negative overscroll
// into the potentially infinite mainAxisExtent will overflow the end of
// the viewport.
_hasVisualOverflow = correctedOffset < 0.0;
switch (cacheExtentStyle) { switch (cacheExtentStyle) {
case CacheExtentStyle.pixel: case CacheExtentStyle.pixel:
_calculatedCacheExtent = cacheExtent; _calculatedCacheExtent = cacheExtent;
...@@ -1928,6 +1931,7 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta ...@@ -1928,6 +1931,7 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta
_calculatedCacheExtent = mainAxisExtent * _cacheExtent; _calculatedCacheExtent = mainAxisExtent * _cacheExtent;
break; break;
} }
return layoutChildSequence( return layoutChildSequence(
child: firstChild, child: firstChild,
scrollOffset: math.max(0.0, correctedOffset), scrollOffset: math.max(0.0, correctedOffset),
......
...@@ -1846,41 +1846,50 @@ void main() { ...@@ -1846,41 +1846,50 @@ void main() {
}); });
}); });
Widget _buildShrinkWrap({ group('Overscrolling RenderShrinkWrappingViewport', () {
ScrollController? controller, Widget _buildSimpleShrinkWrap({
Axis scrollDirection = Axis.vertical, ScrollController? controller,
ScrollPhysics? physics, Axis scrollDirection = Axis.vertical,
}) { ScrollPhysics? physics,
return Directionality( }) {
textDirection: TextDirection.ltr, return Directionality(
child: MediaQuery( textDirection: TextDirection.ltr,
data: const MediaQueryData(), child: MediaQuery(
child: ListView.builder( data: const MediaQueryData(),
controller: controller, child: ListView.builder(
physics: physics, controller: controller,
scrollDirection: scrollDirection, physics: physics,
shrinkWrap: true, scrollDirection: scrollDirection,
itemBuilder: (BuildContext context, int index) => SizedBox(height: 50, width: 50, child: Text('Item $index')), shrinkWrap: true,
itemCount: 20, itemBuilder: (BuildContext context, int index) => SizedBox(height: 50, width: 50, child: Text('Item $index')),
itemExtent: 50, itemCount: 20,
itemExtent: 50,
),
), ),
), );
); }
}
testWidgets('Constrained Shrinkwrapping viewport will not overflow on overscroll', (WidgetTester tester) async { Widget _buildClippingShrinkWrap(
// Regression test for https://github.com/flutter/flutter/issues/89717 ScrollController controller, {
final ScrollController controller = ScrollController(); bool constrain = false,
await tester.pumpWidget( }) {
Directionality( return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: const MediaQueryData(), data: const MediaQueryData(),
child: Container(
color: const Color(0xFF000000),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
Container(height: 100, color: const Color(0x00000000)), // Translucent boxes above and below the shrinkwrapped viewport
// make it easily discernible if the viewport is not being
// clipped properly.
Opacity(
opacity: 0.5,
child: Container(height: 100, color: const Color(0xFF00B0FF)),
),
Container( Container(
height: 150, height: constrain ? 150 : null,
color: const Color(0xFFF44336), color: const Color(0xFFF44336),
child: ListView.builder( child: ListView.builder(
controller: controller, controller: controller,
...@@ -1890,189 +1899,230 @@ void main() { ...@@ -1890,189 +1899,230 @@ void main() {
itemCount: 10, itemCount: 10,
), ),
), ),
Container(height: 100, color: const Color(0x00000000)), Opacity(
opacity: 0.5,
child: Container(height: 100, color: const Color(0xFF00B0FF)),
),
], ],
), ),
), ),
) ),
); );
expect(controller.offset, 0.0); }
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
// Overscroll
final TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.text('Item 0')));
await overscrollGesture.moveBy(const Offset(0, 25));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 125.0);
await expectLater(
find.byType(Directionality),
matchesGoldenFile('shrinkwrapped_overscroll.png'),
);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
});
testWidgets('Shrinkwrap allows overscrolling on default platforms - vertical', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/10949
// Scrollables should overscroll by default on iOS and macOS
final ScrollController controller = ScrollController();
await tester.pumpWidget(
_buildShrinkWrap(controller: controller),
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
// Check overscroll at both ends
// Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(0, 25));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
// End
final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(0, -25));
await tester.pump();
expect(controller.offset, greaterThan(maxExtent));
expect(tester.getBottomLeft(find.text('Item 19')).dy, 575.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Shrinkwrap allows overscrolling on default platforms - horizontal', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/10949
// Scrollables should overscroll by default on iOS and macOS
final ScrollController controller = ScrollController();
await tester.pumpWidget(
_buildShrinkWrap(controller: controller, scrollDirection: Axis.horizontal),
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
// Check overscroll at both ends
// Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(25, 0));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
// End testWidgets('constrained viewport correctly clips overflow', (WidgetTester tester) async {
final double maxExtent = controller.position.maxScrollExtent; // Regression test for https://github.com/flutter/flutter/issues/89717
controller.jumpTo(controller.position.maxScrollExtent); final ScrollController controller = ScrollController();
await tester.pumpAndSettle(); await tester.pumpWidget(
expect(controller.offset, maxExtent); _buildClippingShrinkWrap(controller, constrain: true)
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0); );
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
// Overscroll
final TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.text('Item 0')));
await overscrollGesture.moveBy(const Offset(0, 100));
await tester.pump();
expect(controller.offset, -100.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 200.0);
await expectLater(
find.byType(Directionality),
matchesGoldenFile('shrinkwrap_clipped_constrained_overscroll.png'),
);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
});
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView))); testWidgets('correctly clips overflow without constraints', (WidgetTester tester) async {
await overscrollGesture.moveBy(const Offset(-25, 0)); // Regression test for https://github.com/flutter/flutter/issues/89717
await tester.pump(); final ScrollController controller = ScrollController();
expect(controller.offset, greaterThan(maxExtent)); await tester.pumpWidget(
expect(tester.getTopRight(find.text('Item 19')).dx, 775.0); _buildClippingShrinkWrap(controller)
await overscrollGesture.up(); );
await tester.pumpAndSettle(); expect(controller.offset, 0.0);
expect(controller.offset, maxExtent); expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0); expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
// Overscroll
final TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.text('Item 0')));
await overscrollGesture.moveBy(const Offset(0, 100));
await tester.pump();
expect(controller.offset, -100.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 200.0);
await expectLater(
find.byType(Directionality),
matchesGoldenFile('shrinkwrap_clipped_overscroll.png'),
);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
});
testWidgets('Shrinkwrap allows overscrolling per physics - vertical', (WidgetTester tester) async { testWidgets('allows overscrolling on default platforms - vertical', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/10949 // Regression test for https://github.com/flutter/flutter/issues/10949
// Scrollables should overscroll when the scroll physics allow // Scrollables should overscroll by default on iOS and macOS
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
_buildShrinkWrap(controller: controller, physics: const BouncingScrollPhysics()), _buildSimpleShrinkWrap(controller: controller),
); );
expect(controller.offset, 0.0); expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0); expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
// Check overscroll at both ends // Check overscroll at both ends
// Start // Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView))); TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(0, 25)); await overscrollGesture.moveBy(const Offset(0, 25));
await tester.pump(); await tester.pump();
expect(controller.offset, -25.0); expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 25.0); expect(tester.getTopLeft(find.text('Item 0')).dy, 25.0);
await overscrollGesture.up(); await overscrollGesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.offset, 0.0); expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0); expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
// End // End
final double maxExtent = controller.position.maxScrollExtent; final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent); controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.offset, maxExtent); expect(controller.offset, maxExtent);
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0); expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(0, -25));
await tester.pump();
expect(controller.offset, greaterThan(maxExtent));
expect(tester.getBottomLeft(find.text('Item 19')).dy, 575.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('allows overscrolling on default platforms - horizontal', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/10949
// Scrollables should overscroll by default on iOS and macOS
final ScrollController controller = ScrollController();
await tester.pumpWidget(
_buildSimpleShrinkWrap(controller: controller, scrollDirection: Axis.horizontal),
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
// Check overscroll at both ends
// Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(25, 0));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView))); // End
await overscrollGesture.moveBy(const Offset(0, -25)); final double maxExtent = controller.position.maxScrollExtent;
await tester.pump(); controller.jumpTo(controller.position.maxScrollExtent);
expect(controller.offset, greaterThan(maxExtent)); await tester.pumpAndSettle();
expect(tester.getBottomLeft(find.text('Item 19')).dy, 575.0); expect(controller.offset, maxExtent);
await overscrollGesture.up(); expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent); overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0); await overscrollGesture.moveBy(const Offset(-25, 0));
}); await tester.pump();
expect(controller.offset, greaterThan(maxExtent));
expect(tester.getTopRight(find.text('Item 19')).dx, 775.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('allows overscrolling per physics - vertical', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/10949
// Scrollables should overscroll when the scroll physics allow
final ScrollController controller = ScrollController();
await tester.pumpWidget(
_buildSimpleShrinkWrap(controller: controller, physics: const BouncingScrollPhysics()),
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
// Check overscroll at both ends
// Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(0, 25));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
testWidgets('Shrinkwrap allows overscrolling per physics - horizontal', (WidgetTester tester) async { // End
// Regression test for https://github.com/flutter/flutter/issues/10949 final double maxExtent = controller.position.maxScrollExtent;
// Scrollables should overscroll when the scroll physics allow controller.jumpTo(controller.position.maxScrollExtent);
final ScrollController controller = ScrollController(); await tester.pumpAndSettle();
await tester.pumpWidget( expect(controller.offset, maxExtent);
_buildShrinkWrap( expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
controller: controller,
scrollDirection: Axis.horizontal, overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
physics: const BouncingScrollPhysics(), await overscrollGesture.moveBy(const Offset(0, -25));
), await tester.pump();
); expect(controller.offset, greaterThan(maxExtent));
expect(controller.offset, 0.0); expect(tester.getBottomLeft(find.text('Item 19')).dy, 575.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0); await overscrollGesture.up();
// Check overscroll at both ends await tester.pumpAndSettle();
// Start expect(controller.offset, maxExtent);
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView))); expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
await overscrollGesture.moveBy(const Offset(25, 0)); });
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
// End testWidgets('allows overscrolling per physics - horizontal', (WidgetTester tester) async {
final double maxExtent = controller.position.maxScrollExtent; // Regression test for https://github.com/flutter/flutter/issues/10949
controller.jumpTo(controller.position.maxScrollExtent); // Scrollables should overscroll when the scroll physics allow
await tester.pumpAndSettle(); final ScrollController controller = ScrollController();
expect(controller.offset, maxExtent); await tester.pumpWidget(
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0); _buildSimpleShrinkWrap(
controller: controller,
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
),
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
// Check overscroll at both ends
// Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(25, 0));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView))); // End
await overscrollGesture.moveBy(const Offset(-25, 0)); final double maxExtent = controller.position.maxScrollExtent;
await tester.pump(); controller.jumpTo(controller.position.maxScrollExtent);
expect(controller.offset, greaterThan(maxExtent)); await tester.pumpAndSettle();
expect(tester.getTopRight(find.text('Item 19')).dx, 775.0); expect(controller.offset, maxExtent);
await overscrollGesture.up(); expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent); overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0); await overscrollGesture.moveBy(const Offset(-25, 0));
await tester.pump();
expect(controller.offset, greaterThan(maxExtent));
expect(tester.getTopRight(find.text('Item 19')).dx, 775.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
});
}); });
testWidgets('Handles infinite constraints when TargetPlatform is iOS or macOS', (WidgetTester tester) async { testWidgets('Handles infinite constraints when TargetPlatform is iOS or macOS', (WidgetTester tester) async {
......
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