| 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({this.viewportFraction = 0, Widget? sliver}) |
| 101 | : assert(viewportFraction >= 0), |
| 102 | assert(viewportFraction <= 0.5), |
| 103 | super(child: sliver); |
| 104 | |
| 105 | final double viewportFraction; |
| 106 | |
| 107 | @override |
| 108 | RenderObject createRenderObject(BuildContext context) => |
| 109 | _RenderSliverFractionalPadding(viewportFraction: viewportFraction); |
| 110 | |
| 111 | @override |
| 112 | void updateRenderObject(BuildContext context, _RenderSliverFractionalPadding renderObject) { |
| 113 | renderObject.viewportFraction = viewportFraction; |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | class _RenderSliverFractionalPadding extends RenderSliverEdgeInsetsPadding { |
| 118 | _RenderSliverFractionalPadding({double viewportFraction = 0}) |
| 119 | : assert(viewportFraction <= 0.5), |
| 120 | assert(viewportFraction >= 0), |
| 121 | _viewportFraction = viewportFraction; |
| 122 | |
| 123 | SliverConstraints? _lastResolvedConstraints; |
| 124 | |
| 125 | double get viewportFraction => _viewportFraction; |
| 126 | double _viewportFraction; |
| 127 | set viewportFraction(double newValue) { |
| 128 | if (_viewportFraction == newValue) { |
| 129 | return; |
| 130 | } |
| 131 | _viewportFraction = newValue; |
| 132 | _markNeedsResolution(); |
| 133 | } |
| 134 | |
| 135 | @override |
| 136 | EdgeInsets? get resolvedPadding => _resolvedPadding; |
| 137 | EdgeInsets? _resolvedPadding; |
| 138 | |
| 139 | void _markNeedsResolution() { |
| 140 | _resolvedPadding = null; |
| 141 | markNeedsLayout(); |
| 142 | } |
| 143 | |
| 144 | void _resolve() { |
| 145 | if (_resolvedPadding != null && _lastResolvedConstraints == constraints) { |
| 146 | return; |
| 147 | } |
| 148 | |
| 149 | final double paddingValue = constraints.viewportMainAxisExtent * viewportFraction; |
| 150 | _lastResolvedConstraints = constraints; |
| 151 | _resolvedPadding = switch (constraints.axis) { |
| 152 | Axis.horizontal => EdgeInsets.symmetric(horizontal: paddingValue), |
| 153 | Axis.vertical => EdgeInsets.symmetric(vertical: paddingValue), |
| 154 | }; |
| 155 | |
| 156 | return; |
| 157 | } |
| 158 | |
| 159 | @override |
| 160 | void performLayout() { |
| 161 | _resolve(); |
| 162 | super.performLayout(); |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | /// A sliver that contains a single box child that fills the remaining space in |
| 167 | /// the viewport. |
| 168 | /// |
| 169 | /// _To learn more about slivers, see [CustomScrollView.slivers]._ |
| 170 | /// |
| 171 | /// [SliverFillRemaining] will size its [child] to fill the viewport in the |
| 172 | /// cross axis. The extent of the sliver and its child's size in the main axis |
| 173 | /// is computed conditionally, described in further detail below. |
| 174 | /// |
| 175 | /// Typically this will be the last sliver in a viewport, since (by definition) |
| 176 | /// there is never any room for anything beyond this sliver. |
| 177 | /// |
| 178 | /// ## Main Axis Extent |
| 179 | /// |
| 180 | /// ### When [SliverFillRemaining] has a scrollable child |
| 181 | /// |
| 182 | /// The [hasScrollBody] flag indicates whether the sliver's child has a |
| 183 | /// scrollable body. This value is never null, and defaults to true. A common |
| 184 | /// example of this use is a [NestedScrollView]. In this case, the sliver will |
| 185 | /// size its child to fill the maximum available extent. [SliverFillRemaining] |
| 186 | /// will not constrain the scrollable area, as it could potentially have an |
| 187 | /// infinite depth. This is also true for use cases such as a [ScrollView] when |
| 188 | /// [ScrollView.shrinkWrap] is true. |
| 189 | /// |
| 190 | /// ### When [SliverFillRemaining] does not have a scrollable child |
| 191 | /// |
| 192 | /// When [hasScrollBody] is set to false, the child's size is taken into account |
| 193 | /// when considering the extent to which it should fill the space. The extent to |
| 194 | /// which the preceding slivers have been scrolled is also taken into |
| 195 | /// account in deciding how to layout this sliver. |
| 196 | /// |
| 197 | /// [SliverFillRemaining] will size its [child] to fill the viewport in the |
| 198 | /// main axis if that space is larger than the child's extent, and the amount |
| 199 | /// of space that has been scrolled beforehand has not exceeded the main axis |
| 200 | /// extent of the viewport. |
| 201 | /// |
| 202 | /// {@tool dartpad} |
| 203 | /// In this sample the [SliverFillRemaining] sizes its [child] to fill the |
| 204 | /// remaining extent of the viewport in both axes. The icon is centered in the |
| 205 | /// sliver, and would be in any computed extent for the sliver. |
| 206 | /// |
| 207 | /// ** See code in examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.0.dart ** |
| 208 | /// {@end-tool} |
| 209 | /// |
| 210 | /// [SliverFillRemaining] will defer to the size of its [child] if the |
| 211 | /// child's size exceeds the remaining space in the viewport. |
| 212 | /// |
| 213 | /// {@tool dartpad} |
| 214 | /// In this sample the [SliverFillRemaining] defers to the size of its [child] |
| 215 | /// because the child's extent exceeds that of the remaining extent of the |
| 216 | /// viewport's main axis. |
| 217 | /// |
| 218 | /// ** See code in examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.1.dart ** |
| 219 | /// {@end-tool} |
| 220 | /// |
| 221 | /// [SliverFillRemaining] will defer to the size of its [child] if the |
| 222 | /// [SliverConstraints.precedingScrollExtent] exceeded the length of the viewport's main axis. |
| 223 | /// |
| 224 | /// {@tool dartpad} |
| 225 | /// In this sample the [SliverFillRemaining] defers to the size of its [child] |
| 226 | /// because the [SliverConstraints.precedingScrollExtent] has gone |
| 227 | /// beyond that of the viewport's main axis. |
| 228 | /// |
| 229 | /// ** See code in examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.2.dart ** |
| 230 | /// {@end-tool} |
| 231 | /// |
| 232 | /// For [ScrollPhysics] that allow overscroll, such as |
| 233 | /// [BouncingScrollPhysics], setting the [fillOverscroll] flag to true allows |
| 234 | /// the size of the [child] to _stretch_, filling the overscroll area. It does |
| 235 | /// this regardless of the path chosen to provide the child's size. |
| 236 | /// |
| 237 | /// {@animation 250 500 https://flutter.github.io/assets-for-api-docs/assets/widgets/sliver_fill_remaining_fill_overscroll.mp4} |
| 238 | /// |
| 239 | /// {@tool dartpad} |
| 240 | /// In this sample the [SliverFillRemaining]'s child stretches to fill the |
| 241 | /// overscroll area when [fillOverscroll] is true. This sample also features a |
| 242 | /// button that is pinned to the bottom of the sliver, regardless of size or |
| 243 | /// overscroll behavior. Try switching [fillOverscroll] to see the difference. |
| 244 | /// |
| 245 | /// This sample only shows the overscroll behavior on devices that support |
| 246 | /// overscroll. |
| 247 | /// |
| 248 | /// ** See code in examples/api/lib/widgets/sliver_fill/sliver_fill_remaining.3.dart ** |
| 249 | /// {@end-tool} |
| 250 | /// |
| 251 | /// |
| 252 | /// See also: |
| 253 | /// |
| 254 | /// * [SliverFillViewport], which sizes its children based on the |
| 255 | /// size of the viewport, regardless of what else is in the scroll view. |
| 256 | /// * [SliverList], which shows a list of variable-sized children in a |
| 257 | /// viewport. |
| 258 | class SliverFillRemaining extends StatelessWidget { |
| 259 | /// Creates a sliver that fills the remaining space in the viewport. |
| 260 | const SliverFillRemaining({ |
| 261 | super.key, |
| 262 | this.child, |
| 263 | this.hasScrollBody = true, |
| 264 | this.fillOverscroll = false, |
| 265 | }); |
| 266 | |
| 267 | /// Box child widget that fills the remaining space in the viewport. |
| 268 | /// |
| 269 | /// The main [SliverFillRemaining] documentation contains more details. |
| 270 | final Widget? child; |
| 271 | |
| 272 | /// Indicates whether the child has a scrollable body, this value cannot be |
| 273 | /// null. |
| 274 | /// |
| 275 | /// Defaults to true such that the child will extend beyond the viewport and |
| 276 | /// scroll, as seen in [NestedScrollView]. |
| 277 | /// |
| 278 | /// Setting this value to false will allow the child to fill the remainder of |
| 279 | /// the viewport and not extend further. However, if the |
| 280 | /// [SliverConstraints.precedingScrollExtent] and/or the [child]'s |
| 281 | /// extent exceeds the size of the viewport, the sliver will defer to the |
| 282 | /// child's size rather than overriding it. |
| 283 | final bool hasScrollBody; |
| 284 | |
| 285 | /// Indicates whether the child should stretch to fill the overscroll area |
| 286 | /// created by certain scroll physics, such as iOS' default scroll physics. |
| 287 | /// This flag is only relevant when [hasScrollBody] is false. |
| 288 | /// |
| 289 | /// Defaults to false, meaning that the default behavior is for the child to |
| 290 | /// maintain its size and not extend into the overscroll area. |
| 291 | final bool fillOverscroll; |
| 292 | |
| 293 | @override |
| 294 | Widget build(BuildContext context) { |
| 295 | if (hasScrollBody) { |
| 296 | return _SliverFillRemainingWithScrollable(child: child); |
| 297 | } |
| 298 | if (!fillOverscroll) { |
| 299 | return _SliverFillRemainingWithoutScrollable(child: child); |
| 300 | } |
| 301 | return _SliverFillRemainingAndOverscroll(child: child); |
| 302 | } |
| 303 | |
| 304 | @override |
| 305 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 306 | super.debugFillProperties(properties); |
| 307 | properties.add(DiagnosticsProperty<Widget>('child' , child)); |
| 308 | final List<String> flags = <String>[ |
| 309 | if (hasScrollBody) 'scrollable' , |
| 310 | if (fillOverscroll) 'fillOverscroll' , |
| 311 | ]; |
| 312 | if (flags.isEmpty) { |
| 313 | flags.add('nonscrollable' ); |
| 314 | } |
| 315 | properties.add(IterableProperty<String>('mode' , flags)); |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | class _SliverFillRemainingWithScrollable extends SingleChildRenderObjectWidget { |
| 320 | const _SliverFillRemainingWithScrollable({super.child}); |
| 321 | |
| 322 | @override |
| 323 | RenderSliverFillRemainingWithScrollable createRenderObject(BuildContext context) => |
| 324 | RenderSliverFillRemainingWithScrollable(); |
| 325 | } |
| 326 | |
| 327 | class _SliverFillRemainingWithoutScrollable extends SingleChildRenderObjectWidget { |
| 328 | const _SliverFillRemainingWithoutScrollable({super.child}); |
| 329 | |
| 330 | @override |
| 331 | RenderSliverFillRemaining createRenderObject(BuildContext context) => RenderSliverFillRemaining(); |
| 332 | } |
| 333 | |
| 334 | class _SliverFillRemainingAndOverscroll extends SingleChildRenderObjectWidget { |
| 335 | const _SliverFillRemainingAndOverscroll({super.child}); |
| 336 | |
| 337 | @override |
| 338 | RenderSliverFillRemainingAndOverscroll createRenderObject(BuildContext context) => |
| 339 | RenderSliverFillRemainingAndOverscroll(); |
| 340 | } |
| 341 | |