diff --git a/packages/flutter/lib/src/rendering/sliver_fill.dart b/packages/flutter/lib/src/rendering/sliver_fill.dart index ab3ee26e14..3f3e28267e 100644 --- a/packages/flutter/lib/src/rendering/sliver_fill.dart +++ b/packages/flutter/lib/src/rendering/sliver_fill.dart @@ -59,69 +59,90 @@ class RenderSliverFillViewport extends RenderSliverFixedExtentBoxAdaptor { } } -/// A sliver that contains a single box child that fills the remaining space in -/// the viewport. +/// A sliver that contains a single box child that contains a scrollable and +/// fills the viewport. /// -/// [RenderSliverFillRemaining] sizes its child to fill the viewport in the -/// cross axis and to fill the remaining space in the viewport in the main axis. +/// [RenderSliverFillRemainingWithScrollable] sizes its child to fill the +/// viewport in the cross axis and to fill the remaining space in the viewport +/// in the main axis. /// /// Typically this will be the last sliver in a viewport, since (by definition) /// there is never any room for anything beyond this sliver. /// /// See also: /// +/// * [NestedScrollView], which uses this sliver for the inner scrollable. +/// * [RenderSliverFillRemaining], which lays out its +/// non-scrollable child slightly different than this widget. +/// * [RenderSliverFillRemainingAndOverscroll], which incorporates the +/// overscroll into the remaining space to fill. +/// * [RenderSliverFillViewport], which sizes its children based on the +/// size of the viewport, regardless of what else is in the scroll view. +/// * [RenderSliverList], which shows a list of variable-sized children in a +/// viewport. +class RenderSliverFillRemainingWithScrollable extends RenderSliverSingleBoxAdapter { + /// Creates a [RenderSliver] that wraps a scrollable [RenderBox] which is + /// sized to fit the remaining space in the viewport. + RenderSliverFillRemainingWithScrollable({ RenderBox child }) : super(child: child); + + @override + void performLayout() { + // TODO(Piinks): This may fill too much space for NestedScrollView, https://github.com/flutter/flutter/issues/46028 + final double extent = constraints.remainingPaintExtent - math.min(constraints.overlap, 0.0); + + if (child != null) + child.layout(constraints.asBoxConstraints( + minExtent: extent, + maxExtent: extent, + )); + + final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: extent); + assert(paintedChildSize.isFinite); + assert(paintedChildSize >= 0.0); + geometry = SliverGeometry( + scrollExtent: constraints.viewportMainAxisExtent, + paintExtent: paintedChildSize, + maxPaintExtent: paintedChildSize, + hasVisualOverflow: extent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0, + ); + if (child != null) + setChildParentData(child, constraints, geometry); + } +} + +/// A sliver that contains a single box child that is non-scrollable and fills +/// the remaining space in the viewport. +/// +/// [RenderSliverFillRemaining] sizes its child to fill the +/// viewport in the cross axis and to fill the remaining space in the viewport +/// in the main axis. +/// +/// Typically this will be the last sliver in a viewport, since (by definition) +/// there is never any room for anything beyond this sliver. +/// +/// See also: +/// +/// * [RenderSliverFillRemainingWithScrollable], which lays out its scrollable +/// child slightly different than this widget. +/// * [RenderSliverFillRemainingAndOverscroll], which incorporates the +/// overscroll into the remaining space to fill. /// * [RenderSliverFillViewport], which sizes its children based on the /// size of the viewport, regardless of what else is in the scroll view. /// * [RenderSliverList], which shows a list of variable-sized children in a /// viewport. class RenderSliverFillRemaining extends RenderSliverSingleBoxAdapter { - /// Creates a [RenderSliver] that wraps a [RenderBox] which is sized to fit - /// the remaining space in the viewport. - RenderSliverFillRemaining({ - RenderBox child, - this.hasScrollBody = true, - this.fillOverscroll = false, - }) : assert(hasScrollBody != null), - super(child: child); - - /// Indicates 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. However, if the - /// [precedingScrollExtent] exceeds the size of the viewport, the sliver will - /// defer to the child's size rather than overriding it. - bool hasScrollBody; - - /// Indicates whether the child should stretch to fill the overscroll area - /// created by certain scroll physics, such as iOS' default scroll physics. - /// This value cannot be null. This flag is only relevant when the - /// [hasScrollBody] value is false. - /// - /// Defaults to false, meaning the default behavior is for the child to - /// maintain its size and not extend into the overscroll area. - bool fillOverscroll; + /// Creates a [RenderSliver] that wraps a non-scrollable [RenderBox] which is + /// sized to fit the remaining space in the viewport. + RenderSliverFillRemaining({ RenderBox child }) : super(child: child); @override void performLayout() { - double childExtent; + // The remaining space in the viewportMainAxisExtent. Can be <= 0 if we have + // scrolled beyond the extent of the screen. double extent = constraints.viewportMainAxisExtent - constraints.precedingScrollExtent; - double maxExtent = constraints.remainingPaintExtent - math.min(constraints.overlap, 0.0); - if (hasScrollBody) { - extent = maxExtent; - if (child != null) - child.layout( - constraints.asBoxConstraints( - minExtent: extent, - maxExtent: extent, - ), - parentUsesSize: true, - ); - } else if (child != null) { + if (child != null) { + double childExtent; switch (constraints.axis) { case Axis.horizontal: childExtent = child.getMaxIntrinsicWidth(constraints.crossAxisExtent); @@ -131,34 +152,27 @@ class RenderSliverFillRemaining extends RenderSliverSingleBoxAdapter { break; } - if (constraints.precedingScrollExtent > constraints.viewportMainAxisExtent || childExtent > extent) - extent = childExtent; - if (maxExtent < extent) - maxExtent = extent; - if ((fillOverscroll ? maxExtent : extent) > childExtent) { - child.layout( - constraints.asBoxConstraints( - minExtent: extent, - maxExtent: fillOverscroll ? maxExtent : extent, - ), - parentUsesSize: true, - ); - } else { - child.layout(constraints.asBoxConstraints(), parentUsesSize: true); - } + // If the childExtent is greater than the computed extent, we want to use + // that instead of potentially cutting off the child. This allows us to + // safely specify a maxExtent. + extent = math.max(extent, childExtent); + child.layout(constraints.asBoxConstraints( + minExtent: extent, + maxExtent: extent, + )); } assert(extent.isFinite, 'The calculated extent for the child of SliverFillRemaining is not finite.' - 'This can happen if the child is a scrollable, in which case, the' - 'hasScrollBody property of SliverFillRemaining should not be set to' - 'false.', + 'This can happen if the child is a scrollable, in which case, the' + 'hasScrollBody property of SliverFillRemaining should not be set to' + 'false.', ); final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: extent); assert(paintedChildSize.isFinite); assert(paintedChildSize >= 0.0); geometry = SliverGeometry( - scrollExtent: hasScrollBody ? constraints.viewportMainAxisExtent : extent, + scrollExtent: extent, paintExtent: paintedChildSize, maxPaintExtent: paintedChildSize, hasVisualOverflow: extent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0, @@ -167,3 +181,79 @@ class RenderSliverFillRemaining extends RenderSliverSingleBoxAdapter { setChildParentData(child, constraints, geometry); } } + +/// A sliver that contains a single box child that is non-scrollable and fills +/// the remaining space in the viewport including any overscrolled area. +/// +/// [RenderSliverFillRemainingAndOverscroll] sizes its child to fill the +/// viewport in the cross axis and to fill the remaining space in the viewport +/// in the main axis with the overscroll area included. +/// +/// Typically this will be the last sliver in a viewport, since (by definition) +/// there is never any room for anything beyond this sliver. +/// +/// See also: +/// +/// * [RenderSliverFillRemainingWithScrollable], which lays out its scrollable +/// child without overscroll. +/// * [RenderSliverFillRemaining], which lays out its +/// non-scrollable child without overscroll. +/// * [RenderSliverFillViewport], which sizes its children based on the +/// size of the viewport, regardless of what else is in the scroll view. +/// * [RenderSliverList], which shows a list of variable-sized children in a +/// viewport. +class RenderSliverFillRemainingAndOverscroll extends RenderSliverSingleBoxAdapter { + /// Creates a [RenderSliver] that wraps a non-scrollable [RenderBox] which is + /// sized to fit the remaining space plus any overscroll in the viewport. + RenderSliverFillRemainingAndOverscroll({ RenderBox child }) : super(child: child); + + @override + void performLayout() { + // The remaining space in the viewportMainAxisExtent. Can be <= 0 if we have + // scrolled beyond the extent of the screen. + double extent = constraints.viewportMainAxisExtent - constraints.precedingScrollExtent; + // The maxExtent includes any overscrolled area. Can be < 0 if we have + // overscroll in the opposite direction, away from the end of the list. + double maxExtent = constraints.remainingPaintExtent - math.min(constraints.overlap, 0.0); + + if (child != null) { + double childExtent; + switch (constraints.axis) { + case Axis.horizontal: + childExtent = child.getMaxIntrinsicWidth(constraints.crossAxisExtent); + break; + case Axis.vertical: + childExtent = child.getMaxIntrinsicHeight(constraints.crossAxisExtent); + break; + } + + // If the childExtent is greater than the computed extent, we want to use + // that instead of potentially cutting off the child. This allows us to + // safely specify a maxExtent. + extent = math.max(extent, childExtent); + // The extent could be larger than the maxExtent due to a larger child + // size or overscrolling at the top of the scrollable (rather than at the + // end where this sliver is). + maxExtent = math.max(extent, maxExtent); + child.layout(constraints.asBoxConstraints(minExtent: extent, maxExtent: maxExtent)); + } + + assert(extent.isFinite, + 'The calculated extent for the child of SliverFillRemaining is not finite.' + 'This can happen if the child is a scrollable, in which case, the' + 'hasScrollBody property of SliverFillRemaining should not be set to' + 'false.', + ); + final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: extent); + assert(paintedChildSize.isFinite); + assert(paintedChildSize >= 0.0); + geometry = SliverGeometry( + scrollExtent: extent, + paintExtent: math.min(maxExtent, constraints.remainingPaintExtent), + maxPaintExtent: maxExtent, + hasVisualOverflow: extent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0, + ); + if (child != null) + setChildParentData(child, constraints, geometry); + } +} diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index f7397dfa52..529856a449 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -22,7 +22,7 @@ import 'scroll_metrics.dart'; import 'scroll_physics.dart'; import 'scroll_position.dart'; import 'scroll_view.dart'; -import 'sliver.dart'; +import 'sliver_fill.dart'; import 'ticker_provider.dart'; import 'viewport.dart'; diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 058fec7f54..28548eacbe 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -25,6 +25,7 @@ import 'scroll_position_with_single_context.dart'; import 'scroll_view.dart'; import 'scrollable.dart'; import 'sliver.dart'; +import 'sliver_fill.dart'; import 'viewport.dart'; /// A controller for [PageView]. diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index 0a0a7cde17..70fa5d9cf9 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -1025,148 +1025,6 @@ class SliverGrid extends SliverMultiBoxAdaptorWidget { } } -/// A sliver that contains multiple box children that each fills the viewport. -/// -/// [SliverFillViewport] places its children in a linear array along the main -/// axis. Each child is sized to fill the viewport, both in the main and cross -/// axis. -/// -/// See also: -/// -/// * [SliverFixedExtentList], which has a configurable -/// [SliverFixedExtentList.itemExtent]. -/// * [SliverPrototypeExtentList], which is similar to [SliverFixedExtentList] -/// except that it uses a prototype list item instead of a pixel value to define -/// the main axis extent of each item. -/// * [SliverList], which does not require its children to have the same -/// extent in the main axis. -class SliverFillViewport extends StatelessWidget { - /// Creates a sliver whose box children that each fill the viewport. - const SliverFillViewport({ - Key key, - @required this.delegate, - this.viewportFraction = 1.0, - }) : assert(viewportFraction != null), - assert(viewportFraction > 0.0), - super(key: key); - - /// The fraction of the viewport that each child should fill in the main axis. - /// - /// If this fraction is less than 1.0, more than one child will be visible at - /// once. If this fraction is greater than 1.0, each child will be larger than - /// the viewport in the main axis. - final double viewportFraction; - - /// {@macro flutter.widgets.sliverMultiBoxAdaptor.delegate} - final SliverChildDelegate delegate; - - @override - Widget build(BuildContext context) { - return _SliverFractionalPadding( - viewportFraction: (1 - viewportFraction).clamp(0, 1) / 2, - sliver: _SliverFillViewportRenderObjectWidget( - viewportFraction: viewportFraction, - delegate: delegate, - ), - ); - } -} - -class _SliverFillViewportRenderObjectWidget extends SliverMultiBoxAdaptorWidget { - const _SliverFillViewportRenderObjectWidget({ - Key key, - @required SliverChildDelegate delegate, - this.viewportFraction = 1.0, - }) : assert(viewportFraction != null), - assert(viewportFraction > 0.0), - super(key: key, delegate: delegate); - - final double viewportFraction; - - @override - RenderSliverFillViewport createRenderObject(BuildContext context) { - final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement; - return RenderSliverFillViewport(childManager: element, viewportFraction: viewportFraction); - } - - @override - void updateRenderObject(BuildContext context, RenderSliverFillViewport renderObject) { - renderObject.viewportFraction = viewportFraction; - } -} - -class _SliverFractionalPadding extends SingleChildRenderObjectWidget { - const _SliverFractionalPadding({ - Key key, - this.viewportFraction = 0, - Widget sliver, - }) : assert(viewportFraction != null), - assert(viewportFraction >= 0), - assert(viewportFraction <= 0.5), - super(key: key, child: sliver); - - final double viewportFraction; - - @override - RenderObject createRenderObject(BuildContext context) => _RenderSliverFractionalPadding(viewportFraction: viewportFraction); - - @override - void updateRenderObject(BuildContext context, _RenderSliverFractionalPadding renderObject) { - renderObject.viewportFraction = viewportFraction; - } -} - -class _RenderSliverFractionalPadding extends RenderSliverEdgeInsetsPadding { - _RenderSliverFractionalPadding({ - double viewportFraction = 0, - }) : assert(viewportFraction != null), - assert(viewportFraction <= 0.5), - assert(viewportFraction >= 0), - _viewportFraction = viewportFraction; - - double get viewportFraction => _viewportFraction; - double _viewportFraction; - set viewportFraction(double newValue) { - assert(newValue != null); - if (_viewportFraction == newValue) - return; - _viewportFraction = newValue; - _markNeedsResolution(); - } - - @override - EdgeInsets get resolvedPadding => _resolvedPadding; - EdgeInsets _resolvedPadding; - - void _markNeedsResolution() { - _resolvedPadding = null; - markNeedsLayout(); - } - - void _resolve() { - if (_resolvedPadding != null) - return; - assert(constraints.axis != null); - final double paddingValue = constraints.viewportMainAxisExtent * viewportFraction; - switch (constraints.axis) { - case Axis.horizontal: - _resolvedPadding = EdgeInsets.symmetric(horizontal: paddingValue); - break; - case Axis.vertical: - _resolvedPadding = EdgeInsets.symmetric(vertical: paddingValue); - break; - } - - return; - } - - @override - void performLayout() { - _resolve(); - super.performLayout(); - } -} - /// An element that lazily builds children for a [SliverMultiBoxAdaptorWidget]. /// /// Implements [RenderSliverBoxChildManager], which lets this element manage @@ -1456,275 +1314,6 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render } } -/// A sliver that contains a single box child that fills the remaining space in -/// the viewport. -/// -/// [SliverFillRemaining] will size its [child] to fill the viewport in the -/// cross axis. The extent of the sliver and its child's size in the main axis -/// is computed conditionally, described in further detail below. -/// -/// Typically this will be the last sliver in a viewport, since (by definition) -/// there is never any room for anything beyond this sliver. -/// -/// ## Main Axis Extent -/// -/// ### When [SliverFillRemaining] has a scrollable child -/// -/// The [hasScrollBody] flag indicates whether the sliver's child has a -/// scrollable body. This value is never null, and defaults to true. A common -/// example of this use is a [NestedScrollView]. In this case, the sliver will -/// size its child to fill the maximum available extent. -/// -/// ### When [SliverFillRemaining] does not have a scrollable child -/// -/// When [hasScrollBody] is set to false, the child's size is taken into account -/// when considering the extent to which it should fill the space. The -/// [precedingScrollExtent] of the [SliverConstraints] is also taken into -/// account in deciding how to layout the sliver. -/// -/// * [SliverFillRemaining] will size its [child] to fill the viewport in the -/// main axis if that space is larger than the child's extent, and the -/// [precedingScrollExtent] has not exceeded the main axis extent of the -/// viewport. -/// -/// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_sizes_child.mp4} -/// -/// {@tool snippet --template=stateless_widget_scaffold} -/// -/// In this sample the [SliverFillRemaining] sizes its [child] to fill the -/// remaining extent of the viewport in both axes. The icon is centered in the -/// sliver, and would be in any computed extent for the sliver. -/// -/// ```dart -/// Widget build(BuildContext context) { -/// return CustomScrollView( -/// slivers: [ -/// SliverToBoxAdapter( -/// child: Container( -/// color: Colors.amber[300], -/// height: 150.0, -/// ), -/// ), -/// SliverFillRemaining( -/// hasScrollBody: false, -/// child: Container( -/// color: Colors.blue[100], -/// child: Icon( -/// Icons.sentiment_very_satisfied, -/// size: 75, -/// color: Colors.blue[900], -/// ), -/// ), -/// ), -/// ], -/// ); -/// } -/// ``` -/// {@end-tool} -/// -/// * [SliverFillRemaining] will defer to the size of its [child] if the -/// child's size exceeds the remaining space in the viewport. -/// -/// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_defers_to_child.mp4} -/// -/// {@tool snippet --template=stateless_widget_scaffold} -/// -/// In this sample the [SliverFillRemaining] defers to the size of its [child] -/// because the child's extent exceeds that of the remaining extent of the -/// viewport's main axis. -/// -/// ```dart -/// Widget build(BuildContext context) { -/// return CustomScrollView( -/// slivers: [ -/// SliverFixedExtentList( -/// itemExtent: 100.0, -/// delegate: SliverChildBuilderDelegate( -/// (BuildContext context, int index) { -/// return Container( -/// color: index % 2 == 0 -/// ? Colors.amber[200] -/// : Colors.blue[200], -/// ); -/// }, -/// childCount: 3, -/// ), -/// ), -/// SliverFillRemaining( -/// hasScrollBody: false, -/// child: Container( -/// color: Colors.orange[300], -/// child: Padding( -/// padding: const EdgeInsets.all(50.0), -/// child: FlutterLogo(size: 100), -/// ), -/// ), -/// ), -/// ], -/// ); -/// } -/// ``` -/// {@end-tool} -/// -/// * [SliverFillRemaining] will defer to the size of its [child] if the -/// [precedingScrollExtent] exceeded the length of the viewport's main axis. -/// -/// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_scrolled_beyond.mp4} -/// -/// {@tool snippet --template=stateless_widget_scaffold} -/// -/// In this sample the [SliverFillRemaining] defers to the size of its [child] -/// because the [precedingScrollExtent] of the [SliverConstraints] has gone -/// beyond that of the viewport's main axis. -/// -/// ```dart -/// Widget build(BuildContext context) { -/// return CustomScrollView( -/// slivers: [ -/// SliverFixedExtentList( -/// itemExtent: 130.0, -/// delegate: SliverChildBuilderDelegate( -/// (BuildContext context, int index) { -/// return Container( -/// color: index % 2 == 0 -/// ? Colors.indigo[200] -/// : Colors.orange[200], -/// ); -/// }, -/// childCount: 5, -/// ), -/// ), -/// SliverFillRemaining( -/// hasScrollBody: false, -/// child: Container( -/// child: Padding( -/// padding: const EdgeInsets.all(50.0), -/// child: Icon( -/// Icons.pan_tool, -/// size: 60, -/// color: Colors.blueGrey, -/// ), -/// ), -/// ), -/// ), -/// ], -/// ); -/// } -/// ``` -/// {@end-tool} -/// -/// * For [ScrollPhysics] that allow overscroll, such as -/// [BouncingScrollPhysics], setting the [fillOverscroll] flag to true allows -/// the size of the [child] to _stretch_, filling the overscroll area. It does -/// this regardless of the path chosen to provide the child's size. -/// -/// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_fill_overscroll.mp4} -/// -/// {@tool snippet --template=stateless_widget_scaffold} -/// -/// In this sample the [SliverFillRemaining]'s child stretches to fill the -/// overscroll area when [fillOverscroll] is true. This sample also features a -/// button that is pinned to the bottom of the sliver, regardless of size or -/// overscroll behavior. Try switching [fillOverscroll] to see the difference. -/// -/// ```dart -/// Widget build(BuildContext context) { -/// return CustomScrollView( -/// // The ScrollPhysics are overridden here to illustrate the functionality -/// // of fillOverscroll on all devices this sample may be run on. -/// // fillOverscroll only changes the behavior of your layout when applied -/// // to Scrollables that allow for overscroll. BouncingScrollPhysics are -/// // one example, which are provided by default on the iOS platform. -/// physics: BouncingScrollPhysics(), -/// slivers: [ -/// SliverToBoxAdapter( -/// child: Container( -/// color: Colors.tealAccent[700], -/// height: 150.0, -/// ), -/// ), -/// SliverFillRemaining( -/// hasScrollBody: false, -/// // Switch for different overscroll behavior in your layout. -/// // If your ScrollPhysics do not allow for overscroll, setting -/// // fillOverscroll to true will have no effect. -/// fillOverscroll: true, -/// child: Container( -/// color: Colors.teal[100], -/// child: Align( -/// alignment: Alignment.bottomCenter, -/// child: Padding( -/// padding: const EdgeInsets.all(16.0), -/// child: RaisedButton( -/// onPressed: () { -/// /* Place your onPressed code here! */ -/// }, -/// child: Text('Bottom Pinned Button!'), -/// ), -/// ), -/// ), -/// ), -/// ), -/// ], -/// ); -/// } -/// ``` -/// {@end-tool} -/// -/// -/// See also: -/// -/// * [SliverFillViewport], which sizes its children based on the -/// size of the viewport, regardless of what else is in the scroll view. -/// * [SliverList], which shows a list of variable-sized children in a -/// viewport. -class SliverFillRemaining extends SingleChildRenderObjectWidget { - /// Creates a sliver that fills the remaining space in the viewport. - const SliverFillRemaining({ - Key key, - Widget child, - this.hasScrollBody = true, - this.fillOverscroll = false, - }) : assert(hasScrollBody != null), - super(key: key, child: child); - - /// Indicates 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. However, if the - /// [precedingScrollExtent] of the [SliverContraints] and/or the [child]'s - /// extent exceeds the size of the viewport, the sliver will defer to the - /// child's size rather than overriding it. - final bool hasScrollBody; - - /// Indicates whether the child should stretch to fill the overscroll area - /// created by certain scroll physics, such as iOS' default scroll physics. - /// This value cannot be null. This flag is only relevant when the - /// [hasScrollBody] value is false. - /// - /// Defaults to false, meaning the default behavior is for the child to - /// maintain its size and not extend into the overscroll area. - final bool fillOverscroll; - - @override - RenderSliverFillRemaining createRenderObject(BuildContext context) { - return RenderSliverFillRemaining( - hasScrollBody: hasScrollBody, - fillOverscroll: fillOverscroll, - ); - } - - @override - void updateRenderObject(BuildContext context, RenderSliverFillRemaining renderObject) { - renderObject.hasScrollBody = hasScrollBody; - renderObject.fillOverscroll = fillOverscroll; - } -} - /// A sliver widget that makes its sliver child partially transparent. /// /// This class paints its sliver child into an intermediate buffer and then diff --git a/packages/flutter/lib/src/widgets/sliver_fill.dart b/packages/flutter/lib/src/widgets/sliver_fill.dart new file mode 100644 index 0000000000..27d3c965af --- /dev/null +++ b/packages/flutter/lib/src/widgets/sliver_fill.dart @@ -0,0 +1,467 @@ +// 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 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'basic.dart'; +import 'framework.dart'; +import 'sliver.dart'; + +/// A sliver that contains multiple box children that each fills the viewport. +/// +/// [SliverFillViewport] places its children in a linear array along the main +/// axis. Each child is sized to fill the viewport, both in the main and cross +/// axis. +/// +/// See also: +/// +/// * [SliverFixedExtentList], which has a configurable +/// [SliverFixedExtentList.itemExtent]. +/// * [SliverPrototypeExtentList], which is similar to [SliverFixedExtentList] +/// except that it uses a prototype list item instead of a pixel value to define +/// the main axis extent of each item. +/// * [SliverList], which does not require its children to have the same +/// extent in the main axis. +class SliverFillViewport extends StatelessWidget { + /// Creates a sliver whose box children that each fill the viewport. + const SliverFillViewport({ + Key key, + @required this.delegate, + this.viewportFraction = 1.0, + }) : assert(viewportFraction != null), + assert(viewportFraction > 0.0), + super(key: key); + + /// The fraction of the viewport that each child should fill in the main axis. + /// + /// If this fraction is less than 1.0, more than one child will be visible at + /// once. If this fraction is greater than 1.0, each child will be larger than + /// the viewport in the main axis. + final double viewportFraction; + + /// {@macro flutter.widgets.sliverMultiBoxAdaptor.delegate} + final SliverChildDelegate delegate; + + @override + Widget build(BuildContext context) { + return _SliverFractionalPadding( + viewportFraction: (1 - viewportFraction).clamp(0, 1) / 2, + sliver: _SliverFillViewportRenderObjectWidget( + viewportFraction: viewportFraction, + delegate: delegate, + ), + ); + } +} + +class _SliverFillViewportRenderObjectWidget extends SliverMultiBoxAdaptorWidget { + const _SliverFillViewportRenderObjectWidget({ + Key key, + @required SliverChildDelegate delegate, + this.viewportFraction = 1.0, + }) : assert(viewportFraction != null), + assert(viewportFraction > 0.0), + super(key: key, delegate: delegate); + + final double viewportFraction; + + @override + RenderSliverFillViewport createRenderObject(BuildContext context) { + final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement; + return RenderSliverFillViewport(childManager: element, viewportFraction: viewportFraction); + } + + @override + void updateRenderObject(BuildContext context, RenderSliverFillViewport renderObject) { + renderObject.viewportFraction = viewportFraction; + } +} + +class _SliverFractionalPadding extends SingleChildRenderObjectWidget { + const _SliverFractionalPadding({ + this.viewportFraction = 0, + Widget sliver, + }) : assert(viewportFraction != null), + assert(viewportFraction >= 0), + assert(viewportFraction <= 0.5), + super(child: sliver); + + final double viewportFraction; + + @override + RenderObject createRenderObject(BuildContext context) => _RenderSliverFractionalPadding(viewportFraction: viewportFraction); + + @override + void updateRenderObject(BuildContext context, _RenderSliverFractionalPadding renderObject) { + renderObject.viewportFraction = viewportFraction; + } +} + +class _RenderSliverFractionalPadding extends RenderSliverEdgeInsetsPadding { + _RenderSliverFractionalPadding({ + double viewportFraction = 0, + }) : assert(viewportFraction != null), + assert(viewportFraction <= 0.5), + assert(viewportFraction >= 0), + _viewportFraction = viewportFraction; + + double get viewportFraction => _viewportFraction; + double _viewportFraction; + set viewportFraction(double newValue) { + assert(newValue != null); + if (_viewportFraction == newValue) + return; + _viewportFraction = newValue; + _markNeedsResolution(); + } + + @override + EdgeInsets get resolvedPadding => _resolvedPadding; + EdgeInsets _resolvedPadding; + + void _markNeedsResolution() { + _resolvedPadding = null; + markNeedsLayout(); + } + + void _resolve() { + if (_resolvedPadding != null) + return; + assert(constraints.axis != null); + final double paddingValue = constraints.viewportMainAxisExtent * viewportFraction; + switch (constraints.axis) { + case Axis.horizontal: + _resolvedPadding = EdgeInsets.symmetric(horizontal: paddingValue); + break; + case Axis.vertical: + _resolvedPadding = EdgeInsets.symmetric(vertical: paddingValue); + break; + } + + return; + } + + @override + void performLayout() { + _resolve(); + super.performLayout(); + } +} + +/// A sliver that contains a single box child that fills the remaining space in +/// the viewport. +/// +/// [SliverFillRemaining] will size its [child] to fill the viewport in the +/// cross axis. The extent of the sliver and its child's size in the main axis +/// is computed conditionally, described in further detail below. +/// +/// Typically this will be the last sliver in a viewport, since (by definition) +/// there is never any room for anything beyond this sliver. +/// +/// ## Main Axis Extent +/// +/// ### When [SliverFillRemaining] has a scrollable child +/// +/// The [hasScrollBody] flag indicates whether the sliver's child has a +/// scrollable body. This value is never null, and defaults to true. A common +/// example of this use is a [NestedScrollView]. In this case, the sliver will +/// size its child to fill the maximum available extent. +/// +/// ### When [SliverFillRemaining] does not have a scrollable child +/// +/// When [hasScrollBody] is set to false, the child's size is taken into account +/// when considering the extent to which it should fill the space. The extent to +/// which the preceding slivers have been scrolled is also taken into +/// account in deciding how to layout this sliver. +/// +/// [SliverFillRemaining] will size its [child] to fill the viewport in the +/// main axis if that space is larger than the child's extent, and the +/// the amount of space that has been scrolled beforehand has not exceeded the +/// main axis extent of the viewport. +/// +/// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_sizes_child.mp4} +/// +/// {@tool snippet --template=stateless_widget_scaffold} +/// +/// In this sample the [SliverFillRemaining] sizes its [child] to fill the +/// remaining extent of the viewport in both axes. The icon is centered in the +/// sliver, and would be in any computed extent for the sliver. +/// +/// ```dart +/// Widget build(BuildContext context) { +/// return CustomScrollView( +/// slivers: [ +/// SliverToBoxAdapter( +/// child: Container( +/// color: Colors.amber[300], +/// height: 150.0, +/// ), +/// ), +/// SliverFillRemaining( +/// hasScrollBody: false, +/// child: Container( +/// color: Colors.blue[100], +/// child: Icon( +/// Icons.sentiment_very_satisfied, +/// size: 75, +/// color: Colors.blue[900], +/// ), +/// ), +/// ), +/// ], +/// ); +/// } +/// ``` +/// {@end-tool} +/// +/// [SliverFillRemaining] will defer to the size of its [child] if the +/// child's size exceeds the remaining space in the viewport. +/// +/// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_defers_to_child.mp4} +/// +/// {@tool snippet --template=stateless_widget_scaffold} +/// +/// In this sample the [SliverFillRemaining] defers to the size of its [child] +/// because the child's extent exceeds that of the remaining extent of the +/// viewport's main axis. +/// +/// ```dart +/// Widget build(BuildContext context) { +/// return CustomScrollView( +/// slivers: [ +/// SliverFixedExtentList( +/// itemExtent: 100.0, +/// delegate: SliverChildBuilderDelegate( +/// (BuildContext context, int index) { +/// return Container( +/// color: index % 2 == 0 +/// ? Colors.amber[200] +/// : Colors.blue[200], +/// ); +/// }, +/// childCount: 3, +/// ), +/// ), +/// SliverFillRemaining( +/// hasScrollBody: false, +/// child: Container( +/// color: Colors.orange[300], +/// child: Padding( +/// padding: const EdgeInsets.all(50.0), +/// child: FlutterLogo(size: 100), +/// ), +/// ), +/// ), +/// ], +/// ); +/// } +/// ``` +/// {@end-tool} +/// +/// [SliverFillRemaining] will defer to the size of its [child] if the +/// [precedingScrollExtent] exceeded the length of the viewport's main axis. +/// +/// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_scrolled_beyond.mp4} +/// +/// {@tool snippet --template=stateless_widget_scaffold} +/// +/// In this sample the [SliverFillRemaining] defers to the size of its [child] +/// because the [precedingScrollExtent] of the [SliverConstraints] has gone +/// beyond that of the viewport's main axis. +/// +/// ```dart +/// Widget build(BuildContext context) { +/// return CustomScrollView( +/// slivers: [ +/// SliverFixedExtentList( +/// itemExtent: 130.0, +/// delegate: SliverChildBuilderDelegate( +/// (BuildContext context, int index) { +/// return Container( +/// color: index % 2 == 0 +/// ? Colors.indigo[200] +/// : Colors.orange[200], +/// ); +/// }, +/// childCount: 5, +/// ), +/// ), +/// SliverFillRemaining( +/// hasScrollBody: false, +/// child: Container( +/// child: Padding( +/// padding: const EdgeInsets.all(50.0), +/// child: Icon( +/// Icons.pan_tool, +/// size: 60, +/// color: Colors.blueGrey, +/// ), +/// ), +/// ), +/// ), +/// ], +/// ); +/// } +/// ``` +/// {@end-tool} +/// +/// For [ScrollPhysics] that allow overscroll, such as +/// [BouncingScrollPhysics], setting the [fillOverscroll] flag to true allows +/// the size of the [child] to _stretch_, filling the overscroll area. It does +/// this regardless of the path chosen to provide the child's size. +/// +/// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_fill_overscroll.mp4} +/// +/// {@tool snippet --template=stateless_widget_scaffold} +/// +/// In this sample the [SliverFillRemaining]'s child stretches to fill the +/// overscroll area when [fillOverscroll] is true. This sample also features a +/// button that is pinned to the bottom of the sliver, regardless of size or +/// overscroll behavior. Try switching [fillOverscroll] to see the difference. +/// +/// ```dart +/// Widget build(BuildContext context) { +/// return CustomScrollView( +/// // The ScrollPhysics are overridden here to illustrate the functionality +/// // of fillOverscroll on all devices this sample may be run on. +/// // fillOverscroll only changes the behavior of your layout when applied +/// // to Scrollables that allow for overscroll. BouncingScrollPhysics are +/// // one example, which are provided by default on the iOS platform. +/// physics: BouncingScrollPhysics(), +/// slivers: [ +/// SliverToBoxAdapter( +/// child: Container( +/// color: Colors.tealAccent[700], +/// height: 150.0, +/// ), +/// ), +/// SliverFillRemaining( +/// hasScrollBody: false, +/// // Switch for different overscroll behavior in your layout. +/// // If your ScrollPhysics do not allow for overscroll, setting +/// // fillOverscroll to true will have no effect. +/// fillOverscroll: true, +/// child: Container( +/// color: Colors.teal[100], +/// child: Align( +/// alignment: Alignment.bottomCenter, +/// child: Padding( +/// padding: const EdgeInsets.all(16.0), +/// child: RaisedButton( +/// onPressed: () { +/// /* Place your onPressed code here! */ +/// }, +/// child: Text('Bottom Pinned Button!'), +/// ), +/// ), +/// ), +/// ), +/// ), +/// ], +/// ); +/// } +/// ``` +/// {@end-tool} +/// +/// +/// See also: +/// +/// * [SliverFillViewport], which sizes its children based on the +/// size of the viewport, regardless of what else is in the scroll view. +/// * [SliverList], which shows a list of variable-sized children in a +/// viewport. +class SliverFillRemaining extends StatelessWidget { + /// Creates a sliver that fills the remaining space in the viewport. + const SliverFillRemaining({ + Key key, + this.child, + this.hasScrollBody = true, + this.fillOverscroll = false, + }) : assert(hasScrollBody != null), + assert(fillOverscroll != null), + super(key: key); + + /// Doc + final Widget child; + + /// Indicates 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. However, if the + /// [precedingScrollExtent] of the [SliverConstraints] and/or the [child]'s + /// extent exceeds the size of the viewport, the sliver will defer to the + /// child's size rather than overriding it. + final bool hasScrollBody; + + /// Indicates whether the child should stretch to fill the overscroll area + /// created by certain scroll physics, such as iOS' default scroll physics. + /// This value cannot be null. This flag is only relevant when the + /// [hasScrollBody] value is false. + /// + /// Defaults to false, meaning the default behavior is for the child to + /// maintain its size and not extend into the overscroll area. + final bool fillOverscroll; + + @override + Widget build(BuildContext context) { + if (hasScrollBody) + return _SliverFillRemainingWithScrollable(child: child); + if (!fillOverscroll) + return _SliverFillRemainingWithoutScrollable(child: child); + return _SliverFillRemainingAndOverscroll(child: child); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty( + 'child', + child, + ) + ); + final List flags = [ + if (hasScrollBody) 'scrollable', + if (fillOverscroll) 'fillOverscroll', + ]; + if (flags.isEmpty) + flags.add('nonscrollable'); + properties.add(IterableProperty('mode', flags)); + } +} + +class _SliverFillRemainingWithScrollable extends SingleChildRenderObjectWidget { + const _SliverFillRemainingWithScrollable({ + Key key, + Widget child, + }) : super(key: key, child: child); + + @override + RenderSliverFillRemainingWithScrollable createRenderObject(BuildContext context) => RenderSliverFillRemainingWithScrollable(); +} + +class _SliverFillRemainingWithoutScrollable extends SingleChildRenderObjectWidget { + const _SliverFillRemainingWithoutScrollable({ + Key key, + Widget child, + }) : super(key: key, child: child); + + @override + RenderSliverFillRemaining createRenderObject(BuildContext context) => RenderSliverFillRemaining(); +} + +class _SliverFillRemainingAndOverscroll extends SingleChildRenderObjectWidget { + const _SliverFillRemainingAndOverscroll({ + Key key, + Widget child, + }) : super(key: key, child: child); + + @override + RenderSliverFillRemainingAndOverscroll createRenderObject(BuildContext context) => RenderSliverFillRemainingAndOverscroll(); +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 02907dd9e4..6eb3a2c714 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -95,6 +95,7 @@ export 'src/widgets/shortcuts.dart'; export 'src/widgets/single_child_scroll_view.dart'; export 'src/widgets/size_changed_layout_notifier.dart'; export 'src/widgets/sliver.dart'; +export 'src/widgets/sliver_fill.dart'; export 'src/widgets/sliver_layout_builder.dart'; export 'src/widgets/sliver_persistent_header.dart'; export 'src/widgets/sliver_prototype_extent_list.dart'; diff --git a/packages/flutter/test/widgets/sliver_fill_remaining_test.dart b/packages/flutter/test/widgets/sliver_fill_remaining_test.dart index a521e399f4..44502fd2b1 100644 --- a/packages/flutter/test/widgets/sliver_fill_remaining_test.dart +++ b/packages/flutter/test/widgets/sliver_fill_remaining_test.dart @@ -8,442 +8,815 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; void main() { - testWidgets('SliverFillRemaining - no siblings', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: CustomScrollView( - controller: controller, - slivers: [ - SliverFillRemaining(child: Container()), - ], - ), - ), - ); - expect(tester.renderObject(find.byType(Container)).size.height, equals(600.0)); - controller.jumpTo(50.0); - await tester.pump(); - expect(tester.renderObject(find.byType(Container)).size.height, equals(600.0)); - - controller.jumpTo(-100.0); - await tester.pump(); - expect(tester.renderObject(find.byType(Container)).size.height, equals(600.0)); - - controller.jumpTo(0.0); - await tester.pump(); - expect(tester.renderObject(find.byType(Container)).size.height, equals(600.0)); - }); - - testWidgets('SliverFillRemaining - one sibling', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: CustomScrollView( - controller: controller, - slivers: [ - const SliverToBoxAdapter(child: SizedBox(height: 100.0)), - SliverFillRemaining(child: Container()), - ], - ), - ), - ); - expect(tester.renderObject(find.byType(Container)).size.height, equals(500.0)); - - controller.jumpTo(50.0); - await tester.pump(); - expect(tester.renderObject(find.byType(Container)).size.height, equals(550.0)); - - controller.jumpTo(-100.0); - await tester.pump(); - expect(tester.renderObject(find.byType(Container)).size.height, equals(400.0)); // (!) - - controller.jumpTo(0.0); - await tester.pump(); - expect(tester.renderObject(find.byType(Container)).size.height, equals(500.0)); - }); - - group('SliverFillRemaining - hasScrollBody', () { - final Widget sliverBox = SliverToBoxAdapter( - child: Container( - color: Colors.amber, - height: 150.0, - width: 150, - ), - ); - Widget boilerplate( - List slivers, { + // Helpers + final Widget sliverBox = SliverToBoxAdapter( + child: Container( + color: Colors.amber, + height: 150.0, + width: 150, + ), + ); + Widget boilerplate( + List slivers, { ScrollController controller, Axis scrollDirection = Axis.vertical, }) { - return MaterialApp( - home: Scaffold( - body: CustomScrollView( - scrollDirection: scrollDirection, - slivers: slivers, - controller: controller, - ), + return MaterialApp( + home: Scaffold( + body: CustomScrollView( + scrollDirection: scrollDirection, + slivers: slivers, + controller: controller, ), - ); - } + ), + ); + } - testWidgets('does not extend past viewport when false', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); - final List slivers = [ - sliverBox, - SliverFillRemaining( - child: Container(color: Colors.white), - hasScrollBody: false, - ), - ]; - 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('scrolls beyond viewport by default', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); - final List slivers = [ - 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); - }); - - // SliverFillRemaining considers child size when hasScrollBody: false - testWidgets('child without size is sized by extent when false', (WidgetTester tester) async { - final List slivers = [ - sliverBox, - SliverFillRemaining( - hasScrollBody: false, - child: Container(color: Colors.blue), - ), - ]; - await tester.pumpWidget(boilerplate(slivers)); - RenderBox box = tester.renderObject(find.byType(Container).last); - expect(box.size.height, equals(450)); - - await tester.pumpWidget(boilerplate(slivers, scrollDirection: Axis.horizontal)); - box = tester.renderObject(find.byType(Container).last); - expect(box.size.width, equals(650)); - }); - - testWidgets('child with size is sized by extent when false', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - final List slivers = [ - sliverBox, - SliverFillRemaining( - hasScrollBody: false, - child: Container( - key: key, - color: Colors.blue, - child: Align( - alignment: Alignment.bottomCenter, - child: RaisedButton( - child: const Text('bottomCenter button'), - onPressed: () {}, - ), + 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: [ + SliverFillRemaining(child: Container()), + ], ), ), - ), - ]; - await tester.pumpWidget(boilerplate(slivers)); - expect(tester.renderObject(find.byKey(key)).size.height, equals(450)); + ); + expect( + tester.renderObject(find.byType(Container)).size.height, + equals(600.0), + ); - // Also check that the button alignment is true to expectations - final Finder button = find.byType(RaisedButton); - expect(tester.getBottomLeft(button).dy, equals(600.0)); - expect(tester.getCenter(button).dx, equals(400.0)); + controller.jumpTo(50.0); + await tester.pump(); + expect( + tester.renderObject(find.byType(Container)).size.height, + equals(600.0), + ); - await tester.pumpWidget(boilerplate(slivers, scrollDirection: Axis.horizontal)); - expect(tester.renderObject(find.byKey(key)).size.width, equals(650)); + controller.jumpTo(-100.0); + await tester.pump(); + expect( + tester.renderObject(find.byType(Container)).size.height, + equals(600.0), + ); + + controller.jumpTo(0.0); + await tester.pump(); + expect( + tester.renderObject(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: [ + const SliverToBoxAdapter(child: SizedBox(height: 100.0)), + SliverFillRemaining(child: Container()), + ], + ), + ), + ); + expect( + tester.renderObject(find.byType(Container)).size.height, + equals(500.0), + ); + + controller.jumpTo(50.0); + await tester.pump(); + expect( + tester.renderObject(find.byType(Container)).size.height, + equals(550.0), + ); + + controller.jumpTo(-100.0); + await tester.pump(); + expect( + tester.renderObject(find.byType(Container)).size.height, + equals(400.0), + ); + + controller.jumpTo(0.0); + await tester.pump(); + expect( + tester.renderObject(find.byType(Container)).size.height, + equals(500.0), + ); + }); + + testWidgets('scrolls beyond viewportMainAxisExtent', (WidgetTester tester) async { + final ScrollController controller = ScrollController(); + final List slivers = [ + 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); + }); }); - testWidgets('extent is overridden by child with larger size when false', (WidgetTester tester) async { - final List slivers = [ - sliverBox, - SliverFillRemaining( - hasScrollBody: false, - child: Container( - color: Colors.blue, - height: 600, - width: 1000, + group('hasScrollBody: false', () { + testWidgets('does not extend past viewportMainAxisExtent', (WidgetTester tester) async { + final ScrollController controller = ScrollController(); + final List slivers = [ + sliverBox, + SliverFillRemaining( + child: Container(color: Colors.white), + hasScrollBody: false, ), - ), - ]; - await tester.pumpWidget(boilerplate(slivers)); - RenderBox box = tester.renderObject(find.byType(Container).last); - expect(box.size.height, equals(600)); + ]; - await tester.pumpWidget(boilerplate(slivers, scrollDirection: Axis.horizontal)); - box = tester.renderObject(find.byType(Container).last); - expect(box.size.width, equals(1000)); - }); + 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('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent when false', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - final List slivers = [ - SliverFixedExtentList( - itemExtent: 150, - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) => Container(color: Colors.amber), - childCount: 5, + testWidgets('child without size is sized by extent', (WidgetTester tester) async { + final List slivers = [ + sliverBox, + SliverFillRemaining( + hasScrollBody: false, + child: Container(color: Colors.blue), ), - ), - SliverFillRemaining( - hasScrollBody: false, - child: Container( - key: key, - color: Colors.blue[300], - child: Align( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.all(50.0), + ]; + + await tester.pumpWidget(boilerplate(slivers)); + RenderBox box = tester.renderObject(find.byType(Container).last); + expect(box.size.height, equals(450)); + + await tester.pumpWidget(boilerplate( + slivers, + scrollDirection: Axis.horizontal, + )); + box = tester.renderObject(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 slivers = [ + sliverBox, + SliverFillRemaining( + hasScrollBody: false, + child: Container( + key: key, + color: Colors.blue, + child: Align( + alignment: Alignment.bottomCenter, child: RaisedButton( - child: const Text('center button'), + child: const Text('bottomCenter 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(find.byKey(key)).size.height, equals(148.0)); + ]; + await tester.pumpWidget(boilerplate(slivers)); + expect( + tester.renderObject(find.byKey(key)).size.height, + equals(450), + ); - // Also check that the button alignment is true to expectations - final Finder button = find.byType(RaisedButton); - expect(tester.getBottomLeft(button).dy, equals(550.0)); - expect(tester.getCenter(button).dx, equals(400.0)); - }); + // Also check that the button alignment is true to expectations + final Finder button = find.byType(RaisedButton); + expect(tester.getBottomLeft(button).dy, equals(600.0)); + expect(tester.getCenter(button).dx, equals(400.0)); - // iOS/Similar scroll physics when hasScrollBody: false & fillOverscroll: true behavior - testWidgets('child without size is sized by extent and overscroll', (WidgetTester tester) async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - final List slivers = [ - sliverBox, - SliverFillRemaining( - hasScrollBody: false, - fillOverscroll: true, - child: Container(color: Colors.blue), - ), - ]; - await tester.pumpWidget(boilerplate(slivers)); - final RenderBox box1 = tester.renderObject(find.byType(Container).last); - expect(box1.size.height, equals(450)); + // Check Axis.horizontal + await tester.pumpWidget(boilerplate( + slivers, + scrollDirection: Axis.horizontal, + )); + expect( + tester.renderObject(find.byKey(key)).size.width, + equals(650), + ); + }); - await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0)); - await tester.pump(); - final RenderBox box2 = tester.renderObject(find.byType(Container).last); - expect(box2.size.height, greaterThan(450)); - debugDefaultTargetPlatformOverride = null; - }); - - testWidgets('child with size is overridden and sized by extent and overscroll', (WidgetTester tester) async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - final GlobalKey key = GlobalKey(); - final List slivers = [ - sliverBox, - SliverFillRemaining( - hasScrollBody: false, - fillOverscroll: true, - child: Container( - key: key, - color: Colors.blue, - child: Align( - alignment: Alignment.bottomCenter, - child: RaisedButton( - child: const Text('bottomCenter button'), - onPressed: () {}, - ), + testWidgets('extent is overridden by child with larger size', (WidgetTester tester) async { + final List slivers = [ + sliverBox, + SliverFillRemaining( + hasScrollBody: false, + child: Container( + color: Colors.blue, + height: 600, + width: 1000, ), ), - ), - ]; - await tester.pumpWidget(boilerplate(slivers)); - expect(tester.renderObject(find.byKey(key)).size.height, equals(450)); + ]; + await tester.pumpWidget(boilerplate(slivers)); + RenderBox box = tester.renderObject(find.byType(Container).last); + expect(box.size.height, equals(600)); - await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0)); - await tester.pump(); - expect(tester.renderObject(find.byKey(key)).size.height, greaterThan(450)); + await tester.pumpWidget(boilerplate( + slivers, + scrollDirection: Axis.horizontal, + )); + box = tester.renderObject(find.byType(Container).last); + expect(box.size.width, equals(1000)); + }); - // Also check that the button alignment is true to expectations, even with - // child stretching to fill overscroll - final Finder button = find.byType(RaisedButton); - expect(tester.getBottomLeft(button).dy, equals(600.0)); - expect(tester.getCenter(button).dx, equals(400.0)); - debugDefaultTargetPlatformOverride = null; - }); - - testWidgets('extent is overridden by child size and overscroll if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - final GlobalKey key = GlobalKey(); - final ScrollController controller = ScrollController(); - final List slivers = [ - SliverFixedExtentList( - itemExtent: 150, - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) => Container(color: Colors.amber), - childCount: 5, + testWidgets('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + final List slivers = [ + 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( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.all(50.0), - child: RaisedButton( - child: const Text('center button'), - onPressed: () {}, + SliverFillRemaining( + hasScrollBody: false, + child: Container( + key: key, + color: Colors.blue[300], + child: Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.all(50.0), + child: RaisedButton( + 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(find.byKey(key)).size.height, equals(148.0)); - // Check that the button alignment is true to expectations - final Finder button = find.byType(RaisedButton); - expect(tester.getBottomLeft(button).dy, equals(550.0)); - expect(tester.getCenter(button).dx, equals(400.0)); - debugDefaultTargetPlatformOverride = null; + ]; + await tester.pumpWidget(boilerplate(slivers)); + await tester.drag(find.byType(Scrollable), const Offset(0.0, -750.0)); + await tester.pump(); + expect( + tester.renderObject(find.byKey(key)).size.height, + equals(148.0), + ); - // Drag for overscroll - await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0)); - await tester.pump(); - expect(tester.renderObject(find.byKey(key)).size.height, greaterThan(148.0)); + // Also check that the button alignment is true to expectations + final Finder button = find.byType(RaisedButton); + expect(tester.getBottomLeft(button).dy, equals(550.0)); + expect(tester.getCenter(button).dx, equals(400.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)); - debugDefaultTargetPlatformOverride = null; - }); - - // Android/Other scroll physics when hasScrollBody: false, ignores fillOverscroll: true - testWidgets('child without size is sized by extent, fillOverscroll is ignored', (WidgetTester tester) async { - final List slivers = [ - sliverBox, - SliverFillRemaining( - hasScrollBody: false, - fillOverscroll: true, - child: Container(color: Colors.blue), - ), - ]; - await tester.pumpWidget(boilerplate(slivers)); - final RenderBox box1 = tester.renderObject(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(find.byType(Container).last); - expect(box2.size.height, equals(450)); - }); - - testWidgets('child with size is overridden and sized by extent, fillOverscroll is ignored', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - final List slivers = [ - sliverBox, - SliverFillRemaining( - hasScrollBody: false, - fillOverscroll: true, - child: Container( - key: key, - color: Colors.blue, - child: Align( - alignment: Alignment.bottomCenter, - child: RaisedButton( - child: const Text('bottomCenter button'), - onPressed: () {}, - ), + testWidgets('alignment with a flexible works', (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final GlobalKey key = GlobalKey(); + final List slivers = [ + sliverBox, + SliverFillRemaining( + hasScrollBody: false, + child: Column( + key: key, + mainAxisSize: MainAxisSize.min, + children: [ + const Flexible( + child: Center(child: FlutterLogo(size: 100)), + fit: FlexFit.loose, + ), + RaisedButton( + child: const Text('Bottom'), + onPressed: () {}, + ), + ] ), ), - ), - ]; - await tester.pumpWidget(boilerplate(slivers)); - expect(tester.renderObject(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(find.byKey(key)).size.height, equals(450)); + await tester.pumpWidget(boilerplate(slivers)); + expect( + tester.renderObject(find.byKey(key)).size.height, + equals(450), + ); - // Also check that the button alignment is true to expectations - final Finder button = find.byType(RaisedButton); - expect(tester.getBottomLeft(button).dy, equals(600.0)); - expect(tester.getCenter(button).dx, equals(400.0)); - }); + // Check that the logo alignment is true to expectations + final Finder logo = find.byType(FlutterLogo); + expect( + tester.renderObject(logo).size, + const Size(100.0, 100.0), + ); + expect(tester.getCenter(logo), const Offset(400.0, 351.0)); - testWidgets('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent, fillOverscroll is ignored', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - final ScrollController controller = ScrollController(); - final List slivers = [ - 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( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.all(50.0), - child: RaisedButton( - child: const Text('center button'), - onPressed: () {}, + // Also check that the button alignment is true to expectations + final Finder button = find.byType(RaisedButton); + expect( + tester.renderObject(button).size, + const Size(116.0, 48.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(find.byKey(key)).size.height, + equals(450), + ); + expect( + tester.renderObject(logo).size, + const Size(100.0, 100.0), + ); + expect(tester.getCenter(logo).dy, lessThan(351.0)); + expect( + tester.renderObject(button).size, + const Size(116.0, 48.0), + ); + expect(tester.getBottomLeft(button).dy, lessThan(600.0)); + expect(tester.getCenter(button).dx, equals(400.0)); + + debugDefaultTargetPlatformOverride = null; + }); + + group('fillOverscroll: true, relevant platforms', () { + testWidgets('child without size is sized by extent and overscroll', (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final List slivers = [ + sliverBox, + SliverFillRemaining( + hasScrollBody: false, + fillOverscroll: true, + child: Container(color: Colors.blue), + ), + ]; + + // Check size + await tester.pumpWidget(boilerplate(slivers)); + final RenderBox box1 = tester.renderObject(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(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(find.byType(Container).last); + expect(box3.size.height, equals(450)); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('child with smaller size is overridden and sized by extent and overscroll', (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final GlobalKey key = GlobalKey(); + final List slivers = [ + sliverBox, + SliverFillRemaining( + hasScrollBody: false, + fillOverscroll: true, + child: Container( + key: key, + color: Colors.blue, + child: Align( + alignment: Alignment.bottomCenter, + child: RaisedButton( + child: const Text('bottomCenter button'), + onPressed: () {}, + ), ), ), ), - ), - ), - ]; - await tester.pumpWidget(boilerplate(slivers, controller: controller)); - // Scroll to the end - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pump(); - expect(tester.renderObject(find.byKey(key)).size.height, equals(148.0)); - // Check that the button alignment is true to expectations - final Finder button = find.byType(RaisedButton); - expect(tester.getBottomLeft(button).dy, equals(550.0)); - expect(tester.getCenter(button).dx, equals(400.0)); + ]; + await tester.pumpWidget(boilerplate(slivers)); + expect( + tester.renderObject(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(find.byKey(key)).size.height, equals(148.0)); + await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0)); + await tester.pump(); + expect( + tester.renderObject(find.byKey(key)).size.height, + greaterThan(450), + ); - // Check that the button alignment is still centered in stretched child - expect(tester.getBottomLeft(button).dy, equals(550.0)); - expect(tester.getCenter(button).dx, equals(400.0)); + // Also check that the button alignment is true to expectations, even with + // child stretching to fill overscroll + final Finder button = find.byType(RaisedButton); + 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(find.byKey(key)).size.height, + equals(450), + ); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('extent is overridden by child size and overscroll if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final GlobalKey key = GlobalKey(); + final ScrollController controller = ScrollController(); + final List slivers = [ + 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( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.all(50.0), + child: RaisedButton( + 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(find.byKey(key)).size.height, + equals(148.0), + ); + + // Check that the button alignment is true to expectations + final Finder button = find.byType(RaisedButton); + expect(tester.getBottomLeft(button).dy, equals(550.0)); + expect(tester.getCenter(button).dx, equals(400.0)); + debugDefaultTargetPlatformOverride = null; + + // Drag for overscroll + await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0)); + await tester.pump(); + expect( + tester.renderObject(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(find.byKey(key)).size.height, + equals(148.0), + ); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('fillOverscroll works when child has no size and precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final GlobalKey key = GlobalKey(); + final ScrollController controller = ScrollController(); + final List slivers = [ + 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, + ), + ), + ]; + + await tester.pumpWidget(boilerplate(slivers, controller: controller)); + const BoxDecoration amberBox = BoxDecoration(color: Colors.amber); + const BoxDecoration blueBox = BoxDecoration(color: Colors.blue); + + // 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( + tester.widgetList(find.byType(DecoratedBox)).last.decoration, + amberBox, + ); + + // 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( + tester.widgetList(find.byType(DecoratedBox)).last.decoration, + blueBox, + ); + + // Ensure overscroll retracts to original size after releasing gesture + await tester.pumpAndSettle(); + expect(find.byKey(key), findsNothing); + expect( + tester.widgetList(find.byType(DecoratedBox)).last.decoration, + amberBox, + ); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('alignment with a flexible works with fillOverscroll', (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final GlobalKey key = GlobalKey(); + final List slivers = [ + sliverBox, + SliverFillRemaining( + hasScrollBody: false, + fillOverscroll: true, + child: Column( + key: key, + mainAxisSize: MainAxisSize.min, + children: [ + const Flexible( + child: Center(child: FlutterLogo(size: 100)), + fit: FlexFit.loose, + ), + RaisedButton( + child: const Text('Bottom'), + onPressed: () {}, + ), + ] + ), + ), + ]; + + await tester.pumpWidget(boilerplate(slivers)); + expect( + tester.renderObject(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(logo).size, + const Size(100.0, 100.0), + ); + expect(tester.getCenter(logo), const Offset(400.0, 351.0)); + + // Also check that the button alignment is true to expectations. + final Finder button = find.byType(RaisedButton); + expect( + tester.renderObject(button).size, + const Size(116.0, 48.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(find.byKey(key)).size.height, + greaterThan(450), + ); + expect( + tester.renderObject(logo).size, + const Size(100.0, 100.0), + ); + expect(tester.getCenter(logo).dy, lessThan(351.0)); + expect( + tester.renderObject(button).size, + const Size(116.0, 48.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(find.byKey(key)).size.height, + equals(450), + ); + expect( + tester.renderObject(logo).size, + const Size(100.0, 100.0), + ); + expect(tester.getCenter(logo), const Offset(400.0, 351.0)); + expect( + tester.renderObject(button).size, + const Size(116.0, 48.0), + ); + expect(tester.getBottomLeft(button).dy, equals(600.0)); + expect(tester.getCenter(button).dx, equals(400.0)); + + debugDefaultTargetPlatformOverride = null; + }); + }); + + group('fillOverscroll: true, is ignored on irrevelant platforms', () { + // Android/Other scroll physics when hasScrollBody: false, ignores fillOverscroll: true + testWidgets('child without size is sized by extent', (WidgetTester tester) async { + final List slivers = [ + sliverBox, + SliverFillRemaining( + hasScrollBody: false, + fillOverscroll: true, + child: Container(color: Colors.blue), + ), + ]; + await tester.pumpWidget(boilerplate(slivers)); + final RenderBox box1 = tester.renderObject(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(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 slivers = [ + sliverBox, + SliverFillRemaining( + hasScrollBody: false, + fillOverscroll: true, + child: Container( + key: key, + color: Colors.blue, + child: Align( + alignment: Alignment.bottomCenter, + child: RaisedButton( + child: const Text('bottomCenter button'), + onPressed: () {}, + ), + ), + ), + ), + ]; + await tester.pumpWidget(boilerplate(slivers)); + expect( + tester.renderObject(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(find.byKey(key)).size.height, + equals(450), + ); + + // Also check that the button alignment is true to expectations + final Finder button = find.byType(RaisedButton); + 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 slivers = [ + 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( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.all(50.0), + child: RaisedButton( + 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(find.byKey(key)).size.height, + equals(148.0), + ); + + // Check that the button alignment is true to expectations + final Finder button = find.byType(RaisedButton); + 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(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 slivers = [ + 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, + ), + ), + ]; + + await tester.pumpWidget(boilerplate(slivers, controller: controller)); + const BoxDecoration amberBox = BoxDecoration(color: Colors.amber); + + // Scroll to bottom + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pump(); + + // End of list + expect(find.byKey(key), findsNothing); + expect( + tester.widgetList(find.byType(DecoratedBox)).last.decoration, + amberBox, + ); + + // Overscroll + await tester.drag(find.byType(Scrollable), const Offset(0.0, -50.0)); + await tester.pump(); + + expect(find.byKey(key), findsNothing); + expect( + tester.widgetList(find.byType(DecoratedBox)).last.decoration, + amberBox, + ); + }); + }); }); }); }