Commit 76b21d28 authored by Kate Lovett's avatar Kate Lovett Committed by Flutter GitHub Bot

Refactor SliverFillRemaining (#47379)

parent 50058247
......@@ -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 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;
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() {
double childExtent;
double extent = constraints.viewportMainAxisExtent - constraints.precedingScrollExtent;
double maxExtent = constraints.remainingPaintExtent - math.min(constraints.overlap, 0.0);
// 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 (hasScrollBody) {
extent = maxExtent;
if (child != null)
child.layout(
constraints.asBoxConstraints(
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,
paintExtent: paintedChildSize,
maxPaintExtent: paintedChildSize,
hasVisualOverflow: extent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
} else if (child != null) {
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 non-scrollable [RenderBox] which is
/// sized to fit the remaining space in the viewport.
RenderSliverFillRemaining({ 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;
if (child != null) {
double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child.getMaxIntrinsicWidth(constraints.crossAxisExtent);
......@@ -131,21 +152,14 @@ 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(
// 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: fillOverscroll ? maxExtent : extent,
),
parentUsesSize: true,
);
} else {
child.layout(constraints.asBoxConstraints(), parentUsesSize: true);
}
maxExtent: extent,
));
}
assert(extent.isFinite,
......@@ -158,7 +172,7 @@ class RenderSliverFillRemaining extends RenderSliverSingleBoxAdapter {
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);
}
}
......@@ -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';
......
......@@ -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].
......
......@@ -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: <Widget>[
/// 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: <Widget>[
/// 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: <Widget>[
/// 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: <Widget>[
/// 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
......
// 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: <Widget>[
/// 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: <Widget>[
/// 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: <Widget>[
/// 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: <Widget>[
/// 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<Widget>(
'child',
child,
)
);
final List<String> flags = <String>[
if (hasScrollBody) 'scrollable',
if (fillOverscroll) 'fillOverscroll',
];
if (flags.isEmpty)
flags.add('nonscrollable');
properties.add(IterableProperty<String>('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();
}
......@@ -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';
......
......@@ -8,7 +8,34 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() {
testWidgets('SliverFillRemaining - no siblings', (WidgetTester tester) async {
// 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(
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(
......@@ -21,22 +48,34 @@ void main() {
),
),
);
expect(tester.renderObject<RenderBox>(find.byType(Container)).size.height, equals(600.0));
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));
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));
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));
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(600.0),
);
});
testWidgets('SliverFillRemaining - one sibling', (WidgetTester tester) async {
testWidgets('one sibling', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(
Directionality(
......@@ -50,52 +89,39 @@ void main() {
),
),
);
expect(tester.renderObject<RenderBox>(find.byType(Container)).size.height, equals(500.0));
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));
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)); // (!)
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));
});
group('SliverFillRemaining - hasScrollBody', () {
final Widget sliverBox = SliverToBoxAdapter(
child: Container(
color: Colors.amber,
height: 150.0,
width: 150,
),
expect(
tester.renderObject<RenderBox>(find.byType(Container)).size.height,
equals(500.0),
);
Widget boilerplate(
List<Widget> slivers, {
ScrollController controller,
Axis scrollDirection = Axis.vertical,
}) {
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
scrollDirection: scrollDirection,
slivers: slivers,
controller: controller,
),
),
);
}
});
testWidgets('does not extend past viewport when false', (WidgetTester tester) async {
testWidgets('scrolls beyond viewportMainAxisExtent', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
child: Container(color: Colors.white),
hasScrollBody: false,
),
];
await tester.pumpWidget(boilerplate(slivers, controller: controller));
......@@ -103,29 +129,32 @@ void main() {
expect(find.byType(Container), findsNWidgets(2));
controller.jumpTo(150.0);
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(find.byType(Container), findsNWidgets(2));
expect(controller.offset, 150.0);
expect(find.byType(Container), findsOneWidget);
});
});
testWidgets('scrolls beyond viewport by default', (WidgetTester tester) async {
group('hasScrollBody: false', () {
testWidgets('does not extend past viewportMainAxisExtent', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
final List<Widget> slivers = <Widget>[
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, 150.0);
expect(find.byType(Container), findsOneWidget);
expect(controller.offset, 0.0);
expect(find.byType(Container), findsNWidgets(2));
});
// SliverFillRemaining considers child size when hasScrollBody: false
testWidgets('child without size is sized by extent when false', (WidgetTester tester) async {
testWidgets('child without size is sized by extent', (WidgetTester tester) async {
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
......@@ -133,16 +162,20 @@ void main() {
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));
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 size is sized by extent when false', (WidgetTester tester) async {
testWidgets('child with smaller size is sized by extent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final List<Widget> slivers = <Widget>[
sliverBox,
......@@ -162,18 +195,28 @@ void main() {
),
];
await tester.pumpWidget(boilerplate(slivers));
expect(tester.renderObject<RenderBox>(find.byKey(key)).size.height, equals(450));
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(RaisedButton);
expect(tester.getBottomLeft(button).dy, equals(600.0));
expect(tester.getCenter(button).dx, equals(400.0));
await tester.pumpWidget(boilerplate(slivers, scrollDirection: Axis.horizontal));
expect(tester.renderObject<RenderBox>(find.byKey(key)).size.width, equals(650));
// 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 when false', (WidgetTester tester) async {
testWidgets('extent is overridden by child with larger size', (WidgetTester tester) async {
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
......@@ -189,12 +232,15 @@ void main() {
RenderBox box = tester.renderObject<RenderBox>(find.byType(Container).last);
expect(box.size.height, equals(600));
await tester.pumpWidget(boilerplate(slivers, scrollDirection: Axis.horizontal));
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 when false', (WidgetTester tester) async {
testWidgets('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final List<Widget> slivers = <Widget>[
SliverFixedExtentList(
......@@ -225,7 +271,10 @@ void main() {
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));
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(RaisedButton);
......@@ -233,7 +282,76 @@ void main() {
expect(tester.getCenter(button).dx, equals(400.0));
});
// iOS/Similar scroll physics when hasScrollBody: false & fillOverscroll: true behavior
testWidgets('alignment with a flexible works', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
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)),
fit: FlexFit.loose,
),
RaisedButton(
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),
);
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<RenderBox>(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<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,
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<Widget> slivers = <Widget>[
......@@ -244,18 +362,27 @@ void main() {
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));
debugDefaultTargetPlatformOverride = null;
});
testWidgets('child with size is overridden and sized by extent and overscroll', (WidgetTester tester) async {
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<Widget> slivers = <Widget>[
......@@ -277,17 +404,31 @@ void main() {
),
];
await tester.pumpWidget(boilerplate(slivers));
expect(tester.renderObject<RenderBox>(find.byKey(key)).size.height, equals(450));
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));
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(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<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
debugDefaultTargetPlatformOverride = null;
});
......@@ -299,7 +440,8 @@ void main() {
SliverFixedExtentList(
itemExtent: 150,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Container(color: Colors.amber),
(BuildContext context, int index) =>
Container(color: Colors.amber),
childCount: 5,
),
),
......@@ -323,10 +465,15 @@ void main() {
),
];
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));
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(RaisedButton);
expect(tester.getBottomLeft(button).dy, equals(550.0));
......@@ -336,16 +483,179 @@ void main() {
// 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));
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),
);
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<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,
),
),
];
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<DecoratedBox>(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<DecoratedBox>(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<DecoratedBox>(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<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)),
fit: FlexFit.loose,
),
RaisedButton(
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),
);
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<RenderBox>(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<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,
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<RenderBox>(find.byKey(key)).size.height,
equals(450),
);
expect(
tester.renderObject<RenderBox>(logo).size,
const Size(100.0, 100.0),
);
expect(tester.getCenter(logo), const Offset(400.0, 351.0));
expect(
tester.renderObject<RenderBox>(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, fillOverscroll is ignored', (WidgetTester tester) async {
testWidgets('child without size is sized by extent', (WidgetTester tester) async {
final List<Widget> slivers = <Widget>[
sliverBox,
SliverFillRemaining(
......@@ -364,7 +674,7 @@ void main() {
expect(box2.size.height, equals(450));
});
testWidgets('child with size is overridden and sized by extent, fillOverscroll is ignored', (WidgetTester tester) async {
testWidgets('child with size is overridden and sized by extent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final List<Widget> slivers = <Widget>[
sliverBox,
......@@ -385,11 +695,17 @@ void main() {
),
];
await tester.pumpWidget(boilerplate(slivers));
expect(tester.renderObject<RenderBox>(find.byKey(key)).size.height, equals(450));
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));
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(RaisedButton);
......@@ -397,14 +713,15 @@ void main() {
expect(tester.getCenter(button).dx, equals(400.0));
});
testWidgets('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent, fillOverscroll is ignored', (WidgetTester tester) async {
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),
(BuildContext context, int index) =>
Container(color: Colors.amber),
childCount: 5,
),
),
......@@ -428,10 +745,15 @@ void main() {
),
];
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));
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(RaisedButton);
expect(tester.getBottomLeft(button).dy, equals(550.0));
......@@ -439,11 +761,62 @@ void main() {
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));
expect(
tester.renderObject<RenderBox>(find.byKey(key)).size.height,
equals(148.0),
);
// Check that the button alignment is still centered in stretched child
// 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) => 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<DecoratedBox>(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<DecoratedBox>(find.byType(DecoratedBox)).last.decoration,
amberBox,
);
});
});
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment