1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/rendering.dart';
7
8import 'framework.dart';
9import 'scroll_delegate.dart';
10import 'sliver.dart';
11
12/// A sliver that contains multiple box children that each fills the viewport.
13///
14/// _To learn more about slivers, see [CustomScrollView.slivers]._
15///
16/// [SliverFillViewport] places its children in a linear array along the main
17/// axis. Each child is sized to fill the viewport, both in the main and cross
18/// axis.
19///
20/// See also:
21///
22/// * [SliverFixedExtentList], which has a configurable
23/// [SliverFixedExtentList.itemExtent].
24/// * [SliverPrototypeExtentList], which is similar to [SliverFixedExtentList]
25/// except that it uses a prototype list item instead of a pixel value to define
26/// the main axis extent of each item.
27/// * [SliverList], which does not require its children to have the same
28/// extent in the main axis.
29class SliverFillViewport extends StatelessWidget {
30 /// Creates a sliver whose box children that each fill the viewport.
31 const SliverFillViewport({
32 super.key,
33 required this.delegate,
34 this.viewportFraction = 1.0,
35 this.padEnds = true,
36 }) : assert(viewportFraction > 0.0);
37
38 /// The fraction of the viewport that each child should fill in the main axis.
39 ///
40 /// If this fraction is less than 1.0, more than one child will be visible at
41 /// once. If this fraction is greater than 1.0, each child will be larger than
42 /// the viewport in the main axis.
43 final double viewportFraction;
44
45 /// Whether to add padding to both ends of the list.
46 ///
47 /// If this is set to true and [viewportFraction] < 1.0, padding will be added
48 /// such that the first and last child slivers will be in the center of the
49 /// viewport when scrolled all the way to the start or end, respectively. You
50 /// may want to set this to false if this [SliverFillViewport] is not the only
51 /// widget along this main axis, such as in a [CustomScrollView] with multiple
52 /// children.
53 ///
54 /// If [viewportFraction] is greater than one, this option has no effect.
55 /// Defaults to true.
56 final bool padEnds;
57
58 /// {@macro flutter.widgets.SliverMultiBoxAdaptorWidget.delegate}
59 final SliverChildDelegate delegate;
60
61 @override
62 Widget build(BuildContext context) {
63 return _SliverFractionalPadding(
64 viewportFraction: padEnds ? clampDouble(1 - viewportFraction, 0, 1) / 2 : 0,
65 sliver: _SliverFillViewportRenderObjectWidget(
66 viewportFraction: viewportFraction,
67 delegate: delegate,
68 ),
69 );
70 }
71}
72
73class _SliverFillViewportRenderObjectWidget extends SliverMultiBoxAdaptorWidget {
74 const _SliverFillViewportRenderObjectWidget({
75 required super.delegate,
76 this.viewportFraction = 1.0,
77 }) : assert(viewportFraction > 0.0);
78
79 final double viewportFraction;
80
81 @override
82 RenderSliverFillViewport createRenderObject(BuildContext context) {
83 final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
84 return RenderSliverFillViewport(childManager: element, viewportFraction: viewportFraction);
85 }
86
87 @override
88 void updateRenderObject(BuildContext context, RenderSliverFillViewport renderObject) {
89 renderObject.viewportFraction = viewportFraction;
90 }
91}
92
93class _SliverFractionalPadding extends SingleChildRenderObjectWidget {
94 const _SliverFractionalPadding({
95 this.viewportFraction = 0,
96 Widget? sliver,
97 }) : assert(viewportFraction >= 0),
98 assert(viewportFraction <= 0.5),
99 super(child: sliver);
100
101 final double viewportFraction;
102
103 @override
104 RenderObject createRenderObject(BuildContext context) => _RenderSliverFractionalPadding(viewportFraction: viewportFraction);
105
106 @override
107 void updateRenderObject(BuildContext context, _RenderSliverFractionalPadding renderObject) {
108 renderObject.viewportFraction = viewportFraction;
109 }
110}
111
112class _RenderSliverFractionalPadding extends RenderSliverEdgeInsetsPadding {
113 _RenderSliverFractionalPadding({
114 double viewportFraction = 0,
115 }) : assert(viewportFraction <= 0.5),
116 assert(viewportFraction >= 0),
117 _viewportFraction = viewportFraction;
118
119 SliverConstraints? _lastResolvedConstraints;
120
121 double get viewportFraction => _viewportFraction;
122 double _viewportFraction;
123 set viewportFraction(double newValue) {
124 if (_viewportFraction == newValue) {
125 return;
126 }
127 _viewportFraction = newValue;
128 _markNeedsResolution();
129 }
130
131 @override
132 EdgeInsets? get resolvedPadding => _resolvedPadding;
133 EdgeInsets? _resolvedPadding;
134
135 void _markNeedsResolution() {
136 _resolvedPadding = null;
137 markNeedsLayout();
138 }
139
140 void _resolve() {
141 if (_resolvedPadding != null && _lastResolvedConstraints == constraints) {
142 return;
143 }
144
145 final double paddingValue = constraints.viewportMainAxisExtent * viewportFraction;
146 _lastResolvedConstraints = constraints;
147 switch (constraints.axis) {
148 case Axis.horizontal:
149 _resolvedPadding = EdgeInsets.symmetric(horizontal: paddingValue);
150 case Axis.vertical:
151 _resolvedPadding = EdgeInsets.symmetric(vertical: paddingValue);
152 }
153
154 return;
155 }
156
157 @override
158 void performLayout() {
159 _resolve();
160 super.performLayout();
161 }
162}
163
164/// A sliver that contains a single box child that fills the remaining space in
165/// the viewport.
166///
167/// _To learn more about slivers, see [CustomScrollView.slivers]._
168///
169/// [SliverFillRemaining] will size its [child] to fill the viewport in the
170/// cross axis. The extent of the sliver and its child's size in the main axis
171/// is computed conditionally, described in further detail below.
172///
173/// Typically this will be the last sliver in a viewport, since (by definition)
174/// there is never any room for anything beyond this sliver.
175///
176/// ## Main Axis Extent
177///
178/// ### When [SliverFillRemaining] has a scrollable child
179///
180/// The [hasScrollBody] flag indicates whether the sliver's child has a
181/// scrollable body. This value is never null, and defaults to true. A common
182/// example of this use is a [NestedScrollView]. In this case, the sliver will
183/// size its child to fill the maximum available extent. [SliverFillRemaining]
184/// will not constrain the scrollable area, as it could potentially have an
185/// infinite depth. This is also true for use cases such as a [ScrollView] when
186/// [ScrollView.shrinkWrap] is true.
187///
188/// ### When [SliverFillRemaining] does not have a scrollable child
189///
190/// When [hasScrollBody] is set to false, the child's size is taken into account
191/// when considering the extent to which it should fill the space. The extent to
192/// which the preceding slivers have been scrolled is also taken into
193/// account in deciding how to layout this sliver.
194///
195/// [SliverFillRemaining] will size its [child] to fill the viewport in the
196/// main axis if that space is larger than the child's extent, and the amount
197/// of space that has been scrolled beforehand has not exceeded the main axis
198/// extent of the viewport.
199///
200/// {@tool dartpad}
201/// In this sample the [SliverFillRemaining] sizes its [child] to fill the
202/// remaining extent of the viewport in both axes. The icon is centered in the
203/// sliver, and would be in any computed extent for the sliver.
204///
205/// ** See code in examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.0.dart **
206/// {@end-tool}
207///
208/// [SliverFillRemaining] will defer to the size of its [child] if the
209/// child's size exceeds the remaining space in the viewport.
210///
211/// {@tool dartpad}
212/// In this sample the [SliverFillRemaining] defers to the size of its [child]
213/// because the child's extent exceeds that of the remaining extent of the
214/// viewport's main axis.
215///
216/// ** See code in examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.1.dart **
217/// {@end-tool}
218///
219/// [SliverFillRemaining] will defer to the size of its [child] if the
220/// [SliverConstraints.precedingScrollExtent] exceeded the length of the viewport's main axis.
221///
222/// {@tool dartpad}
223/// In this sample the [SliverFillRemaining] defers to the size of its [child]
224/// because the [SliverConstraints.precedingScrollExtent] has gone
225/// beyond that of the viewport's main axis.
226///
227/// ** See code in examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.2.dart **
228/// {@end-tool}
229///
230/// For [ScrollPhysics] that allow overscroll, such as
231/// [BouncingScrollPhysics], setting the [fillOverscroll] flag to true allows
232/// the size of the [child] to _stretch_, filling the overscroll area. It does
233/// this regardless of the path chosen to provide the child's size.
234///
235/// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_fill_overscroll.mp4}
236///
237/// {@tool dartpad}
238/// In this sample the [SliverFillRemaining]'s child stretches to fill the
239/// overscroll area when [fillOverscroll] is true. This sample also features a
240/// button that is pinned to the bottom of the sliver, regardless of size or
241/// overscroll behavior. Try switching [fillOverscroll] to see the difference.
242///
243/// This sample only shows the overscroll behavior on devices that support
244/// overscroll.
245///
246/// ** See code in examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.3.dart **
247/// {@end-tool}
248///
249///
250/// See also:
251///
252/// * [SliverFillViewport], which sizes its children based on the
253/// size of the viewport, regardless of what else is in the scroll view.
254/// * [SliverList], which shows a list of variable-sized children in a
255/// viewport.
256class SliverFillRemaining extends StatelessWidget {
257 /// Creates a sliver that fills the remaining space in the viewport.
258 const SliverFillRemaining({
259 super.key,
260 this.child,
261 this.hasScrollBody = true,
262 this.fillOverscroll = false,
263 });
264
265 /// Box child widget that fills the remaining space in the viewport.
266 ///
267 /// The main [SliverFillRemaining] documentation contains more details.
268 final Widget? child;
269
270 /// Indicates whether the child has a scrollable body, this value cannot be
271 /// null.
272 ///
273 /// Defaults to true such that the child will extend beyond the viewport and
274 /// scroll, as seen in [NestedScrollView].
275 ///
276 /// Setting this value to false will allow the child to fill the remainder of
277 /// the viewport and not extend further. However, if the
278 /// [SliverConstraints.precedingScrollExtent] and/or the [child]'s
279 /// extent exceeds the size of the viewport, the sliver will defer to the
280 /// child's size rather than overriding it.
281 final bool hasScrollBody;
282
283 /// Indicates whether the child should stretch to fill the overscroll area
284 /// created by certain scroll physics, such as iOS' default scroll physics.
285 /// This flag is only relevant when [hasScrollBody] is false.
286 ///
287 /// Defaults to false, meaning that the default behavior is for the child to
288 /// maintain its size and not extend into the overscroll area.
289 final bool fillOverscroll;
290
291 @override
292 Widget build(BuildContext context) {
293 if (hasScrollBody) {
294 return _SliverFillRemainingWithScrollable(child: child);
295 }
296 if (!fillOverscroll) {
297 return _SliverFillRemainingWithoutScrollable(child: child);
298 }
299 return _SliverFillRemainingAndOverscroll(child: child);
300 }
301
302 @override
303 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
304 super.debugFillProperties(properties);
305 properties.add(
306 DiagnosticsProperty<Widget>(
307 'child',
308 child,
309 ),
310 );
311 final List<String> flags = <String>[
312 if (hasScrollBody) 'scrollable',
313 if (fillOverscroll) 'fillOverscroll',
314 ];
315 if (flags.isEmpty) {
316 flags.add('nonscrollable');
317 }
318 properties.add(IterableProperty<String>('mode', flags));
319 }
320}
321
322class _SliverFillRemainingWithScrollable extends SingleChildRenderObjectWidget {
323 const _SliverFillRemainingWithScrollable({
324 super.child,
325 });
326
327 @override
328 RenderSliverFillRemainingWithScrollable createRenderObject(BuildContext context) => RenderSliverFillRemainingWithScrollable();
329}
330
331class _SliverFillRemainingWithoutScrollable extends SingleChildRenderObjectWidget {
332 const _SliverFillRemainingWithoutScrollable({
333 super.child,
334 });
335
336 @override
337 RenderSliverFillRemaining createRenderObject(BuildContext context) => RenderSliverFillRemaining();
338}
339
340class _SliverFillRemainingAndOverscroll extends SingleChildRenderObjectWidget {
341 const _SliverFillRemainingAndOverscroll({
342 super.child,
343 });
344
345 @override
346 RenderSliverFillRemainingAndOverscroll createRenderObject(BuildContext context) => RenderSliverFillRemainingAndOverscroll();
347}
348