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';
7library;
8
9import 'dart:math' as math;
10
11import 'package:flutter/foundation.dart';
12
13import 'box.dart';
14import 'sliver.dart';
15import '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.
42abstract 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].
454class 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.
476class 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