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
assert(correctedOffset.isFinite);
_maxScrollExtent = 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) {
case CacheExtentStyle.pixel:
_calculatedCacheExtent = cacheExtent;
......@@ -1928,6 +1931,7 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta
_calculatedCacheExtent = mainAxisExtent * _cacheExtent;
break;
}
return layoutChildSequence(
child: firstChild,
scrollOffset: math.max(0.0, correctedOffset),
......
......@@ -1846,41 +1846,50 @@ void main() {
});
});
Widget _buildShrinkWrap({
ScrollController? controller,
Axis scrollDirection = Axis.vertical,
ScrollPhysics? physics,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: ListView.builder(
controller: controller,
physics: physics,
scrollDirection: scrollDirection,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) => SizedBox(height: 50, width: 50, child: Text('Item $index')),
itemCount: 20,
itemExtent: 50,
group('Overscrolling RenderShrinkWrappingViewport', () {
Widget _buildSimpleShrinkWrap({
ScrollController? controller,
Axis scrollDirection = Axis.vertical,
ScrollPhysics? physics,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: ListView.builder(
controller: controller,
physics: physics,
scrollDirection: scrollDirection,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) => SizedBox(height: 50, width: 50, child: Text('Item $index')),
itemCount: 20,
itemExtent: 50,
),
),
),
);
}
);
}
testWidgets('Constrained Shrinkwrapping viewport will not overflow on overscroll', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/89717
final ScrollController controller = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
Widget _buildClippingShrinkWrap(
ScrollController controller, {
bool constrain = false,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Container(
color: const Color(0xFF000000),
child: Column(
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(
height: 150,
height: constrain ? 150 : null,
color: const Color(0xFFF44336),
child: ListView.builder(
controller: controller,
......@@ -1890,189 +1899,230 @@ void main() {
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
final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
testWidgets('constrained viewport correctly clips overflow', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/89717
final ScrollController controller = ScrollController();
await tester.pumpWidget(
_buildClippingShrinkWrap(controller, constrain: true)
);
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)));
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('correctly clips overflow without constraints', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/89717
final ScrollController controller = ScrollController();
await tester.pumpWidget(
_buildClippingShrinkWrap(controller)
);
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_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 {
// 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(
_buildShrinkWrap(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('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(
_buildSimpleShrinkWrap(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);
// 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('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)));
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);
});
// End
final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
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 {
// 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(
_buildShrinkWrap(
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);
// 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);
});
// End
final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
testWidgets('allows overscrolling per physics - horizontal', (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,
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)));
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);
// End
final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
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 {
......
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