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 'sliver.dart'; |
11 | import '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. |
38 | abstract 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]. |
409 | class 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 | |