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