SliverFillRemaining flag for different use cases (#33627)

* WIP

* Fixing nested scroll view tests

* Fixing nested scroll view tests

* Fixing nested scroll view tests

* WIP

* Added flag for SliverFillRemaining

* Clean up

* Review

* Reverted nested scroll view tests

* Updating tests

* Updated for overscroll behavior
diff --git a/packages/flutter/lib/src/rendering/sliver_fill.dart b/packages/flutter/lib/src/rendering/sliver_fill.dart
index 89ca497..1adbe83 100644
--- a/packages/flutter/lib/src/rendering/sliver_fill.dart
+++ b/packages/flutter/lib/src/rendering/sliver_fill.dart
@@ -113,18 +113,35 @@
   /// the remaining space in the viewport.
   RenderSliverFillRemaining({
     RenderBox child,
-  }) : super(child: child);
+    this.hasScrollBody = true,
+  }) : assert(hasScrollBody != null),
+       super(child: child);
+
+  /// Whether the child has a scrollable body, this value cannot be null.
+  ///
+  /// Defaults to true such that the child will extend beyond the viewport and
+  /// scroll, as seen in [NestedScrollView].
+  ///
+  /// Setting this value to false will allow the child to fill the remainder of
+  /// the viewport and not extend further.
+  bool hasScrollBody;
 
   @override
   void performLayout() {
-    final double extent = constraints.remainingPaintExtent - math.min(constraints.overlap, 0.0);
+    final double extent = constraints.remainingPaintExtent
+      - math.min(constraints.overlap, 0.0)
+      // Adding the offset for when this SliverFillRemaining is not scrollable,
+      // so it will stretch to fill on overscroll.
+      + (hasScrollBody ? 0.0 : constraints.scrollOffset);
     if (child != null)
       child.layout(constraints.asBoxConstraints(minExtent: extent, maxExtent: extent), parentUsesSize: true);
     final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: extent);
     assert(paintedChildSize.isFinite);
     assert(paintedChildSize >= 0.0);
     geometry = SliverGeometry(
-      scrollExtent: constraints.viewportMainAxisExtent,
+      // 0.0 can be applied here for cases when there is not scroll body since
+      // SliverFillRemaining will not have any slivers following it.
+      scrollExtent: hasScrollBody ? constraints.viewportMainAxisExtent : 0.0,
       paintExtent: paintedChildSize,
       maxPaintExtent: paintedChildSize,
       hasVisualOverflow: extent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart
index 53a28a6..0e0fb86 100644
--- a/packages/flutter/lib/src/widgets/sliver.dart
+++ b/packages/flutter/lib/src/widgets/sliver.dart
@@ -1368,10 +1368,24 @@
   const SliverFillRemaining({
     Key key,
     Widget child,
-  }) : super(key: key, child: child);
+    this.hasScrollBody = true,
+  }) : assert(hasScrollBody != null),
+       super(key: key, child: child);
+
+  /// Whether the child has a scrollable body, this value cannot be null.
+  ///
+  /// Defaults to true such that the child will extend beyond the viewport and
+  /// scroll, as seen in [NestedScrollView].
+  ///
+  /// Setting this value to false will allow the child to fill the remainder of
+  /// the viewport and not extend further.
+  final bool hasScrollBody;
 
   @override
-  RenderSliverFillRemaining createRenderObject(BuildContext context) => RenderSliverFillRemaining();
+  RenderSliverFillRemaining createRenderObject(BuildContext context) => RenderSliverFillRemaining(hasScrollBody: hasScrollBody);
+
+  @override
+  void updateRenderObject(BuildContext context, RenderSliverFillRemaining renderObject) => renderObject.hasScrollBody = hasScrollBody;
 }
 
 /// Mark a child as needing to stay alive even when it's in a lazy list that
diff --git a/packages/flutter/test/widgets/sliver_fill_remaining_test.dart b/packages/flutter/test/widgets/sliver_fill_remaining_test.dart
index ff2090b..1b58378 100644
--- a/packages/flutter/test/widgets/sliver_fill_remaining_test.dart
+++ b/packages/flutter/test/widgets/sliver_fill_remaining_test.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 
 void main() {
@@ -62,4 +63,63 @@
     await tester.pump();
     expect(tester.renderObject<RenderBox>(find.byType(Container)).size.height, equals(500.0));
   });
+
+  testWidgets('SliverFillRemaining does not extend past viewport.', (WidgetTester tester) async {
+    final ScrollController controller = ScrollController();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: CustomScrollView(
+          controller: controller,
+          slivers: <Widget>[
+            SliverToBoxAdapter(
+              child: Container(
+                color: Colors.red,
+                height: 150.0,
+              ),
+            ),
+            SliverFillRemaining(
+              child: Container(color: Colors.white),
+              hasScrollBody: false,
+            ),
+          ],
+        ),
+      ),
+    );
+    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('SliverFillRemaining scrolls beyond viewport by default.', (WidgetTester tester) async {
+    final ScrollController controller = ScrollController();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: CustomScrollView(
+          controller: controller,
+          slivers: <Widget>[
+            SliverToBoxAdapter(
+              child: Container(
+                color: Colors.red,
+                height: 150.0,
+              ),
+            ),
+            SliverFillRemaining(
+              child: Container(color: Colors.white),
+            ),
+          ],
+        ),
+      ),
+    );
+    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);
+  });
 }
diff --git a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart
index e71db18..e5b60dd 100644
--- a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart
+++ b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart
@@ -6,7 +6,7 @@
 import 'package:flutter/widgets.dart';
 
 void main() {
-  testWidgets('SliverFillRemaining control test', (WidgetTester tester) async {
+  testWidgets('SliverFillViewport control test', (WidgetTester tester) async {
     final List<Widget> children = List<Widget>.generate(20, (int i) {
       return Container(child: Text('$i', textDirection: TextDirection.ltr));
     });