sliver_list.dart 14.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Adam Barth's avatar
Adam Barth committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
Adam Barth's avatar
Adam Barth committed
6 7 8 9 10

import 'box.dart';
import 'sliver.dart';
import 'sliver_multi_box_adaptor.dart';

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
/// A sliver that places multiple box children in a linear array along the main
/// axis.
///
/// Each child is forced to have the [SliverConstraints.crossAxisExtent] in the
/// cross axis but determines its own main axis extent.
///
/// [RenderSliverList] determines its scroll offset by "dead reckoning" because
/// children outside the visible part of the sliver are not materialized, which
/// means [RenderSliverList] cannot learn their main axis extent. Instead, newly
/// materialized children are placed adjacent to existing children. If this dead
/// reckoning results in a logical inconsistency (e.g., attempting to place the
/// zeroth child at a scroll offset other than zero), the [RenderSliverList]
/// generates a [SliverGeometry.scrollOffsetCorrection] to restore consistency.
///
/// If the children have a fixed extent in the main axis, consider using
/// [RenderSliverFixedExtentList] rather than [RenderSliverList] because
27 28 29
/// [RenderSliverFixedExtentList] does not need to perform layout on its
/// children to obtain their extent in the main axis and is therefore more
/// efficient.
30 31 32 33 34 35
///
/// See also:
///
///  * [RenderSliverFixedExtentList], which is more efficient for children with
///    the same extent in the main axis.
///  * [RenderSliverGrid], which places its children in arbitrary positions.
36
class RenderSliverList extends RenderSliverMultiBoxAdaptor {
37 38
  /// Creates a sliver that places multiple box children in a linear array along
  /// the main axis.
39
  RenderSliverList({
40 41
    required super.childManager,
  });
Adam Barth's avatar
Adam Barth committed
42 43 44

  @override
  void performLayout() {
45
    final SliverConstraints constraints = this.constraints;
46
    childManager.didStartLayout();
47 48
    childManager.setDidUnderflow(false);

49
    final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
Adam Barth's avatar
Adam Barth committed
50
    assert(scrollOffset >= 0.0);
51 52 53
    final double remainingExtent = constraints.remainingCacheExtent;
    assert(remainingExtent >= 0.0);
    final double targetEndScrollOffset = scrollOffset + remainingExtent;
54
    final BoxConstraints childConstraints = constraints.asBoxConstraints();
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
    int leadingGarbage = 0;
    int trailingGarbage = 0;
    bool reachedEnd = false;

    // This algorithm in principle is straight-forward: find the first child
    // that overlaps the given scrollOffset, creating more children at the top
    // of the list if necessary, then walk down the list updating and laying out
    // each child and adding more at the end if necessary until we have enough
    // children to cover the entire viewport.
    //
    // It is complicated by one minor issue, which is that any time you update
    // or create a child, it's possible that the some of the children that
    // haven't yet been laid out will be removed, leaving the list in an
    // inconsistent state, and requiring that missing nodes be recreated.
    //
    // To keep this mess tractable, this algorithm starts from what is currently
    // the first child, if any, and then walks up and/or down from there, so
    // that the nodes that might get removed are always at the edges of what has
    // already been laid out.

    // Make sure we have at least one child to start from.
Adam Barth's avatar
Adam Barth committed
76
    if (firstChild == null) {
77
      if (!addInitialChild()) {
Adam Barth's avatar
Adam Barth committed
78 79
        // There are no children.
        geometry = SliverGeometry.zero;
80
        childManager.didFinishLayout();
Adam Barth's avatar
Adam Barth committed
81 82 83 84
        return;
      }
    }

85 86 87 88 89
    // We have at least one child.

    // These variables track the range of children that we have laid out. Within
    // this range, the children have consecutive indices. Outside this range,
    // it's possible for a child to get removed without notice.
90
    RenderBox? leadingChildWithLayout, trailingChildWithLayout;
91

92
    RenderBox? earliestUsefulChild = firstChild;
93 94 95 96 97 98 99

    // A firstChild with null layout offset is likely a result of children
    // reordering.
    //
    // We rely on firstChild to have accurate layout offset. In the case of null
    // layout offset, we have to find the first child that has valid layout
    // offset.
100
    if (childScrollOffset(firstChild!) == null) {
101
      int leadingChildrenWithoutLayoutOffset = 0;
102 103
      while (earliestUsefulChild != null && childScrollOffset(earliestUsefulChild) == null) {
        earliestUsefulChild = childAfter(earliestUsefulChild);
104 105 106 107 108
        leadingChildrenWithoutLayoutOffset += 1;
      }
      // We should be able to destroy children with null layout offset safely,
      // because they are likely outside of viewport
      collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
109 110 111 112 113 114 115 116 117
      // If can not find a valid layout offset, start from the initial child.
      if (firstChild == null) {
        if (!addInitialChild()) {
          // There are no children.
          geometry = SliverGeometry.zero;
          childManager.didFinishLayout();
          return;
        }
      }
118 119 120 121
    }

    // Find the last child that is at or before the scrollOffset.
    earliestUsefulChild = firstChild;
122
    for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
123
        earliestScrollOffset > scrollOffset;
124
        earliestScrollOffset = childScrollOffset(earliestUsefulChild)!) {
125 126 127
      // We have to add children before the earliestUsefulChild.
      earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
      if (earliestUsefulChild == null) {
128
        final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
129
        childParentData.layoutOffset = 0.0;
130 131

        if (scrollOffset == 0.0) {
132 133 134
          // insertAndLayoutLeadingChild only lays out the children before
          // firstChild. In this case, nothing has been laid out. We have
          // to lay out firstChild manually.
135
          firstChild!.layout(childConstraints, parentUsesSize: true);
136 137 138 139 140 141 142 143
          earliestUsefulChild = firstChild;
          leadingChildWithLayout = earliestUsefulChild;
          trailingChildWithLayout ??= earliestUsefulChild;
          break;
        } else {
          // We ran out of children before reaching the scroll offset.
          // We must inform our parent that this sliver cannot fulfill
          // its contract and that we need a scroll offset correction.
144
          geometry = SliverGeometry(
145 146 147 148
            scrollOffsetCorrection: -scrollOffset,
          );
          return;
        }
149
      }
150

151
      final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!);
152
      // firstChildScrollOffset may contain double precision error
153
      if (firstChildScrollOffset < -precisionErrorTolerance) {
154 155 156 157 158
        // Let's assume there is no child before the first child. We will
        // correct it on the next layout if it is not.
        geometry = SliverGeometry(
          scrollOffsetCorrection: -firstChildScrollOffset,
        );
159
        final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
160 161
        childParentData.layoutOffset = 0.0;
        return;
162 163
      }

164
      final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData;
165
      childParentData.layoutOffset = firstChildScrollOffset;
166 167 168
      assert(earliestUsefulChild == firstChild);
      leadingChildWithLayout = earliestUsefulChild;
      trailingChildWithLayout ??= earliestUsefulChild;
Adam Barth's avatar
Adam Barth committed
169 170
    }

