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 'dart:math' as math; |
6 | |
7 | import 'package:flutter/foundation.dart'; |
8 | |
9 | import 'box.dart'; |
10 | import 'object.dart'; |
11 | import 'sliver.dart'; |
12 | import '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 |
35 | class 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 |
124 | abstract 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. |
165 | class 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. |
283 | abstract 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. |
335 | class 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. |
433 | class 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]. |
522 | class 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. |
547 | class 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 | |