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'; |
9 | library; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | import 'package:flutter/rendering.dart'; |
13 | |
14 | import 'framework.dart'; |
15 | import 'scroll_delegate.dart'; |
16 | import '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. |
35 | class 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 | |
79 | class _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 | |
99 | class _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 | |
118 | class _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. |
260 | class 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 | |
326 | class _SliverFillRemainingWithScrollable extends SingleChildRenderObjectWidget { |
327 | const _SliverFillRemainingWithScrollable({ |
328 | super.child, |
329 | }); |
330 | |
331 | @override |
332 | RenderSliverFillRemainingWithScrollable createRenderObject(BuildContext context) => RenderSliverFillRemainingWithScrollable(); |
333 | } |
334 | |
335 | class _SliverFillRemainingWithoutScrollable extends SingleChildRenderObjectWidget { |
336 | const _SliverFillRemainingWithoutScrollable({ |
337 | super.child, |
338 | }); |
339 | |
340 | @override |
341 | RenderSliverFillRemaining createRenderObject(BuildContext context) => RenderSliverFillRemaining(); |
342 | } |
343 | |
344 | class _SliverFillRemainingAndOverscroll extends SingleChildRenderObjectWidget { |
345 | const _SliverFillRemainingAndOverscroll({ |
346 | super.child, |
347 | }); |
348 | |
349 | @override |
350 | RenderSliverFillRemainingAndOverscroll createRenderObject(BuildContext context) => RenderSliverFillRemainingAndOverscroll(); |
351 | } |
352 | |