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 'sliver_fill.dart'; |
6 | /// @docImport 'sliver_list.dart'; |
7 | library; |
8 | |
9 | import 'dart:math' as math; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | |
13 | import 'box.dart'; |
14 | import 'sliver.dart'; |
15 | import 'sliver_multi_box_adaptor.dart'; |
16 | |
17 | /// A sliver that contains multiple box children that have the explicit extent in |
18 | /// the main axis. |
19 | /// |
20 | /// [RenderSliverFixedExtentBoxAdaptor] places its children in a linear array |
21 | /// along the main axis. Each child is forced to have the returned value of [itemExtentBuilder] |
22 | /// when the [itemExtentBuilder] is non-null or the [itemExtent] when [itemExtentBuilder] |
23 | /// is null in the main axis and the [SliverConstraints.crossAxisExtent] in the cross axis. |
24 | /// |
25 | /// Subclasses should override [itemExtent] or [itemExtentBuilder] to control |
26 | /// the size of the children in the main axis. For a concrete subclass with a |
27 | /// configurable [itemExtent], see [RenderSliverFixedExtentList] or [RenderSliverVariedExtentList]. |
28 | /// |
29 | /// [RenderSliverFixedExtentBoxAdaptor] is more efficient than |
30 | /// [RenderSliverList] because [RenderSliverFixedExtentBoxAdaptor] does not need |
31 | /// to perform layout on its children to obtain their extent in the main axis. |
32 | /// |
33 | /// See also: |
34 | /// |
35 | /// * [RenderSliverFixedExtentList], which has a configurable [itemExtent]. |
36 | /// * [RenderSliverFillViewport], which determines the [itemExtent] based on |
37 | /// [SliverConstraints.viewportMainAxisExtent]. |
38 | /// * [RenderSliverFillRemaining], which determines the [itemExtent] based on |
39 | /// [SliverConstraints.remainingPaintExtent]. |
40 | /// * [RenderSliverList], which does not require its children to have the same |
41 | /// extent in the main axis. |
42 | abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { |
43 | /// Creates a sliver that contains multiple box children that have the same |
44 | /// extent in the main axis. |
45 | RenderSliverFixedExtentBoxAdaptor({ |
46 | required super.childManager, |
47 | }); |
48 | |
49 | /// The main-axis extent of each item. |
50 | /// |
51 | /// If this is non-null, the [itemExtentBuilder] must be null. |
52 | /// If this is null, the [itemExtentBuilder] must be non-null. |
53 | double? get itemExtent; |
54 | |
55 | /// The main-axis extent builder of each item. |
56 | /// |
57 | /// If this is non-null, the [itemExtent] must be null. |
58 | /// If this is null, the [itemExtent] must be non-null. |
59 | ItemExtentBuilder? get itemExtentBuilder => null; |
60 | |
61 | /// The layout offset for the child with the given index. |
62 | /// |
63 | /// This function uses the returned value of [itemExtentBuilder] or the |
64 | /// [itemExtent] to avoid recomputing item size repeatedly during layout. |
65 | /// |
66 | /// By default, places the children in order, without gaps, starting from |
67 | /// layout offset zero. |
68 | @visibleForTesting |
69 | @protected |
70 | double indexToLayoutOffset( |
71 | @Deprecated( |
72 | 'The itemExtent is already available within the scope of this function. ' |
73 | 'This feature was deprecated after v3.20.0-7.0.pre.' |
74 | ) |
75 | double itemExtent, |
76 | int index, |
77 | ) { |
78 | if (itemExtentBuilder == null) { |
79 | itemExtent = this.itemExtent!; |
80 | return itemExtent * index; |
81 | } else { |
82 | double offset = 0.0; |
83 | double? itemExtent; |
84 | for (int i = 0; i < index; i++) { |
85 | final int? childCount = childManager.estimatedChildCount; |
86 | if (childCount != null && i > childCount - 1) { |
87 | break; |
88 | } |
89 | itemExtent = itemExtentBuilder!(i, _currentLayoutDimensions); |
90 | if (itemExtent == null) { |
91 | break; |
92 | } |
93 | offset += itemExtent; |
94 | } |
95 | return offset; |
96 | } |
97 | } |
98 | |
99 | /// The minimum child index that is visible at the given scroll offset. |
100 | /// |
101 | /// This function uses the returned value of [itemExtentBuilder] or the |
102 | /// [itemExtent] to avoid recomputing item size repeatedly during layout. |
103 | /// |
104 | /// By default, returns a value consistent with the children being placed in |
105 | /// order, without gaps, starting from layout offset zero. |
106 | @visibleForTesting |
107 | @protected |
108 | int getMinChildIndexForScrollOffset( |
109 | double scrollOffset, |
110 | @Deprecated( |
111 | 'The itemExtent is already available within the scope of this function. ' |
112 | 'This feature was deprecated after v3.20.0-7.0.pre.' |
113 | ) |
114 | double itemExtent, |
115 | ) { |
116 | if (itemExtentBuilder == null) { |
117 | itemExtent = this.itemExtent!; |
118 | if (itemExtent > 0.0) { |
119 | final double actual = scrollOffset / itemExtent; |
120 | final int round = actual.round(); |
121 | if ((actual * itemExtent - round * itemExtent).abs() < precisionErrorTolerance) { |
122 | return round; |
123 | } |
124 | return actual.floor(); |
125 | } |
126 | return 0; |
127 | } else { |
128 | return _getChildIndexForScrollOffset(scrollOffset, itemExtentBuilder!); |
129 | } |
130 | } |
131 | |
132 | /// The maximum child index that is visible at the given scroll offset. |
133 | /// |
134 | /// This function uses the returned value of [itemExtentBuilder] or the |
135 | /// [itemExtent] to avoid recomputing item size repeatedly during layout. |
136 | /// |
137 | /// By default, returns a value consistent with the children being placed in |
138 | /// order, without gaps, starting from layout offset zero. |
139 | @visibleForTesting |
140 | @protected |
141 | int getMaxChildIndexForScrollOffset( |
142 | double scrollOffset, |
143 | @Deprecated( |
144 | 'The itemExtent is already available within the scope of this function. ' |
145 | 'This feature was deprecated after v3.20.0-7.0.pre.' |
146 | ) |
147 | double itemExtent, |
148 | ) { |
149 | if (itemExtentBuilder == null) { |
150 | itemExtent = this.itemExtent!; |
151 | if (itemExtent > 0.0) { |
152 | final double actual = scrollOffset / itemExtent - 1; |
153 | final int round = actual.round(); |
154 | if ((actual * itemExtent - round * itemExtent).abs() < precisionErrorTolerance) { |
155 | return math.max(0, round); |
156 | } |
157 | return math.max(0, actual.ceil()); |
158 | } |
159 | return 0; |
160 | } else { |
161 | return _getChildIndexForScrollOffset(scrollOffset, itemExtentBuilder!); |
162 | } |
163 | } |
164 | |
165 | /// Called to estimate the total scrollable extents of this object. |
166 | /// |
167 | /// Must return the total distance from the start of the child with the |
168 | /// earliest possible index to the end of the child with the last possible |
169 | /// index. |
170 | /// |
171 | /// By default, defers to [RenderSliverBoxChildManager.estimateMaxScrollOffset]. |
172 | /// |
173 | /// See also: |
174 | /// |
175 | /// * [computeMaxScrollOffset], which is similar but must provide a precise |
176 | /// value. |
177 | @protected |
178 | double estimateMaxScrollOffset( |
179 | SliverConstraints constraints, { |
180 | int? firstIndex, |
181 | int? lastIndex, |
182 | double? leadingScrollOffset, |
183 | double? trailingScrollOffset, |
184 | }) { |
185 | return childManager.estimateMaxScrollOffset( |
186 | constraints, |
187 | firstIndex: firstIndex, |
188 | lastIndex: lastIndex, |
189 | leadingScrollOffset: leadingScrollOffset, |
190 | trailingScrollOffset: trailingScrollOffset, |
191 | ); |
192 | } |
193 | |
194 | /// Called to obtain a precise measure of the total scrollable extents of this |
195 | /// object. |
196 | /// |
197 | /// Must return the precise total distance from the start of the child with |
198 | /// the earliest possible index to the end of the child with the last possible |
199 | /// index. |
200 | /// |
201 | /// This is used when no child is available for the index corresponding to the |
202 | /// current scroll offset, to determine the precise dimensions of the sliver. |
203 | /// It must return a precise value. It will not be called if the |
204 | /// [childManager] returns an infinite number of children for positive |
205 | /// indices. |
206 | /// |
207 | /// If [itemExtentBuilder] is null, multiplies the [itemExtent] by the number |
208 | /// of children reported by [RenderSliverBoxChildManager.childCount]. |
209 | /// If [itemExtentBuilder] is non-null, sum the extents of the first |
210 | /// [RenderSliverBoxChildManager.childCount] children. |
211 | /// |
212 | /// See also: |
213 | /// |
214 | /// * [estimateMaxScrollOffset], which is similar but may provide inaccurate |
215 | /// values. |
216 | @visibleForTesting |
217 | @protected |
218 | double computeMaxScrollOffset( |
219 | SliverConstraints constraints, |
220 | @Deprecated( |
221 | 'The itemExtent is already available within the scope of this function. ' |
222 | 'This feature was deprecated after v3.20.0-7.0.pre.' |
223 | ) |
224 | double itemExtent, |
225 | ) { |
226 | if (itemExtentBuilder == null) { |
227 | itemExtent = this.itemExtent!; |
228 | return childManager.childCount * itemExtent; |
229 | } else { |
230 | double offset = 0.0; |
231 | double? itemExtent; |
232 | for (int i = 0; i < childManager.childCount; i++) { |
233 | itemExtent = itemExtentBuilder!(i, _currentLayoutDimensions); |
234 | if (itemExtent == null) { |
235 | break; |
236 | } |
237 | offset += itemExtent; |
238 | } |
239 | return offset; |
240 | } |
241 | } |
242 | |
243 | int _getChildIndexForScrollOffset(double scrollOffset, ItemExtentBuilder callback) { |
244 | if (scrollOffset == 0.0) { |
245 | return 0; |
246 | } |
247 | double position = 0.0; |
248 | int index = 0; |
249 | double? itemExtent; |
250 | while (position < scrollOffset) { |
251 | final int? childCount = childManager.estimatedChildCount; |
252 | if (childCount != null && index > childCount - 1) { |
253 | break; |
254 | } |
255 | itemExtent = callback(index, _currentLayoutDimensions); |
256 | if (itemExtent == null) { |
257 | break; |
258 | } |
259 | position += itemExtent; |
260 | ++index; |
261 | } |
262 | return index - 1; |
263 | } |
264 | |
265 | BoxConstraints _getChildConstraints(int index) { |
266 | double extent; |
267 | if (itemExtentBuilder == null) { |
268 | extent = itemExtent!; |
269 | } else { |
270 | extent = itemExtentBuilder!(index, _currentLayoutDimensions)!; |
271 | } |
272 | return constraints.asBoxConstraints( |
273 | minExtent: extent, |
274 | maxExtent: extent, |
275 | ); |
276 | } |
277 | |
278 | late SliverLayoutDimensions _currentLayoutDimensions; |
279 | |
280 | @override |
281 | void performLayout() { |
282 | assert((itemExtent != null && itemExtentBuilder == null) || |
283 | (itemExtent == null && itemExtentBuilder != null)); |
284 | assert(itemExtentBuilder != null || (itemExtent!.isFinite && itemExtent! >= 0)); |
285 | |
286 | final SliverConstraints constraints = this.constraints; |
287 | childManager.didStartLayout(); |
288 | childManager.setDidUnderflow(false); |
289 | |
290 | final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin; |
291 | assert(scrollOffset >= 0.0); |
292 | final double remainingExtent = constraints.remainingCacheExtent; |
293 | assert(remainingExtent >= 0.0); |
294 | final double targetEndScrollOffset = scrollOffset + remainingExtent; |
295 | |
296 | _currentLayoutDimensions = SliverLayoutDimensions( |
297 | scrollOffset: constraints.scrollOffset, |
298 | precedingScrollExtent: constraints.precedingScrollExtent, |
299 | viewportMainAxisExtent: constraints.viewportMainAxisExtent, |
300 | crossAxisExtent: constraints.crossAxisExtent |
301 | ); |
302 | // TODO(Piinks): Clean up when deprecation expires. |
303 | const double deprecatedExtraItemExtent = -1; |
304 | |
305 | final int firstIndex = getMinChildIndexForScrollOffset(scrollOffset, deprecatedExtraItemExtent); |
306 | final int? targetLastIndex = targetEndScrollOffset.isFinite ? |
307 | getMaxChildIndexForScrollOffset(targetEndScrollOffset, deprecatedExtraItemExtent) : null; |
308 | |
309 | if (firstChild != null) { |
310 | final int leadingGarbage = calculateLeadingGarbage(firstIndex: firstIndex); |
311 | final int trailingGarbage = targetLastIndex != null ? calculateTrailingGarbage(lastIndex: targetLastIndex) : 0; |
312 | collectGarbage(leadingGarbage, trailingGarbage); |
313 | } else { |
314 | collectGarbage(0, 0); |
315 | } |
316 | |
317 | if (firstChild == null) { |
318 | final double layoutOffset = indexToLayoutOffset(deprecatedExtraItemExtent, firstIndex); |
319 | if (!addInitialChild(index: firstIndex, layoutOffset: layoutOffset)) { |
320 | // There are either no children, or we are past the end of all our children. |
321 | final double max; |
322 | if (firstIndex <= 0) { |
323 | max = 0.0; |
324 | } else { |
325 | max = computeMaxScrollOffset(constraints, deprecatedExtraItemExtent); |
326 | } |
327 | geometry = SliverGeometry( |
328 | scrollExtent: max, |
329 | maxPaintExtent: max, |
330 | ); |
331 | childManager.didFinishLayout(); |
332 | return; |
333 | } |
334 | } |
335 | |
336 | RenderBox? trailingChildWithLayout; |
337 | |
338 | for (int index = indexOf(firstChild!) - 1; index >= firstIndex; --index) { |
339 | final RenderBox? child = insertAndLayoutLeadingChild(_getChildConstraints(index)); |
340 | if (child == null) { |
341 | // Items before the previously first child are no longer present. |
342 | // Reset the scroll offset to offset all items prior and up to the |
343 | // missing item. Let parent re-layout everything. |
344 | geometry = SliverGeometry(scrollOffsetCorrection: indexToLayoutOffset(deprecatedExtraItemExtent, index)); |
345 | return; |
346 | } |
347 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
348 | childParentData.layoutOffset = indexToLayoutOffset(deprecatedExtraItemExtent, index); |
349 | assert(childParentData.index == index); |
350 | trailingChildWithLayout ??= child; |
351 | } |
352 | |
353 | if (trailingChildWithLayout == null) { |
354 | firstChild!.layout(_getChildConstraints(indexOf(firstChild!))); |
355 | final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; |
356 | childParentData.layoutOffset = indexToLayoutOffset(deprecatedExtraItemExtent, firstIndex); |
357 | trailingChildWithLayout = firstChild; |
358 | } |
359 | |
360 | double estimatedMaxScrollOffset = double.infinity; |
361 | for (int index = indexOf(trailingChildWithLayout!) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { |
362 | RenderBox? child = childAfter(trailingChildWithLayout!); |
363 | if (child == null || indexOf(child) != index) { |
364 | child = insertAndLayoutChild(_getChildConstraints(index), after: trailingChildWithLayout); |
365 | if (child == null) { |
366 | // We have run out of children. |
367 | estimatedMaxScrollOffset = indexToLayoutOffset(deprecatedExtraItemExtent, index); |
368 | break; |
369 | } |
370 | } else { |
371 | child.layout(_getChildConstraints(index)); |
372 | } |
373 | trailingChildWithLayout = child; |
374 | final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
375 | assert(childParentData.index == index); |
376 | childParentData.layoutOffset = indexToLayoutOffset(deprecatedExtraItemExtent, childParentData.index!); |
377 | } |
378 | |
379 | final int lastIndex = indexOf(lastChild!); |
380 | final double leadingScrollOffset = indexToLayoutOffset(deprecatedExtraItemExtent, firstIndex); |
381 | final double trailingScrollOffset = indexToLayoutOffset(deprecatedExtraItemExtent, lastIndex + 1); |
382 | |
383 | assert(firstIndex == 0 || childScrollOffset(firstChild!)! - scrollOffset <= precisionErrorTolerance); |
384 | assert(debugAssertChildListIsNonEmptyAndContiguous()); |
385 | assert(indexOf(firstChild!) == firstIndex); |
386 | assert(targetLastIndex == null || lastIndex <= targetLastIndex); |
387 | |
388 | estimatedMaxScrollOffset = math.min( |
389 | estimatedMaxScrollOffset, |
390 | estimateMaxScrollOffset( |
391 | constraints, |
392 | firstIndex: firstIndex, |
393 | lastIndex: lastIndex, |
394 | leadingScrollOffset: leadingScrollOffset, |
395 | trailingScrollOffset: trailingScrollOffset, |
396 | ), |
397 | ); |
398 | |
399 | final double paintExtent = calculatePaintOffset( |
400 | constraints, |
401 | from: leadingScrollOffset, |
402 | to: trailingScrollOffset, |
403 | ); |
404 | |
405 | final double cacheExtent = calculateCacheOffset( |
406 | constraints, |
407 | from: leadingScrollOffset, |
408 | to: trailingScrollOffset, |
409 | ); |
410 | |
411 | final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent; |
412 | final int? targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite ? |
413 | getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint, deprecatedExtraItemExtent) : null; |
414 | |
415 | geometry = SliverGeometry( |
416 | scrollExtent: estimatedMaxScrollOffset, |
417 | paintExtent: paintExtent, |
418 | cacheExtent: cacheExtent, |
419 | maxPaintExtent: estimatedMaxScrollOffset, |
420 | // Conservative to avoid flickering away the clip during scroll. |
421 | hasVisualOverflow: (targetLastIndexForPaint != null && lastIndex >= targetLastIndexForPaint) |
422 | || constraints.scrollOffset > 0.0, |
423 | ); |
424 | |
425 | // We may have started the layout while scrolled to the end, which would not |
426 | // expose a new child. |
427 | if (estimatedMaxScrollOffset == trailingScrollOffset) { |
428 | childManager.setDidUnderflow(true); |
429 | } |
430 | childManager.didFinishLayout(); |
431 | } |
432 | } |
433 | |
434 | /// A sliver that places multiple box children with the same main axis extent in |
435 | /// a linear array. |
436 | /// |
437 | /// [RenderSliverFixedExtentList] places its children in a linear array along |
438 | /// the main axis starting at offset zero and without gaps. Each child is forced |
439 | /// to have the [itemExtent] in the main axis and the |
440 | /// [SliverConstraints.crossAxisExtent] in the cross axis. |
441 | /// |
442 | /// [RenderSliverFixedExtentList] is more efficient than [RenderSliverList] |
443 | /// because [RenderSliverFixedExtentList] does not need to perform layout on its |
444 | /// children to obtain their extent in the main axis. |
445 | /// |
446 | /// See also: |
447 | /// |
448 | /// * [RenderSliverList], which does not require its children to have the same |
449 | /// extent in the main axis. |
450 | /// * [RenderSliverFillViewport], which determines the [itemExtent] based on |
451 | /// [SliverConstraints.viewportMainAxisExtent]. |
452 | /// * [RenderSliverFillRemaining], which determines the [itemExtent] based on |
453 | /// [SliverConstraints.remainingPaintExtent]. |
454 | class RenderSliverFixedExtentList extends RenderSliverFixedExtentBoxAdaptor { |
455 | /// Creates a sliver that contains multiple box children that have a given |
456 | /// extent in the main axis. |
457 | RenderSliverFixedExtentList({ |
458 | required super.childManager, |
459 | required double itemExtent, |
460 | }) : _itemExtent = itemExtent; |
461 | |
462 | @override |
463 | double get itemExtent => _itemExtent; |
464 | double _itemExtent; |
465 | set itemExtent(double value) { |
466 | if (_itemExtent == value) { |
467 | return; |
468 | } |
469 | _itemExtent = value; |
470 | markNeedsLayout(); |
471 | } |
472 | } |
473 | |
474 | /// A sliver that places multiple box children with the corresponding main axis extent in |
475 | /// a linear array. |
476 | class RenderSliverVariedExtentList extends RenderSliverFixedExtentBoxAdaptor { |
477 | /// Creates a sliver that contains multiple box children that have a explicit |
478 | /// extent in the main axis. |
479 | RenderSliverVariedExtentList({ |
480 | required super.childManager, |
481 | required ItemExtentBuilder itemExtentBuilder, |
482 | }) : _itemExtentBuilder = itemExtentBuilder; |
483 | |
484 | @override |
485 | ItemExtentBuilder get itemExtentBuilder => _itemExtentBuilder; |
486 | ItemExtentBuilder _itemExtentBuilder; |
487 | set itemExtentBuilder(ItemExtentBuilder value) { |
488 | if (_itemExtentBuilder == value) { |
489 | return; |
490 | } |
491 | _itemExtentBuilder = value; |
492 | markNeedsLayout(); |
493 | } |
494 | |
495 | @override |
496 | double? get itemExtent => null; |
497 | } |
498 | |