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_fixed_extent_list.dart';
6/// @docImport 'sliver_grid.dart';
7library;
8
9import 'package:flutter/foundation.dart';
10
11import 'box.dart';
12import 'sliver.dart';
13import 'sliver_multi_box_adaptor.dart';
14
15/// A sliver that places multiple box children in a linear array along the main
16/// axis.
17///
18/// Each child is forced to have the [SliverConstraints.crossAxisExtent] in the
19/// cross axis but determines its own main axis extent.
20///
21/// [RenderSliverList] determines its scroll offset by "dead reckoning" because
22/// children outside the visible part of the sliver are not materialized, which
23/// means [RenderSliverList] cannot learn their main axis extent. Instead, newly
24/// materialized children are placed adjacent to existing children. If this dead
25/// reckoning results in a logical inconsistency (e.g., attempting to place the
26/// zeroth child at a scroll offset other than zero), the [RenderSliverList]
27/// generates a [SliverGeometry.scrollOffsetCorrection] to restore consistency.
28///
29/// If the children have a fixed extent in the main axis, consider using
30/// [RenderSliverFixedExtentList] rather than [RenderSliverList] because
31/// [RenderSliverFixedExtentList] does not need to perform layout on its
32/// children to obtain their extent in the main axis and is therefore more
33/// efficient.
34///
35/// See also:
36///
37/// * [RenderSliverFixedExtentList], which is more efficient for children with
38/// the same extent in the main axis.
39/// * [RenderSliverGrid], which places its children in arbitrary positions.
40class RenderSliverList extends RenderSliverMultiBoxAdaptor {
41 /// Creates a sliver that places multiple box children in a linear array along
42 /// the main axis.
43 RenderSliverList({required super.childManager});
44
45 @override
46 void performLayout() {
47 final SliverConstraints constraints = this.constraints;
48 childManager.didStartLayout();
49 childManager.setDidUnderflow(false);
50
51 final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
52 assert(scrollOffset >= 0.0);
53 final double remainingExtent = constraints.remainingCacheExtent;
54 assert(remainingExtent >= 0.0);
55 final double targetEndScrollOffset = scrollOffset + remainingExtent;
56 final BoxConstraints childConstraints = constraints.asBoxConstraints();
57 int leadingGarbage = 0;
58 int trailingGarbage = 0;
59 bool reachedEnd = false;
60
61 // This algorithm in principle is straight-forward: find the first child
62 // that overlaps the given scrollOffset, creating more children at the top
63 // of the list if necessary, then walk down the list updating and laying out
64 // each child and adding more at the end if necessary until we have enough
65 // children to cover the entire viewport.
66 //
67 // It is complicated by one minor issue, which is that any time you update
68 // or create a child, it's possible that the some of the children that
69 // haven't yet been laid out will be removed, leaving the list in an
70 // inconsistent state, and requiring that missing nodes be recreated.
71 //
72 // To keep this mess tractable, this algorithm starts from what is currently
73 // the first child, if any, and then walks up and/or down from there, so
74 // that the nodes that might get removed are always at the edges of what has
75 // already been laid out.
76
77 // Make sure we have at least one child to start from.
78 if (firstChild == null) {
79 if (!addInitialChild()) {
80 // There are no children.
81 geometry = SliverGeometry.zero;
82 childManager.didFinishLayout();
83 return;
84 }
85 }
86
87 // We have at least one child.
88
89 // These variables track the range of children that we have laid out. Within
90 // this range, the children have consecutive indices. Outside this range,
91 // it's possible for a child to get removed without notice.
92 RenderBox? leadingChildWithLayout, trailingChildWithLayout;
93
94 RenderBox? earliestUsefulChild = firstChild;
95
96 // A firstChild with null layout offset is likely a result of children
97 // reordering.
98 //
99 // We rely on firstChild to have accurate layout offset. In the case of null
100 // layout offset, we have to find the first child that has valid layout
101 // offset.
102 if (childScrollOffset(firstChild!) == null) {
103 int leadingChildrenWithoutLayoutOffset = 0;
104 while (earliestUsefulChild != null && childScrollOffset(earliestUsefulChild) == null) {
105 earliestUsefulChild = childAfter(earliestUsefulChild);
106 leadingChildrenWithoutLayoutOffset += 1;
107 }
108 // We should be able to destroy children with null layout offset safely,
109 // because they are likely outside of viewport
110 collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
111 // If can not find a valid layout offset, start from the initial child.
112 if (firstChild == null) {
113 if (!addInitialChild()) {
114 // There are no children.
115 geometry = SliverGeometry.zero;
116 childManager.didFinishLayout();
117 return;
118 }
119 }
120 }
121
122 // Find the last child that is at or before the scrollOffset.
123 earliestUsefulChild = firstChild;
124 for (
125 double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
126 earliestScrollOffset > scrollOffset;
127 earliestScrollOffset = childScrollOffset(earliestUsefulChild)!
128 ) {
129 // We have to add children before the earliestUsefulChild.
130 earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
131 if (earliestUsefulChild == null) {
132 final SliverMultiBoxAdaptorParentData childParentData =
133 firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
134 childParentData.layoutOffset = 0.0;
135
136 if (scrollOffset == 0.0) {
137 // insertAndLayoutLeadingChild only lays out the children before
138 // firstChild. In this case, nothing has been laid out. We have
139 // to lay out firstChild manually.
140 firstChild!.layout(childConstraints, parentUsesSize: true);
141 earliestUsefulChild = firstChild;
142 leadingChildWithLayout = earliestUsefulChild;
143 trailingChildWithLayout ??= earliestUsefulChild;
144 break;
145 } else {
146 // We ran out of children before reaching the scroll offset.
147 // We must inform our parent that this sliver cannot fulfill
148 // its contract and that we need a scroll offset correction.
149 geometry = SliverGeometry(scrollOffsetCorrection: -scrollOffset);
150 return;
151 }
152 }
153
154 final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!);
155 // firstChildScrollOffset may contain double precision error
156 if (firstChildScrollOffset < -precisionErrorTolerance) {
157 // Let's assume there is no child before the first child. We will
158 // correct it on the next layout if it is not.
159 geometry = SliverGeometry(scrollOffsetCorrection: -firstChildScrollOffset);
160 final SliverMultiBoxAdaptorParentData childParentData =
161 firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
162 childParentData.layoutOffset = 0.0;
163 return;
164 }
165
166 final SliverMultiBoxAdaptorParentData childParentData =
167 earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData;
168 childParentData.layoutOffset = firstChildScrollOffset;
169 assert(earliestUsefulChild == firstChild);
170 leadingChildWithLayout = earliestUsefulChild;
171 trailingChildWithLayout ??= earliestUsefulChild;
172 }
173
174 assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance);
175
176 // If the scroll offset is at zero, we should make sure we are
177 // actually at the beginning of the list.
178 if (scrollOffset < precisionErrorTolerance) {
179 // We iterate from the firstChild in case the leading child has a 0 paint
180 // extent.
181 while (indexOf(firstChild!) > 0) {
182 final double earliestScrollOffset = childScrollOffset(firstChild!)!;
183 // We correct one child at a time. If there are more children before
184 // the earliestUsefulChild, we will correct it once the scroll offset
185 // reaches zero again.
186 earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
187 assert(earliestUsefulChild != null);
188 final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!);
189 final SliverMultiBoxAdaptorParentData childParentData =
190 firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
191 childParentData.layoutOffset = 0.0;
192 // We only need to correct if the leading child actually has a
193 // paint extent.
194 if (firstChildScrollOffset < -precisionErrorTolerance) {
195 geometry = SliverGeometry(scrollOffsetCorrection: -firstChildScrollOffset);
196 return;
197 }
198 }
199 }
200
201 // At this point, earliestUsefulChild is the first child, and is a child
202 // whose scrollOffset is at or before the scrollOffset, and
203 // leadingChildWithLayout and trailingChildWithLayout are either null or
204 // cover a range of render boxes that we have laid out with the first being
205 // the same as earliestUsefulChild and the last being either at or after the
206 // scroll offset.
207
208 assert(earliestUsefulChild == firstChild);
209 assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
210
211 // Make sure we've laid out at least one child.
212 if (leadingChildWithLayout == null) {
213 earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
214 leadingChildWithLayout = earliestUsefulChild;
215 trailingChildWithLayout = earliestUsefulChild;
216 }
217
218 // Here, earliestUsefulChild is still the first child, it's got a
219 // scrollOffset that is at or before our actual scrollOffset, and it has
220 // been laid out, and is in fact our leadingChildWithLayout. It's possible
221 // that some children beyond that one have also been laid out.
222
223 bool inLayoutRange = true;
224 RenderBox? child = earliestUsefulChild;
225 int index = indexOf(child!);
226 double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
227 bool advance() {
228 // returns true if we advanced, false if we have no more children
229 // This function is used in two different places below, to avoid code duplication.
230 assert(child != null);
231 if (child == trailingChildWithLayout) {
232 inLayoutRange = false;
233 }
234 child = childAfter(child!);
235 if (child == null) {
236 inLayoutRange = false;
237 }
238 index += 1;
239 if (!inLayoutRange) {
240 if (child == null || indexOf(child!) != index) {
241 // We are missing a child. Insert it (and lay it out) if possible.
242 child = insertAndLayoutChild(
243 childConstraints,
244 after: trailingChildWithLayout,
245 parentUsesSize: true,
246 );
247 if (child == null) {
248 // We have run out of children.
249 return false;
250 }
251 } else {
252 // Lay out the child.
253 child!.layout(childConstraints, parentUsesSize: true);
254 }
255 trailingChildWithLayout = child;
256 }
257 assert(child != null);
258 final SliverMultiBoxAdaptorParentData childParentData =
259 child!.parentData! as SliverMultiBoxAdaptorParentData;
260 childParentData.layoutOffset = endScrollOffset;
261 assert(childParentData.index == index);
262 endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
263 return true;
264 }
265
266 // Find the first child that ends after the scroll offset.
267 while (endScrollOffset < scrollOffset) {
268 leadingGarbage += 1;
269 if (!advance()) {
270 assert(leadingGarbage == childCount);
271 assert(child == null);
272 // we want to make sure we keep the last child around so we know the end scroll offset
273 collectGarbage(leadingGarbage - 1, 0);
274 assert(firstChild == lastChild);
275 final double extent = childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
276 geometry = SliverGeometry(scrollExtent: extent, maxPaintExtent: extent);
277 return;
278 }
279 }
280
281 // Now find the first child that ends after our end.
282 while (endScrollOffset < targetEndScrollOffset) {
283 if (!advance()) {
284 reachedEnd = true;
285 break;
286 }
287 }
288
289 // Finally count up all the remaining children and label them as garbage.
290 if (child != null) {
291 child = childAfter(child!);
292 while (child != null) {
293 trailingGarbage += 1;
294 child = childAfter(child!);
295 }
296 }
297
298 // At this point everything should be good to go, we just have to clean up
299 // the garbage and report the geometry.
300
301 collectGarbage(leadingGarbage, trailingGarbage);
302
303 assert(debugAssertChildListIsNonEmptyAndContiguous());
304 final double estimatedMaxScrollOffset;
305 if (reachedEnd) {
306 estimatedMaxScrollOffset = endScrollOffset;
307 } else {
308 estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
309 constraints,
310 firstIndex: indexOf(firstChild!),
311 lastIndex: indexOf(lastChild!),
312 leadingScrollOffset: childScrollOffset(firstChild!),
313 trailingScrollOffset: endScrollOffset,
314 );
315 assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild!)!);
316 }
317 final double paintExtent = calculatePaintOffset(
318 constraints,
319 from: childScrollOffset(firstChild!)!,
320 to: endScrollOffset,
321 );
322 final double cacheExtent = calculateCacheOffset(
323 constraints,
324 from: childScrollOffset(firstChild!)!,
325 to: endScrollOffset,
326 );
327 final double targetEndScrollOffsetForPaint =
328 constraints.scrollOffset + constraints.remainingPaintExtent;
329 geometry = SliverGeometry(
330 scrollExtent: estimatedMaxScrollOffset,
331 paintExtent: paintExtent,
332 cacheExtent: cacheExtent,
333 maxPaintExtent: estimatedMaxScrollOffset,
334 // Conservative to avoid flickering away the clip during scroll.
335 hasVisualOverflow:
336 endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,
337 );
338
339 // We may have started the layout while scrolled to the end, which would not
340 // expose a new child.
341 if (estimatedMaxScrollOffset == endScrollOffset) {
342 childManager.setDidUnderflow(true);
343 }
344 childManager.didFinishLayout();
345 }
346}
347

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com