blob: 76c50d41bf82d8d8f5ad829a70ce57f2285302d9 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
// Helpers
final Widget sliverBox = SliverToBoxAdapter(
child: Container(
color: Colors.amber,
height: 150.0,
width: 150,
),
);
Widget boilerplate(
List<Widget> slivers, {
ScrollController? controller,
Axis scrollDirection = Axis.vertical,
}) {
return MaterialApp(
theme: ThemeData(
materialTapTargetSize: MaterialTapTargetSize.padded,
),
home: Scaffold(
body: CustomScrollView(
scrollDirection: scrollDirection,
slivers: slivers,
controller: controller,
),
),
);
}
group('SliverFillRemaining', () {
group('hasScrollBody: true, default', () {
testWidgets('no siblings', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
SliverFillRemaining(child: Container()),
],
),
),
);
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(600.0),
);
controller.jumpTo(50.0);
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(600.0),
);
controller.jumpTo(-100.0);
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(600.0),
);
controller.jumpTo(0.0);
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(600.0),
);
});
testWidgets('one sibling', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
const SliverToBoxAdapter(child: SizedBox(height: 100.0)),
SliverFillRemaining(child: Container()),
],
),
),
);
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(500.0),
);
controller.jumpTo(50.0);
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(550.0),
);
controller.jumpTo(-100.0);
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(400.0),
);
controller.jumpTo(0.0);
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(500.0),
);
});
testWidgets('scrolls beyond viewportMainAxisExtent', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
child: Container(color: Colors.white),
),
];
await tester.pumpWidget(boilerplate(slivers, controller: controller));
expect(controller.offset, 0.0);
expect(find.byType(Container), findsNWidgets(2));
controller.jumpTo(150.0);
await tester.pumpAndSettle();
expect(controller.offset, 150.0);
expect(find.byType(Container), findsOneWidget);
});
});
group('hasScrollBody: false', () {
testWidgets('does not extend past viewportMainAxisExtent', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
child: Container(color: Colors.white),
),
];
await tester.pumpWidget(boilerplate(slivers, controller: controller));
expect(controller.offset, 0.0);
expect(find.byType(Container), findsNWidgets(2));
controller.jumpTo(150.0);
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(find.byType(Container), findsNWidgets(2));
});
testWidgets('child without size is sized by extent', (WidgetTester tester) async {
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
child: Container(color: Colors.blue),
),
];
await tester.pumpWidget(boilerplate(slivers));
RenderBox box = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box.size.height, equals(450));
await tester.pumpWidget(boilerplate(
slivers,
scrollDirection: Axis.horizontal,
));
box = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box.size.width, equals(650));
});
testWidgets('child with smaller size is sized by extent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
child: Container(
key: key,
color: Colors.blue,
child: Align(
alignment: Alignment.bottomCenter,
child: ElevatedButton(
child: const Text('bottomCenter button'),
onPressed: () {},
),
),
),
),
];
await tester.pumpWidget(boilerplate(slivers));
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
// Also check that the button alignment is true to expectations
final Finder button = find.byType(ElevatedButton);
expect(tester.getBottomLeft(button).dy, equals(600.0));
expect(tester.getCenter(button).dx, equals(400.0));
// Check Axis.horizontal
await tester.pumpWidget(boilerplate(
slivers,
scrollDirection: Axis.horizontal,
));
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.width,
equals(650),
);
});
testWidgets('extent is overridden by child with larger size', (WidgetTester tester) async {
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
child: Container(
color: Colors.blue,
height: 600,
width: 1000,
),
),
];
await tester.pumpWidget(boilerplate(slivers));
RenderBox box = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box.size.height, equals(600));
await tester.pumpWidget(boilerplate(
slivers,
scrollDirection: Axis.horizontal,
));
box = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box.size.width, equals(1000));
});
testWidgets('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final List<Widget> slivers = <Widget>[
SliverFixedExtentList(
itemExtent: 150,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Container(color: Colors.amber),
childCount: 5,
),
),
SliverFillRemaining(
hasScrollBody: false,
child: Container(
key: key,
color: Colors.blue[300],
child: Align(
child: Padding(
padding: const EdgeInsets.all(50.0),
child: ElevatedButton(
child: const Text('center button'),
onPressed: () {},
),
),
),
),
),
];
await tester.pumpWidget(boilerplate(slivers));
await tester.drag(find.byType(Scrollable), const Offset(0.0, -750.0));
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(148.0),
);
// Also check that the button alignment is true to expectations
final Finder button = find.byType(ElevatedButton);
expect(tester.getBottomLeft(button).dy, equals(550.0));
expect(tester.getCenter(button).dx, equals(400.0));
});
testWidgets('alignment with a flexible works', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
child: Column(
key: key,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Flexible(
child: Center(child: FlutterLogo(size: 100)),
),
ElevatedButton(
child: const Text('Bottom'),
onPressed: () {},
),
],
),
),
];
await tester.pumpWidget(boilerplate(slivers));
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
// Check that the logo alignment is true to expectations
final Finder logo = find.byType(FlutterLogo);
expect(
tester.renderObject<RenderBox>(logo).size,
const Size(100.0, 100.0),
);
final VisualDensity density = VisualDensity.adaptivePlatformDensity;
expect(tester.getCenter(logo), Offset(400.0, 351.0 - density.vertical * 2.0));
// Also check that the button alignment is true to expectations
// Buttons do not decrease their horizontal padding per the VisualDensity.
final Finder button = find.byType(ElevatedButton);
expect(
tester.renderObject<RenderBox>(button).size,
Size(116.0 + math.max(0, density.horizontal) * 8.0, 48.0 + density.vertical * 4.0),
);
expect(tester.getBottomLeft(button).dy, equals(600.0));
expect(tester.getCenter(button).dx, equals(400.0));
// Overscroll and see that alignment and size is maintained
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
expect(
tester.renderObject<RenderBox>(logo).size,
const Size(100.0, 100.0),
);
expect(tester.getCenter(logo).dy, lessThan(351.0));
expect(
tester.renderObject<RenderBox>(button).size,
// Buttons do not decrease their horizontal padding per the VisualDensity.
Size(116.0 + math.max(0, density.horizontal) * 8.0, 48.0 + density.vertical * 4.0),
);
expect(tester.getBottomLeft(button).dy, lessThan(600.0));
expect(tester.getCenter(button).dx, equals(400.0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
group('fillOverscroll: true, relevant platforms', () {
testWidgets('child without size is sized by extent and overscroll', (WidgetTester tester) async {
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: true,
child: Container(color: Colors.blue),
),
];
// Check size
await tester.pumpWidget(boilerplate(slivers));
final RenderBox box1 = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box1.size.height, equals(450));
// Overscroll and check size
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
final RenderBox box2 = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box2.size.height, greaterThan(450));
// Ensure overscroll retracts to original size after releasing gesture
await tester.pumpAndSettle();
final RenderBox box3 = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box3.size.height, equals(450));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('child with smaller size is overridden and sized by extent and overscroll', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: true,
child: Container(
key: key,
color: Colors.blue,
child: Align(
alignment: Alignment.bottomCenter,
child: ElevatedButton(
child: const Text('bottomCenter button'),
onPressed: () {},
),
),
),
),
];
await tester.pumpWidget(boilerplate(slivers));
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
greaterThan(450),
);
// Also check that the button alignment is true to expectations, even with
// child stretching to fill overscroll
final Finder button = find.byType(ElevatedButton);
expect(tester.getBottomLeft(button).dy, equals(600.0));
expect(tester.getCenter(button).dx, equals(400.0));
// Ensure overscroll retracts to original size after releasing gesture
await tester.pumpAndSettle();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('extent is overridden by child size and overscroll if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final ScrollController controller = ScrollController();
final List<Widget> slivers = <Widget>[
SliverFixedExtentList(
itemExtent: 150,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) =>
Container(color: Colors.amber),
childCount: 5,
),
),
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: true,
child: Container(
key: key,
color: Colors.blue[300],
child: Align(
child: Padding(
padding: const EdgeInsets.all(50.0),
child: ElevatedButton(
child: const Text('center button'),
onPressed: () {},
),
),
),
),
),
];
await tester.pumpWidget(boilerplate(slivers, controller: controller));
// Scroll to the end
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(148.0 + VisualDensity.adaptivePlatformDensity.vertical * 4.0),
);
// Check that the button alignment is true to expectations
final Finder button = find.byType(ElevatedButton);
expect(tester.getBottomLeft(button).dy, equals(550.0));
expect(tester.getCenter(button).dx, equals(400.0));
// Drag for overscroll
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
greaterThan(148.0),
);
// Check that the button alignment is still centered in stretched child
expect(tester.getBottomLeft(button).dy, lessThan(550.0));
expect(tester.getCenter(button).dx, equals(400.0));
// Ensure overscroll retracts to original size after releasing gesture
await tester.pumpAndSettle();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(148.0 + VisualDensity.adaptivePlatformDensity.vertical * 4.0),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('fillOverscroll works when child has no size and precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final ScrollController controller = ScrollController();
final List<Widget> slivers = <Widget>[
SliverFixedExtentList(
itemExtent: 150,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Semantics(label: index.toString(), child: Container(color: Colors.amber));
},
childCount: 5,
),
),
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: true,
child: Container(
key: key,
color: Colors.blue,
),
),
];
await tester.pumpWidget(boilerplate(slivers, controller: controller));
expect(find.byKey(key), findsNothing);
expect(
find.bySemanticsLabel('4'),
findsNothing,
);
// Scroll to bottom
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pump();
// Check item at the end of the list
expect(find.byKey(key), findsNothing);
expect(
find.bySemanticsLabel('4'),
findsOneWidget,
);
// Overscroll
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
// Check for new item at the end of the now overscrolled list
expect(find.byKey(key), findsOneWidget);
expect(
find.bySemanticsLabel('4'),
findsOneWidget,
);
// Ensure overscroll retracts to original size after releasing gesture
await tester.pumpAndSettle();
expect(find.byKey(key), findsNothing);
expect(
find.bySemanticsLabel('4'),
findsOneWidget,
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('alignment with a flexible works with fillOverscroll', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: true,
child: Column(
key: key,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Flexible(
child: Center(child: FlutterLogo(size: 100)),
),
ElevatedButton(
child: const Text('Bottom'),
onPressed: () {},
),
],
),
),
];
await tester.pumpWidget(boilerplate(slivers));
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
// Check that the logo alignment is true to expectations.
final Finder logo = find.byType(FlutterLogo);
expect(
tester.renderObject<RenderBox>(logo).size,
const Size(100.0, 100.0),
);
final VisualDensity density = VisualDensity.adaptivePlatformDensity;
expect(tester.getCenter(logo), Offset(400.0, 351.0 - density.vertical * 2.0));
// Also check that the button alignment is true to expectations.
// Buttons do not decrease their horizontal padding per the VisualDensity.
final Finder button = find.byType(ElevatedButton);
expect(
tester.renderObject<RenderBox>(button).size,
Size(116.0 + math.max(0, density.horizontal) * 8.0, 48.0 + density.vertical * 4.0),
);
expect(tester.getBottomLeft(button).dy, equals(600.0));
expect(tester.getCenter(button).dx, equals(400.0));
// Overscroll and see that logo alignment shifts to maintain center as
// container stretches with overscroll, button remains aligned at the
// bottom.
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
greaterThan(450),
);
expect(
tester.renderObject<RenderBox>(logo).size,
const Size(100.0, 100.0),
);
expect(tester.getCenter(logo).dy, lessThan(351.0));
expect(
tester.renderObject<RenderBox>(button).size,
// Buttons do not decrease their horizontal padding per the VisualDensity.
Size(116.0 + math.max(0, density.horizontal) * 8.0, 48.0 + density.vertical * 4.0),
);
expect(tester.getBottomLeft(button).dy, equals(600.0));
expect(tester.getCenter(button).dx, equals(400.0));
// Ensure overscroll retracts to original position when gesture is
// released.
await tester.pumpAndSettle();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
expect(
tester.renderObject<RenderBox>(logo).size,
const Size(100.0, 100.0),
);
expect(tester.getCenter(logo), Offset(400.0, 351.0 - density.vertical * 2.0));
expect(
tester.renderObject<RenderBox>(button).size,
// Buttons do not decrease their horizontal padding per the VisualDensity.
Size(116.0 + math.max(0, density.horizontal) * 8.0, 48.0 + density.vertical * 4.0),
);
expect(tester.getBottomLeft(button).dy, equals(600.0));
expect(tester.getCenter(button).dx, equals(400.0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
});
group('fillOverscroll: true, is ignored on irrelevant platforms', () {
// Android/Other scroll physics when hasScrollBody: false, ignores fillOverscroll: true
testWidgets('child without size is sized by extent', (WidgetTester tester) async {
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: true,
child: Container(color: Colors.blue),
),
];
await tester.pumpWidget(boilerplate(slivers));
final RenderBox box1 = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box1.size.height, equals(450));
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
final RenderBox box2 = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box2.size.height, equals(450));
});
testWidgets('child with size is overridden and sized by extent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: true,
child: Container(
key: key,
color: Colors.blue,
child: Align(
alignment: Alignment.bottomCenter,
child: ElevatedButton(
child: const Text('bottomCenter button'),
onPressed: () {},
),
),
),
),
];
await tester.pumpWidget(boilerplate(slivers));
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
// Also check that the button alignment is true to expectations
final Finder button = find.byType(ElevatedButton);
expect(tester.getBottomLeft(button).dy, equals(600.0));
expect(tester.getCenter(button).dx, equals(400.0));
});
testWidgets('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final ScrollController controller = ScrollController();
final List<Widget> slivers = <Widget>[
SliverFixedExtentList(
itemExtent: 150,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) =>
Container(color: Colors.amber),
childCount: 5,
),
),
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: true,
child: Container(
key: key,
color: Colors.blue[300],
child: Align(
child: Padding(
padding: const EdgeInsets.all(50.0),
child: ElevatedButton(
child: const Text('center button'),
onPressed: () {},
),
),
),
),
),
];
await tester.pumpWidget(boilerplate(slivers, controller: controller));
// Scroll to the end
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(148.0),
);
// Check that the button alignment is true to expectations
final Finder button = find.byType(ElevatedButton);
expect(tester.getBottomLeft(button).dy, equals(550.0));
expect(tester.getCenter(button).dx, equals(400.0));
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(148.0),
);
// Check that the button alignment is still centered
expect(tester.getBottomLeft(button).dy, equals(550.0));
expect(tester.getCenter(button).dx, equals(400.0));
});
testWidgets('child has no size and precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final ScrollController controller = ScrollController();
final List<Widget> slivers = <Widget>[
SliverFixedExtentList(
itemExtent: 150,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Semantics(label: index.toString(), child: Container(color: Colors.amber));
},
childCount: 5,
),
),
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: true,
child: Container(
key: key,
color: Colors.blue,
),
),
];
await tester.pumpWidget(boilerplate(slivers, controller: controller));
expect(find.byKey(key), findsNothing);
expect(
find.bySemanticsLabel('4'),
findsNothing,
);
// Scroll to bottom
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pump();
// End of list
expect(find.byKey(key), findsNothing);
expect(
find.bySemanticsLabel('4'),
findsOneWidget,
);
// Overscroll
await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0));
await tester.pump();
expect(find.byKey(key), findsNothing);
expect(
find.bySemanticsLabel('4'),
findsOneWidget,
);
});
});
});
});
}