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