sliver_list.dart 12.5 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
    childManager.didStartLayout();
48 49
    childManager.setDidUnderflow(false);

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

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

    // Find the last child that is at or before the scrollOffset.
    RenderBox earliestUsefulChild = firstChild;
    for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild);
96 97
        earliestScrollOffset > scrollOffset;
        earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
98 99
      // We have to add children before the earliestUsefulChild.
      earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
100

101
      if (earliestUsefulChild == null) {
102
        final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
103
        childParentData.layoutOffset = 0.0;
104 105

        if (scrollOffset == 0.0) {
106 107 108 109
          // insertAndLayoutLeadingChild only lays out the children before
          // firstChild. In this case, nothing has been laid out. We have
          // to lay out firstChild manually.
          firstChild.layout(childConstraints, parentUsesSize: true);
110 111 112 113 114 115 116 117
          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.
118
          geometry = SliverGeometry(
119 120 121 122
            scrollOffsetCorrection: -scrollOffset,
          );
          return;
        }
123
      }
124 125

      final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild);
126
      // firstChildScrollOffset may contain double precision error
127
      if (firstChildScrollOffset < -precisionErrorTolerance) {
128 129 130 131 132 133 134 135 136 137 138 139 140 141
        // The first child doesn't fit within the viewport (underflow) and
        // there may be additional children above it. Find the real first child
        // and then correct the scroll position so that there's room for all and
        // so that the trailing edge of the original firstChild appears where it
        // was before the scroll offset correction.
        // TODO(hansmuller): do this work incrementally, instead of all at once,
        // i.e. find a way to avoid visiting ALL of the children whose offset
        // is < 0 before returning for the scroll correction.
        double correction = 0.0;
        while (earliestUsefulChild != null) {
          assert(firstChild == earliestUsefulChild);
          correction += paintExtentOf(firstChild);
          earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
        }
142
        geometry = SliverGeometry(
143 144
          scrollOffsetCorrection: correction - earliestScrollOffset,
        );
145
        final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
146 147 148 149
        childParentData.layoutOffset = 0.0;
        return;
      }

150
      final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData as SliverMultiBoxAdaptorParentData;
151
      childParentData.layoutOffset = firstChildScrollOffset;
152 153 154
      assert(earliestUsefulChild == firstChild);
      leadingChildWithLayout = earliestUsefulChild;
      trailingChildWithLayout ??= earliestUsefulChild;
Adam Barth's avatar
Adam Barth committed
155 156
    }

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
    // 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);
    assert(childScrollOffset(earliestUsefulChild) <= scrollOffset);

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

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
    // 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;
    RenderBox child = earliestUsefulChild;
    int index = indexOf(child);
    double endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
    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;
      child = childAfter(child);
      if (child == null)
        inLayoutRange = false;
      index += 1;
      if (!inLayoutRange) {
        if (child == null || indexOf(child) != index) {
          // 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.
          child.layout(childConstraints, parentUsesSize: true);
Adam Barth's avatar
Adam Barth committed
206
        }
207
        trailingChildWithLayout = child;
Adam Barth's avatar
Adam Barth committed
208 209
      }
      assert(child != null);
210
      final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
211
      childParentData.layoutOffset = endScrollOffset;
212 213 214
      assert(childParentData.index == index);
      endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
      return true;
Adam Barth's avatar
Adam Barth committed
215 216
    }

217 218 219 220 221 222 223 224 225 226
    // 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);
        final double extent = childScrollOffset(lastChild) + paintExtentOf(lastChild);
227
        geometry = SliverGeometry(
228 229 230 231 232 233 234
          scrollExtent: extent,
          paintExtent: 0.0,
          maxPaintExtent: extent,
        );
        return;
      }
    }
Adam Barth's avatar
Adam Barth committed
235

236 237 238 239 240 241 242
    // 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
243

244 245 246 247 248 249 250 251
    // Finally count up all the remaining children and label them as garbage.
    if (child != null) {
      child = childAfter(child);
      while (child != null) {
        trailingGarbage += 1;
        child = childAfter(child);
      }
    }
Adam Barth's avatar
Adam Barth committed
252

253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
    // 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());
    double estimatedMaxScrollOffset;
    if (reachedEnd) {
      estimatedMaxScrollOffset = endScrollOffset;
    } else {
      estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
        constraints,
        firstIndex: indexOf(firstChild),
        lastIndex: indexOf(lastChild),
        leadingScrollOffset: childScrollOffset(firstChild),
        trailingScrollOffset: endScrollOffset,
      );
      assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
    }
Adam Barth's avatar
Adam Barth committed
272
    final double paintExtent = calculatePaintOffset(
Adam Barth's avatar
Adam Barth committed
273
      constraints,
274 275
      from: childScrollOffset(firstChild),
      to: endScrollOffset,
Adam Barth's avatar
Adam Barth committed
276
    );
277 278 279 280 281
    final double cacheExtent = calculateCacheOffset(
      constraints,
      from: childScrollOffset(firstChild),
      to: endScrollOffset,
    );
282
    final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
283
    geometry = SliverGeometry(
284
      scrollExtent: estimatedMaxScrollOffset,
Adam Barth's avatar
Adam Barth committed
285
      paintExtent: paintExtent,
286
      cacheExtent: cacheExtent,
287
      maxPaintExtent: estimatedMaxScrollOffset,
Adam Barth's avatar
Adam Barth committed
288
      // Conservative to avoid flickering away the clip during scroll.
289
      hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,
Adam Barth's avatar
Adam Barth committed
290 291
    );

292 293 294 295
    // We may have started the layout while scrolled to the end, which would not
    // expose a new child.
    if (estimatedMaxScrollOffset == endScrollOffset)
      childManager.setDidUnderflow(true);
296
    childManager.didFinishLayout();
Adam Barth's avatar
Adam Barth committed
297 298
  }
}