sliver_list.dart 11.8 KB
Newer Older
Adam Barth's avatar
Adam Barth committed
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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;
Adam Barth's avatar
Adam Barth committed
51
    assert(scrollOffset >= 0.0);
52
    final double remainingPaintExtent = constraints.remainingPaintExtent;
Adam Barth's avatar
Adam Barth committed
53
    assert(remainingPaintExtent >= 0.0);
54 55
    final double targetEndScrollOffset = scrollOffset + remainingPaintExtent;
    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 96 97 98 99
    // 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);
         earliestScrollOffset > scrollOffset;
         earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
      // We have to add children before the earliestUsefulChild.
      earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
100

101
      if (earliestUsefulChild == null) {
102 103
        final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData;
        childParentData.layoutOffset = 0.0;
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

        if (scrollOffset == 0.0) {
          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.
          geometry = new SliverGeometry(
            scrollOffsetCorrection: -scrollOffset,
          );
          return;
        }
119
      }
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144

      final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild);
      if (firstChildScrollOffset < 0.0) {
        // 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);
        }
        geometry = new SliverGeometry(
          scrollOffsetCorrection: correction - earliestScrollOffset,
        );
        final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData;
        childParentData.layoutOffset = 0.0;
        return;
      }

145
      final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData;
146
      childParentData.layoutOffset = firstChildScrollOffset;
147 148 149
      assert(earliestUsefulChild == firstChild);
      leadingChildWithLayout = earliestUsefulChild;
      trailingChildWithLayout ??= earliestUsefulChild;
Adam Barth's avatar
Adam Barth committed
150 151
    }

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    // 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
167 168
    }

169 170 171 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
    // 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
201
        }
202
        trailingChildWithLayout = child;
Adam Barth's avatar
Adam Barth committed
203 204 205
      }
      assert(child != null);
      final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
206
      childParentData.layoutOffset = endScrollOffset;
207 208 209
      assert(childParentData.index == index);
      endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
      return true;
Adam Barth's avatar
Adam Barth committed
210 211
    }

212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
    // 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);
        geometry = new SliverGeometry(
          scrollExtent: extent,
          paintExtent: 0.0,
          maxPaintExtent: extent,
        );
        return;
      }
    }
Adam Barth's avatar
Adam Barth committed
230

231 232 233 234 235 236 237
    // 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
238

239 240 241 242 243 244 245 246
    // 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
247

248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
    // 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
267
    final double paintExtent = calculatePaintOffset(
Adam Barth's avatar
Adam Barth committed
268
      constraints,
269 270
      from: childScrollOffset(firstChild),
      to: endScrollOffset,
Adam Barth's avatar
Adam Barth committed
271 272
    );
    geometry = new SliverGeometry(
273
      scrollExtent: estimatedMaxScrollOffset,
Adam Barth's avatar
Adam Barth committed
274
      paintExtent: paintExtent,
275
      maxPaintExtent: estimatedMaxScrollOffset,
Adam Barth's avatar
Adam Barth committed
276
      // Conservative to avoid flickering away the clip during scroll.
277
      hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0,
Adam Barth's avatar
Adam Barth committed
278 279
    );

280 281 282 283
    // We may have started the layout while scrolled to the end, which would not
    // expose a new child.
    if (estimatedMaxScrollOffset == endScrollOffset)
      childManager.setDidUnderflow(true);
284
    childManager.didFinishLayout();
Adam Barth's avatar
Adam Barth committed
285 286
  }
}