171
    assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance);
172 173 174 175

    // If the scroll offset is at zero, we should make sure we are
    // actually at the beginning of the list.
    if (scrollOffset < precisionErrorTolerance) {
176 177
      // We iterate from the firstChild in case the leading child has a 0 paint
      // extent.
178 179
      while (indexOf(firstChild!) > 0) {
        final double earliestScrollOffset = childScrollOffset(firstChild!)!;
180 181
        // We correct one child at a time. If there are more children before
        // the earliestUsefulChild, we will correct it once the scroll offset
182
        // reaches zero again.
183 184
        earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
        assert(earliestUsefulChild != null);
185
        final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!);
186
        final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
187
        childParentData.layoutOffset = 0.0;
188 189 190 191 192 193 194 195
        // We only need to correct if the leading child actually has a
        // paint extent.
        if (firstChildScrollOffset < -precisionErrorTolerance) {
          geometry = SliverGeometry(
            scrollOffsetCorrection: -firstChildScrollOffset,
          );
          return;
        }
196 197 198
      }
    }

199 200 201 202 203 204 205 206
    // At this point, earliestUsefulChild is the first child, and is a child
    // whose scrollOffset is at or before the scrollOffset, and
    // leadingChildWithLayout and trailingChildWithLayout are either null or
    // cover a range of render boxes that we have laid out with the first being
    // the same as earliestUsefulChild and the last being either at or after the
    // scroll offset.

    assert(earliestUsefulChild == firstChild);
207
    assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
208 209 210

    // Make sure we've laid out at least one child.
    if (leadingChildWithLayout == null) {
211
      earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
212 213
      leadingChildWithLayout = earliestUsefulChild;
      trailingChildWithLayout = earliestUsefulChild;
Adam Barth's avatar
Adam Barth committed
214 215
    }

216 217 218 219 220 221
    // Here, earliestUsefulChild is still the first child, it's got a
    // scrollOffset that is at or before our actual scrollOffset, and it has
    // been laid out, and is in fact our leadingChildWithLayout. It's possible
    // that some children beyond that one have also been laid out.

    bool inLayoutRange = true;
222 223 224
    RenderBox? child = earliestUsefulChild;
    int index = indexOf(child!);
    double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
