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 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/rendering.dart'; |
7 | |
8 | import 'framework.dart'; |
9 | import 'scroll_delegate.dart'; |
10 | import '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. |
29 | class 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 | |
73 | class _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 | |
93 | class _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 | |
112 | class _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. |
256 | class 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 | |
322 | class _SliverFillRemainingWithScrollable extends SingleChildRenderObjectWidget { |
323 | const _SliverFillRemainingWithScrollable({ |
324 | super.child, |
325 | }); |
326 | |
327 | @override |
328 | RenderSliverFillRemainingWithScrollable createRenderObject(BuildContext context) => RenderSliverFillRemainingWithScrollable(); |
329 | } |
330 | |
331 | class _SliverFillRemainingWithoutScrollable extends SingleChildRenderObjectWidget { |
332 | const _SliverFillRemainingWithoutScrollable({ |
333 | super.child, |
334 | }); |
335 | |
336 | @override |
337 | RenderSliverFillRemaining createRenderObject(BuildContext context) => RenderSliverFillRemaining(); |
338 | } |
339 | |
340 | class _SliverFillRemainingAndOverscroll extends SingleChildRenderObjectWidget { |
341 | const _SliverFillRemainingAndOverscroll({ |
342 | super.child, |
343 | }); |
344 | |
345 | @override |
346 | RenderSliverFillRemainingAndOverscroll createRenderObject(BuildContext context) => RenderSliverFillRemainingAndOverscroll(); |
347 | } |
348 | |