sliver_list.dart 14.3 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 39 40
  /// Creates a sliver that places multiple box children in a linear array along
  /// the main axis.
  ///
  /// The [childManager] argument must not be null.
41
  RenderSliverList({
42
    required RenderSliverBoxChildManager childManager,
Adam Barth's avatar
Adam Barth committed
43
  }) : super(childManager: childManager);
Adam Barth's avatar
Adam Barth committed
44 45 46

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

51
    final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
Adam Barth's avatar
Adam Barth committed
52
    assert(scrollOffset >= 0.0);
53 54 55
    final double remainingExtent = constraints.remainingCacheExtent;
    assert(remainingExtent >= 0.0);
    final double targetEndScrollOffset = scrollOffset + remainingExtent;
56
    final BoxConstraints childConstraints = constraints.asBoxConstraints();
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
    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
78
    if (firstChild == null) {
79
      if (!addInitialChild()) {
Adam Barth's avatar
Adam Barth committed
80 81
        // There are no children.
        geometry = SliverGeometry.zero;
82
        childManager.didFinishLayout();
Adam Barth's avatar
Adam Barth committed
83 84 85 86
        return;
      }
    }

87 88 89 90 91
    // 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.
92
    RenderBox? leadingChildWithLayout, trailingChildWithLayout;
93

94
    RenderBox? earliestUsefulChild = firstChild;
95 96 97 98 99 100 101

    // 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.
102
    if (childScrollOffset(firstChild!) == null) {
103
      int leadingChildrenWithoutLayoutOffset = 0;
104 105
      while (earliestUsefulChild != null && childScrollOffset(earliestUsefulChild) == null) {
        earliestUsefulChild = childAfter(earliestUsefulChild);
106 107 108 109 110
        leadingChildrenWithoutLayoutOffset += 1;
      }
      // We should be able to destroy children with null layout offset safely,
      // because they are likely outside of viewport
      collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
111 112 113 114 115 116 117 118 119
      // 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;
        }
      }
120 121 122 123
    }

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

        if (scrollOffset == 0.0) {
134 135 136
          // insertAndLayoutLeadingChild only lays out the children before
          // firstChild. In this case, nothing has been laid out. We have
          // to lay out firstChild manually.
137
          firstChild!.layout(childConstraints, parentUsesSize: true);
138 139 140 141 142 143 144 145
          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.
146
          geometry = SliverGeometry(
147 148 149 150
            scrollOffsetCorrection: -scrollOffset,
          );
          return;
        }
151
      }
152

153
      final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!);
154
      // firstChildScrollOffset may contain double precision error
155
      if (firstChildScrollOffset < -precisionErrorTolerance) {
156 157 158 159 160
        // 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,
        );
161
        final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
162 163
        childParentData.layoutOffset = 0.0;
        return;
164 165
      }

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

173
    assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance);
174 175 176 177

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

201 202 203 204 205 206 207 208
    // 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);
209
    assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
210 211 212

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

218 219 220 221 222 223
    // 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;
224 225 226
    RenderBox? child = earliestUsefulChild;
    int index = indexOf(child!);
    double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
227 228 229 230 231
    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);
      if (child == trailingChildWithLayout)
        inLayoutRange = false;
232
      child = childAfter(child!);
233 234 235 236
      if (child == null)
        inLayoutRange = false;
      index += 1;
      if (!inLayoutRange) {
237
        if (child == null || indexOf(child!) != index) {
238 239 240 241 242 243 244 245 246 247 248
          // We are missing a child. Insert it (and lay it out) if possible.
          child = insertAndLayoutChild(childConstraints,
            after: trailingChildWithLayout,
            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 337 338
    // We may have started the layout while scrolled to the end, which would not
    // expose a new child.
    if (estimatedMaxScrollOffset == endScrollOffset)
      childManager.setDidUnderflow(true);
339
    childManager.didFinishLayout();
Adam Barth's avatar
Adam Barth committed
340 341
  }
}