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 'dart:math' as math;
6
7import 'package:flutter/foundation.dart';
8
9import 'box.dart';
10import 'object.dart';
11import 'sliver.dart';
12import 'sliver_multi_box_adaptor.dart';
13
14/// Describes the placement of a child in a [RenderSliverGrid].
15///
16/// This class is similar to [Rect], in that it gives a two-dimensional position
17/// and a two-dimensional dimension, but is direction-agnostic.
18///
19/// {@tool dartpad}
20/// This example shows how a custom [SliverGridLayout] uses [SliverGridGeometry]
21/// to lay out the children.
22///
23/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart **
24/// {@end-tool}
25///
26/// See also:
27///
28/// * [SliverGridLayout], which represents the geometry of all the tiles in a
29/// grid.
30/// * [SliverGridLayout.getGeometryForChildIndex], which returns this object
31/// to describe the child's placement.
32/// * [RenderSliverGrid], which uses this class during its
33/// [RenderSliverGrid.performLayout] method.
34@immutable
35class SliverGridGeometry {
36 /// Creates an object that describes the placement of a child in a [RenderSliverGrid].
37 const SliverGridGeometry({
38 required this.scrollOffset,
39 required this.crossAxisOffset,
40 required this.mainAxisExtent,
41 required this.crossAxisExtent,
42 });
43
44 /// The scroll offset of the leading edge of the child relative to the leading
45 /// edge of the parent.
46 final double scrollOffset;
47
48 /// The offset of the child in the non-scrolling axis.
49 ///
50 /// If the scroll axis is vertical, this offset is from the left-most edge of
51 /// the parent to the left-most edge of the child. If the scroll axis is
52 /// horizontal, this offset is from the top-most edge of the parent to the
53 /// top-most edge of the child.
54 final double crossAxisOffset;
55
56 /// The extent of the child in the scrolling axis.
57 ///
58 /// If the scroll axis is vertical, this extent is the child's height. If the
59 /// scroll axis is horizontal, this extent is the child's width.
60 final double mainAxisExtent;
61
62 /// The extent of the child in the non-scrolling axis.
63 ///
64 /// If the scroll axis is vertical, this extent is the child's width. If the
65 /// scroll axis is horizontal, this extent is the child's height.
66 final double crossAxisExtent;
67
68 /// The scroll offset of the trailing edge of the child relative to the
69 /// leading edge of the parent.
70 double get trailingScrollOffset => scrollOffset + mainAxisExtent;
71
72 /// Returns a tight [BoxConstraints] that forces the child to have the
73 /// required size, given a [SliverConstraints].
74 BoxConstraints getBoxConstraints(SliverConstraints constraints) {
75 return constraints.asBoxConstraints(
76 minExtent: mainAxisExtent,
77 maxExtent: mainAxisExtent,
78 crossAxisExtent: crossAxisExtent,
79 );
80 }
81
82 @override
83 String toString() {
84 final List<String> properties = <String>[
85 'scrollOffset: $scrollOffset',
86 'crossAxisOffset: $crossAxisOffset',
87 'mainAxisExtent: $mainAxisExtent',
88 'crossAxisExtent: $crossAxisExtent',
89 ];
90 return 'SliverGridGeometry(${properties.join(', ')})';
91 }
92}
93
94/// The size and position of all the tiles in a [RenderSliverGrid].
95///
96/// Rather that providing a grid with a [SliverGridLayout] directly, the grid is
97/// provided a [SliverGridDelegate], which computes a [SliverGridLayout] given a
98/// set of [SliverConstraints]. This allows the algorithm to dynamically respond
99/// to changes in the environment (e.g. the user rotating the device).
100///
101/// The tiles can be placed arbitrarily, but it is more efficient to place tiles
102/// roughly in order by scroll offset because grids reify a contiguous sequence
103/// of children.
104///
105/// {@tool dartpad}
106/// This example shows how to construct a custom [SliverGridLayout] to lay tiles
107/// in a grid form with some cells stretched to fit the entire width of the
108/// grid (sometimes called "hero tiles").
109///
110/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart **
111/// {@end-tool}
112///
113/// See also:
114///
115/// * [SliverGridRegularTileLayout], which represents a layout that uses
116/// equally sized and spaced tiles.
117/// * [SliverGridGeometry], which represents the size and position of a single
118/// tile in a grid.
119/// * [SliverGridDelegate.getLayout], which returns this object to describe the
120/// delegate's layout.
121/// * [RenderSliverGrid], which uses this class during its
122/// [RenderSliverGrid.performLayout] method.
123@immutable
124abstract class SliverGridLayout {
125 /// Abstract const constructor. This constructor enables subclasses to provide
126 /// const constructors so that they can be used in const expressions.
127 const SliverGridLayout();
128
129 /// The minimum child index that intersects with (or is after) this scroll offset.
130 int getMinChildIndexForScrollOffset(double scrollOffset);
131
132 /// The maximum child index that intersects with (or is before) this scroll offset.
133 int getMaxChildIndexForScrollOffset(double scrollOffset);
134
135 /// The size and position of the child with the given index.
136 SliverGridGeometry getGeometryForChildIndex(int index);
137
138 /// The scroll extent needed to fully display all the tiles if there are
139 /// `childCount` children in total.
140 ///
141 /// The child count will never be null.
142 double computeMaxScrollOffset(int childCount);
143}
144
145/// A [SliverGridLayout] that uses equally sized and spaced tiles.
146///
147/// Rather that providing a grid with a [SliverGridLayout] directly, you instead
148/// provide the grid a [SliverGridDelegate], which can compute a
149/// [SliverGridLayout] given the current [SliverConstraints].
150///
151/// This layout is used by [SliverGridDelegateWithFixedCrossAxisCount] and
152/// [SliverGridDelegateWithMaxCrossAxisExtent].
153///
154/// See also:
155///
156/// * [SliverGridDelegateWithFixedCrossAxisCount], which uses this layout.
157/// * [SliverGridDelegateWithMaxCrossAxisExtent], which uses this layout.
158/// * [SliverGridLayout], which represents an arbitrary tile layout.
159/// * [SliverGridGeometry], which represents the size and position of a single
160/// tile in a grid.
161/// * [SliverGridDelegate.getLayout], which returns this object to describe the
162/// delegate's layout.
163/// * [RenderSliverGrid], which uses this class during its
164/// [RenderSliverGrid.performLayout] method.
165class SliverGridRegularTileLayout extends SliverGridLayout {
166 /// Creates a layout that uses equally sized and spaced tiles.
167 ///
168 /// All of the arguments must not be negative. The `crossAxisCount` argument
169 /// must be greater than zero.
170 const SliverGridRegularTileLayout({
171 required this.crossAxisCount,
172 required this.mainAxisStride,
173 required this.crossAxisStride,
174 required this.childMainAxisExtent,
175 required this.childCrossAxisExtent,
176 required this.reverseCrossAxis,
177 }) : assert(crossAxisCount > 0),
178 assert(mainAxisStride >= 0),
179 assert(crossAxisStride >= 0),
180 assert(childMainAxisExtent >= 0),
181 assert(childCrossAxisExtent >= 0);
182
183 /// The number of children in the cross axis.
184 final int crossAxisCount;
185
186 /// The number of pixels from the leading edge of one tile to the leading edge
187 /// of the next tile in the main axis.
188 final double mainAxisStride;
189
190 /// The number of pixels from the leading edge of one tile to the leading edge
191 /// of the next tile in the cross axis.
192 final double crossAxisStride;
193
194 /// The number of pixels from the leading edge of one tile to the trailing
195 /// edge of the same tile in the main axis.
196 final double childMainAxisExtent;
197
198 /// The number of pixels from the leading edge of one tile to the trailing
199 /// edge of the same tile in the cross axis.
200 final double childCrossAxisExtent;
201
202 /// Whether the children should be placed in the opposite order of increasing
203 /// coordinates in the cross axis.
204 ///
205 /// For example, if the cross axis is horizontal, the children are placed from
206 /// left to right when [reverseCrossAxis] is false and from right to left when
207 /// [reverseCrossAxis] is true.
208 ///
209 /// Typically set to the return value of [axisDirectionIsReversed] applied to
210 /// the [SliverConstraints.crossAxisDirection].
211 final bool reverseCrossAxis;
212
213 @override
214 int getMinChildIndexForScrollOffset(double scrollOffset) {
215 return mainAxisStride > precisionErrorTolerance ? crossAxisCount * (scrollOffset ~/ mainAxisStride) : 0;
216 }
217
218 @override
219 int getMaxChildIndexForScrollOffset(double scrollOffset) {
220 if (mainAxisStride > 0.0) {
221 final int mainAxisCount = (scrollOffset / mainAxisStride).ceil();
222 return math.max(0, crossAxisCount * mainAxisCount - 1);
223 }
224 return 0;
225 }
226
227 double _getOffsetFromStartInCrossAxis(double crossAxisStart) {
228 if (reverseCrossAxis) {
229 return crossAxisCount * crossAxisStride - crossAxisStart - childCrossAxisExtent - (crossAxisStride - childCrossAxisExtent);
230 }
231 return crossAxisStart;
232 }
233
234 @override
235 SliverGridGeometry getGeometryForChildIndex(int index) {
236 final double crossAxisStart = (index % crossAxisCount) * crossAxisStride;
237 return SliverGridGeometry(
238 scrollOffset: (index ~/ crossAxisCount) * mainAxisStride,
239 crossAxisOffset: _getOffsetFromStartInCrossAxis(crossAxisStart),
240 mainAxisExtent: childMainAxisExtent,
241 crossAxisExtent: childCrossAxisExtent,
242 );
243 }
244
245 @override
246 double computeMaxScrollOffset(int childCount) {
247 if (childCount == 0) {
248 // There are no children in the grid. The max scroll offset should be
249 // zero.
250 return 0.0;
251 }
252 final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1;
253 final double mainAxisSpacing = mainAxisStride - childMainAxisExtent;
254 return mainAxisStride * mainAxisCount - mainAxisSpacing;
255 }
256}
257
258/// Controls the layout of tiles in a grid.
259///
260/// Given the current constraints on the grid, a [SliverGridDelegate] computes
261/// the layout for the tiles in the grid. The tiles can be placed arbitrarily,
262/// but it is more efficient to place tiles roughly in order by scroll offset
263/// because grids reify a contiguous sequence of children.
264///
265/// {@tool dartpad}
266/// This example shows how a [SliverGridDelegate] returns a [SliverGridLayout]
267/// configured based on the provided [SliverConstraints] in [getLayout].
268///
269/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart **
270/// {@end-tool}
271///
272/// See also:
273///
274/// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
275/// a fixed number of tiles in the cross axis.
276/// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
277/// tiles that have a maximum cross-axis extent.
278/// * [GridView], which uses this delegate to control the layout of its tiles.
279/// * [SliverGrid], which uses this delegate to control the layout of its
280/// tiles.
281/// * [RenderSliverGrid], which uses this delegate to control the layout of its
282/// tiles.
283abstract class SliverGridDelegate {
284 /// Abstract const constructor. This constructor enables subclasses to provide
285 /// const constructors so that they can be used in const expressions.
286 const SliverGridDelegate();
287
288 /// Returns information about the size and position of the tiles in the grid.
289 SliverGridLayout getLayout(SliverConstraints constraints);
290
291 /// Override this method to return true when the children need to be
292 /// laid out.
293 ///
294 /// This should compare the fields of the current delegate and the given
295 /// `oldDelegate` and return true if the fields are such that the layout would
296 /// be different.
297 bool shouldRelayout(covariant SliverGridDelegate oldDelegate);
298}
299
300/// Creates grid layouts with a fixed number of tiles in the cross axis.
301///
302/// For example, if the grid is vertical, this delegate will create a layout
303/// with a fixed number of columns. If the grid is horizontal, this delegate
304/// will create a layout with a fixed number of rows.
305///
306/// This delegate creates grids with equally sized and spaced tiles.
307///
308/// {@tool dartpad}
309/// Here is an example using the [childAspectRatio] property. On a device with a
310/// screen width of 800.0, it creates a GridView with each tile with a width of
311/// 200.0 and a height of 100.0.
312///
313/// ** See code in examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0.dart **
314/// {@end-tool}
315///
316/// {@tool dartpad}
317/// Here is an example using the [mainAxisExtent] property. On a device with a
318/// screen width of 800.0, it creates a GridView with each tile with a width of
319/// 200.0 and a height of 150.0.
320///
321/// ** See code in examples/api/lib/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1.dart **
322/// {@end-tool}
323///
324/// See also:
325///
326/// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
327/// tiles that have a maximum cross-axis extent.
328/// * [SliverGridDelegate], which creates arbitrary layouts.
329/// * [GridView], which can use this delegate to control the layout of its
330/// tiles.
331/// * [SliverGrid], which can use this delegate to control the layout of its
332/// tiles.
333/// * [RenderSliverGrid], which can use this delegate to control the layout of
334/// its tiles.
335class SliverGridDelegateWithFixedCrossAxisCount extends SliverGridDelegate {
336 /// Creates a delegate that makes grid layouts with a fixed number of tiles in
337 /// the cross axis.
338 ///
339 /// The `mainAxisSpacing`, `mainAxisExtent` and `crossAxisSpacing` arguments
340 /// must not be negative. The `crossAxisCount` and `childAspectRatio`
341 /// arguments must be greater than zero.
342 const SliverGridDelegateWithFixedCrossAxisCount({
343 required this.crossAxisCount,
344 this.mainAxisSpacing = 0.0,
345 this.crossAxisSpacing = 0.0,
346 this.childAspectRatio = 1.0,
347 this.mainAxisExtent,
348 }) : assert(crossAxisCount > 0),
349 assert(mainAxisSpacing >= 0),
350 assert(crossAxisSpacing >= 0),
351 assert(childAspectRatio > 0);
352
353 /// The number of children in the cross axis.
354 final int crossAxisCount;
355
356 /// The number of logical pixels between each child along the main axis.
357 final double mainAxisSpacing;
358
359 /// The number of logical pixels between each child along the cross axis.
360 final double crossAxisSpacing;
361
362 /// The ratio of the cross-axis to the main-axis extent of each child.
363 final double childAspectRatio;
364
365 /// The extent of each tile in the main axis. If provided it would define the
366 /// logical pixels taken by each tile in the main-axis.
367 ///
368 /// If null, [childAspectRatio] is used instead.
369 final double? mainAxisExtent;
370
371 bool _debugAssertIsValid() {
372 assert(crossAxisCount > 0);
373 assert(mainAxisSpacing >= 0.0);
374 assert(crossAxisSpacing >= 0.0);
375 assert(childAspectRatio > 0.0);
376 return true;
377 }
378
379 @override
380 SliverGridLayout getLayout(SliverConstraints constraints) {
381 assert(_debugAssertIsValid());
382 final double usableCrossAxisExtent = math.max(
383 0.0,
384 constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
385 );
386 final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
387 final double childMainAxisExtent = mainAxisExtent ?? childCrossAxisExtent / childAspectRatio;
388 return SliverGridRegularTileLayout(
389 crossAxisCount: crossAxisCount,
390 mainAxisStride: childMainAxisExtent + mainAxisSpacing,
391 crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
392 childMainAxisExtent: childMainAxisExtent,
393 childCrossAxisExtent: childCrossAxisExtent,
394 reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
395 );
396 }
397
398 @override
399 bool shouldRelayout(SliverGridDelegateWithFixedCrossAxisCount oldDelegate) {
400 return oldDelegate.crossAxisCount != crossAxisCount
401 || oldDelegate.mainAxisSpacing != mainAxisSpacing
402 || oldDelegate.crossAxisSpacing != crossAxisSpacing
403 || oldDelegate.childAspectRatio != childAspectRatio
404 || oldDelegate.mainAxisExtent != mainAxisExtent;
405 }
406}
407
408/// Creates grid layouts with tiles that each have a maximum cross-axis extent.
409///
410/// This delegate will select a cross-axis extent for the tiles that is as
411/// large as possible subject to the following conditions:
412///
413/// - The extent evenly divides the cross-axis extent of the grid.
414/// - The extent is at most [maxCrossAxisExtent].
415///
416/// For example, if the grid is vertical, the grid is 500.0 pixels wide, and
417/// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4
418/// columns that are 125.0 pixels wide.
419///
420/// This delegate creates grids with equally sized and spaced tiles.
421///
422/// See also:
423///
424/// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
425/// a fixed number of tiles in the cross axis.
426/// * [SliverGridDelegate], which creates arbitrary layouts.
427/// * [GridView], which can use this delegate to control the layout of its
428/// tiles.
429/// * [SliverGrid], which can use this delegate to control the layout of its
430/// tiles.
431/// * [RenderSliverGrid], which can use this delegate to control the layout of
432/// its tiles.
433class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate {
434 /// Creates a delegate that makes grid layouts with tiles that have a maximum
435 /// cross-axis extent.
436 ///
437 /// The [maxCrossAxisExtent], [mainAxisExtent], [mainAxisSpacing],
438 /// and [crossAxisSpacing] arguments must not be negative.
439 /// The [childAspectRatio] argument must be greater than zero.
440 const SliverGridDelegateWithMaxCrossAxisExtent({
441 required this.maxCrossAxisExtent,
442 this.mainAxisSpacing = 0.0,
443 this.crossAxisSpacing = 0.0,
444 this.childAspectRatio = 1.0,
445 this.mainAxisExtent,
446 }) : assert(maxCrossAxisExtent > 0),
447 assert(mainAxisSpacing >= 0),
448 assert(crossAxisSpacing >= 0),
449 assert(childAspectRatio > 0);
450
451 /// The maximum extent of tiles in the cross axis.
452 ///
453 /// This delegate will select a cross-axis extent for the tiles that is as
454 /// large as possible subject to the following conditions:
455 ///
456 /// - The extent evenly divides the cross-axis extent of the grid.
457 /// - The extent is at most [maxCrossAxisExtent].
458 ///
459 /// For example, if the grid is vertical, the grid is 500.0 pixels wide, and
460 /// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4
461 /// columns that are 125.0 pixels wide.
462 final double maxCrossAxisExtent;
463
464 /// The number of logical pixels between each child along the main axis.
465 final double mainAxisSpacing;
466
467 /// The number of logical pixels between each child along the cross axis.
468 final double crossAxisSpacing;
469
470 /// The ratio of the cross-axis to the main-axis extent of each child.
471 final double childAspectRatio;
472
473 /// The extent of each tile in the main axis. If provided it would define the
474 /// logical pixels taken by each tile in the main-axis.
475 ///
476 /// If null, [childAspectRatio] is used instead.
477 final double? mainAxisExtent;
478
479 bool _debugAssertIsValid(double crossAxisExtent) {
480 assert(crossAxisExtent > 0.0);
481 assert(maxCrossAxisExtent > 0.0);
482 assert(mainAxisSpacing >= 0.0);
483 assert(crossAxisSpacing >= 0.0);
484 assert(childAspectRatio > 0.0);
485 return true;
486 }
487
488 @override
489 SliverGridLayout getLayout(SliverConstraints constraints) {
490 assert(_debugAssertIsValid(constraints.crossAxisExtent));
491 int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil();
492 // Ensure a minimum count of 1, can be zero and result in an infinite extent
493 // below when the window size is 0.
494 crossAxisCount = math.max(1, crossAxisCount);
495 final double usableCrossAxisExtent = math.max(
496 0.0,
497 constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
498 );
499 final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
500 final double childMainAxisExtent = mainAxisExtent ?? childCrossAxisExtent / childAspectRatio;
501 return SliverGridRegularTileLayout(
502 crossAxisCount: crossAxisCount,
503 mainAxisStride: childMainAxisExtent + mainAxisSpacing,
504 crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
505 childMainAxisExtent: childMainAxisExtent,
506 childCrossAxisExtent: childCrossAxisExtent,
507 reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
508 );
509 }
510
511 @override
512 bool shouldRelayout(SliverGridDelegateWithMaxCrossAxisExtent oldDelegate) {
513 return oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent
514 || oldDelegate.mainAxisSpacing != mainAxisSpacing
515 || oldDelegate.crossAxisSpacing != crossAxisSpacing
516 || oldDelegate.childAspectRatio != childAspectRatio
517 || oldDelegate.mainAxisExtent != mainAxisExtent;
518 }
519}
520
521/// Parent data structure used by [RenderSliverGrid].
522class SliverGridParentData extends SliverMultiBoxAdaptorParentData {
523 /// The offset of the child in the non-scrolling axis.
524 ///
525 /// If the scroll axis is vertical, this offset is from the left-most edge of
526 /// the parent to the left-most edge of the child. If the scroll axis is
527 /// horizontal, this offset is from the top-most edge of the parent to the
528 /// top-most edge of the child.
529 double? crossAxisOffset;
530
531 @override
532 String toString() => 'crossAxisOffset=$crossAxisOffset; ${super.toString()}';
533}
534
535/// A sliver that places multiple box children in a two dimensional arrangement.
536///
537/// [RenderSliverGrid] places its children in arbitrary positions determined by
538/// [gridDelegate]. Each child is forced to have the size specified by the
539/// [gridDelegate].
540///
541/// See also:
542///
543/// * [RenderSliverList], which places its children in a linear
544/// array.
545/// * [RenderSliverFixedExtentList], which places its children in a linear
546/// array with a fixed extent in the main axis.
547class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
548 /// Creates a sliver that contains multiple box children that whose size and
549 /// position are determined by a delegate.
550 RenderSliverGrid({
551 required super.childManager,
552 required SliverGridDelegate gridDelegate,
553 }) : _gridDelegate = gridDelegate;
554
555 @override
556 void setupParentData(RenderObject child) {
557 if (child.parentData is! SliverGridParentData) {
558 child.parentData = SliverGridParentData();
559 }
560 }
561
562 /// The delegate that controls the size and position of the children.
563 SliverGridDelegate get gridDelegate => _gridDelegate;
564 SliverGridDelegate _gridDelegate;
565 set gridDelegate(SliverGridDelegate value) {
566 if (_gridDelegate == value) {
567 return;
568 }
569 if (value.runtimeType != _gridDelegate.runtimeType ||
570 value.shouldRelayout(_gridDelegate)) {
571 markNeedsLayout();
572 }
573 _gridDelegate = value;
574 }
575
576 @override
577 double childCrossAxisPosition(RenderBox child) {
578 final SliverGridParentData childParentData = child.parentData! as SliverGridParentData;
579 return childParentData.crossAxisOffset!;
580 }
581
582 @override
583 void performLayout() {
584 final SliverConstraints constraints = this.constraints;
585 childManager.didStartLayout();
586 childManager.setDidUnderflow(false);
587
588 final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
589 assert(scrollOffset >= 0.0);
590 final double remainingExtent = constraints.remainingCacheExtent;
591 assert(remainingExtent >= 0.0);
592 final double targetEndScrollOffset = scrollOffset + remainingExtent;
593
594 final SliverGridLayout layout = _gridDelegate.getLayout(constraints);
595
596 final int firstIndex = layout.getMinChildIndexForScrollOffset(scrollOffset);
597 final int? targetLastIndex = targetEndScrollOffset.isFinite ?
598 layout.getMaxChildIndexForScrollOffset(targetEndScrollOffset) : null;
599 if (firstChild != null) {
600 final int leadingGarbage = _calculateLeadingGarbage(firstIndex);
601 final int trailingGarbage = targetLastIndex != null ? _calculateTrailingGarbage(targetLastIndex) : 0;
602 collectGarbage(leadingGarbage, trailingGarbage);
603 } else {
604 collectGarbage(0, 0);
605 }
606
607 final SliverGridGeometry firstChildGridGeometry = layout.getGeometryForChildIndex(firstIndex);
608
609 if (firstChild == null) {
610 if (!addInitialChild(index: firstIndex, layoutOffset: firstChildGridGeometry.scrollOffset)) {
611 // There are either no children, or we are past the end of all our children.
612 final double max = layout.computeMaxScrollOffset(childManager.childCount);
613 geometry = SliverGeometry(
614 scrollExtent: max,
615 maxPaintExtent: max,
616 );
617 childManager.didFinishLayout();
618 return;
619 }
620 }
621
622 final double leadingScrollOffset = firstChildGridGeometry.scrollOffset;
623 double trailingScrollOffset = firstChildGridGeometry.trailingScrollOffset;
624 RenderBox? trailingChildWithLayout;
625 bool reachedEnd = false;
626
627 for (int index = indexOf(firstChild!) - 1; index >= firstIndex; --index) {
628 final SliverGridGeometry gridGeometry = layout.getGeometryForChildIndex(index);
629 final RenderBox child = insertAndLayoutLeadingChild(
630 gridGeometry.getBoxConstraints(constraints),
631 )!;
632 final SliverGridParentData childParentData = child.parentData! as SliverGridParentData;
633 childParentData.layoutOffset = gridGeometry.scrollOffset;
634 childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
635 assert(childParentData.index == index);
636 trailingChildWithLayout ??= child;
637 trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
638 }
639
640 if (trailingChildWithLayout == null) {
641 firstChild!.layout(firstChildGridGeometry.getBoxConstraints(constraints));
642 final SliverGridParentData childParentData = firstChild!.parentData! as SliverGridParentData;
643 childParentData.layoutOffset = firstChildGridGeometry.scrollOffset;
644 childParentData.crossAxisOffset = firstChildGridGeometry.crossAxisOffset;
645 trailingChildWithLayout = firstChild;
646 }
647
648 for (int index = indexOf(trailingChildWithLayout!) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) {
649 final SliverGridGeometry gridGeometry = layout.getGeometryForChildIndex(index);
650 final BoxConstraints childConstraints = gridGeometry.getBoxConstraints(constraints);
651 RenderBox? child = childAfter(trailingChildWithLayout!);
652 if (child == null || indexOf(child) != index) {
653 child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout);
654 if (child == null) {
655 reachedEnd = true;
656 // We have run out of children.
657 break;
658 }
659 } else {
660 child.layout(childConstraints);
661 }
662 trailingChildWithLayout = child;
663 final SliverGridParentData childParentData = child.parentData! as SliverGridParentData;
664 childParentData.layoutOffset = gridGeometry.scrollOffset;
665 childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
666 assert(childParentData.index == index);
667 trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
668 }
669
670 final int lastIndex = indexOf(lastChild!);
671
672 assert(debugAssertChildListIsNonEmptyAndContiguous());
673 assert(indexOf(firstChild!) == firstIndex);
674 assert(targetLastIndex == null || lastIndex <= targetLastIndex);
675
676 final double estimatedTotalExtent = reachedEnd
677 ? trailingScrollOffset
678 : childManager.estimateMaxScrollOffset(
679 constraints,
680 firstIndex: firstIndex,
681 lastIndex: lastIndex,
682 leadingScrollOffset: leadingScrollOffset,
683 trailingScrollOffset: trailingScrollOffset,
684 );
685 final double paintExtent = calculatePaintOffset(
686 constraints,
687 from: math.min(constraints.scrollOffset, leadingScrollOffset),
688 to: trailingScrollOffset,
689 );
690 final double cacheExtent = calculateCacheOffset(
691 constraints,
692 from: leadingScrollOffset,
693 to: trailingScrollOffset,
694 );
695
696 geometry = SliverGeometry(
697 scrollExtent: estimatedTotalExtent,
698 paintExtent: paintExtent,
699 maxPaintExtent: estimatedTotalExtent,
700 cacheExtent: cacheExtent,
701 hasVisualOverflow: estimatedTotalExtent > paintExtent || constraints.scrollOffset > 0.0 || constraints.overlap != 0.0,
702 );
703
704 // We may have started the layout while scrolled to the end, which
705 // would not expose a new child.
706 if (estimatedTotalExtent == trailingScrollOffset) {
707 childManager.setDidUnderflow(true);
708 }
709 childManager.didFinishLayout();
710 }
711
712 int _calculateLeadingGarbage(int firstIndex) {
713 RenderBox? walker = firstChild;
714 int leadingGarbage = 0;
715 while (walker != null && indexOf(walker) < firstIndex) {
716 leadingGarbage += 1;
717 walker = childAfter(walker);
718 }
719 return leadingGarbage;
720 }
721
722 int _calculateTrailingGarbage(int targetLastIndex) {
723 RenderBox? walker = lastChild;
724 int trailingGarbage = 0;
725 while (walker != null && indexOf(walker) > targetLastIndex) {
726 trailingGarbage += 1;
727 walker = childBefore(walker);
728 }
729 return trailingGarbage;
730 }
731}
732