225 226 227
    bool advance() { // returns true if we advanced, false if we have no more children
      // This function is used in two different places below, to avoid code duplication.
      assert(child != null);
228
      if (child == trailingChildWithLayout) {
229
        inLayoutRange = false;
230
      }
231
      child = childAfter(child!);
232
      if (child == null) {
233
        inLayoutRange = false;
234
      }
235 236
      index += 1;
      if (!inLayoutRange) {
237
        if (child == null || indexOf(child!) != index) {
238 239
          // We are missing a child. Insert it (and lay it out) if possible.
          child = insertAndLayoutChild(childConstraints,
240
            after: trailingChildWithLayout,
241 242 243 244 245 246 247 248
            parentUsesSize: true,
          );
          if (child == null) {
            // We have run out of children.
            return false;
          }
        } else {
          // Lay out the child.
249
          child!.layout(childConstraints, parentUsesSize: true);
Adam Barth's avatar
Adam Barth committed
250
        }
251
        trailingChildWithLayout = child;
Adam Barth's avatar
Adam Barth committed
252 253
      }
      assert(child != null);
254
      final SliverMultiBoxAdaptorParentData childParentData = child!.parentData! as SliverMultiBoxAdaptorParentData;
255
      childParentData.layoutOffset = endScrollOffset;
256
      assert(childParentData.index == index);
257
      endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
258
      return true;
Adam Barth's avatar
Adam Barth committed
259 260
    }

261 262 263 264 265 266 267 268 269
    // Find the first child that ends after the scroll offset.
    while (endScrollOffset < scrollOffset) {
      leadingGarbage += 1;
      if (!advance()) {
        assert(leadingGarbage == childCount);
        assert(child == null);
        // we want to make sure we keep the last child around so we know the end scroll offset
        collectGarbage(leadingGarbage - 1, 0);
        assert(firstChild == lastChild);
270
        final double extent = childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
271
        geometry = SliverGeometry(
272 273 274 275 276 277
          scrollExtent: extent,
          maxPaintExtent: extent,
        );
        return;
      }
    }
Adam Barth's avatar
Adam Barth committed
278

279 280 281 282 283 284 285
    // Now find the first child that ends after our end.
    while (endScrollOffset < targetEndScrollOffset) {
      if (!advance()) {
        reachedEnd = true;
        break;
      }
    }
Adam Barth's avatar
Adam Barth committed
286

287 288
    // Finally count up all the remaining children and label them as garbage.
    if (child != null) {
289
      child = childAfter(child!);
290 291
      while (child != null) {
        trailingGarbage += 1;
292
        child = childAfter(child!);
293 294
      }
    }
Adam Barth's avatar
Adam Barth committed
295

296 297 298 299 300 301
    // At this point everything should be good to go, we just have to clean up
    // the garbage and report the geometry.

    collectGarbage(leadingGarbage, trailingGarbage);

    assert(debugAssertChildListIsNonEmptyAndContiguous());
302
    final double estimatedMaxScrollOffset;
303 304 305 306 307
    if (reachedEnd) {
      estimatedMaxScrollOffset = endScrollOffset;
    } else {
      estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
        constraints,
308 309 310
        firstIndex: indexOf(firstChild!),
        lastIndex: indexOf(lastChild!),
        leadingScrollOffset: childScrollOffset(firstChild!),
311 312
        trailingScrollOffset: endScrollOffset,
      );
313
      assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild!)!);
314
    }
Adam Barth's avatar
Adam Barth committed
315
    final double paintExtent = calculatePaintOffset(
Adam Barth's avatar
Adam Barth committed
316
      constraints,
317
      from: childScrollOffset(firstChild!)!,
318
      to: endScrollOffset,
Adam Barth's avatar
Adam Barth committed
319
    );
320 321
    final double cacheExtent = calculateCacheOffset(
      constraints,
322
      from: childScrollOffset(firstChild!)!,
323 324
      to: endScrollOffset,
    );
325
    final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
326
    geometry = SliverGeometry(
327
      scrollExtent: estimatedMaxScrollOffset,
Adam Barth's avatar
Adam Barth committed
328
      paintExtent: paintExtent,
329
      cacheExtent: cacheExtent,
330
      maxPaintExtent: estimatedMaxScrollOffset,
Adam Barth's avatar
Adam Barth committed
331
      // Conservative to avoid flickering away the clip during scroll.
332
      hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,
Adam Barth's avatar
Adam Barth committed
333 334
    );

335 336
    // We may have started the layout while scrolled to the end, which would not
    // expose a new child.
337
    if (estimatedMaxScrollOffset == endScrollOffset) {
338
      childManager.setDidUnderflow(true);
339
    }
340
    childManager.didFinishLayout();
Adam Barth's avatar
Adam Barth committed
341 342
  }
}