294 lines
12 KiB
Dart
294 lines
12 KiB
Dart
// 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.
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'box.dart';
|
|
import 'sliver.dart';
|
|
import 'sliver_multi_box_adaptor.dart';
|
|
|
|
/// 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
|
|
/// [RenderSliverFixedExtentList] does not need to perform layout on its
|
|
/// children to obtain their extent in the main axis and is therefore more
|
|
/// efficient.
|
|
///
|
|
/// 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.
|
|
class RenderSliverList extends RenderSliverMultiBoxAdaptor {
|
|
/// Creates a sliver that places multiple box children in a linear array along
|
|
/// the main axis.
|
|
///
|
|
/// The [childManager] argument must not be null.
|
|
RenderSliverList({
|
|
@required RenderSliverBoxChildManager childManager,
|
|
}) : super(childManager: childManager);
|
|
|
|
@override
|
|
void performLayout() {
|
|
childManager.didStartLayout();
|
|
childManager.setDidUnderflow(false);
|
|
|
|
final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
|
|
assert(scrollOffset >= 0.0);
|
|
final double remainingExtent = constraints.remainingCacheExtent;
|
|
assert(remainingExtent >= 0.0);
|
|
final double targetEndScrollOffset = scrollOffset + remainingExtent;
|
|
final BoxConstraints childConstraints = constraints.asBoxConstraints();
|
|
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.
|
|
if (firstChild == null) {
|
|
if (!addInitialChild()) {
|
|
// There are no children.
|
|
geometry = SliverGeometry.zero;
|
|
childManager.didFinishLayout();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
if (earliestUsefulChild == null) {
|
|
final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData;
|
|
childParentData.layoutOffset = 0.0;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData;
|
|
childParentData.layoutOffset = firstChildScrollOffset;
|
|
assert(earliestUsefulChild == firstChild);
|
|
leadingChildWithLayout = earliestUsefulChild;
|
|
trailingChildWithLayout ??= earliestUsefulChild;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
trailingChildWithLayout = child;
|
|
}
|
|
assert(child != null);
|
|
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
|
|
childParentData.layoutOffset = endScrollOffset;
|
|
assert(childParentData.index == index);
|
|
endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
|
|
return true;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// Now find the first child that ends after our end.
|
|
while (endScrollOffset < targetEndScrollOffset) {
|
|
if (!advance()) {
|
|
reachedEnd = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
final double paintExtent = calculatePaintOffset(
|
|
constraints,
|
|
from: childScrollOffset(firstChild),
|
|
to: endScrollOffset,
|
|
);
|
|
final double cacheExtent = calculateCacheOffset(
|
|
constraints,
|
|
from: childScrollOffset(firstChild),
|
|
to: endScrollOffset,
|
|
);
|
|
final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
|
|
geometry = new SliverGeometry(
|
|
scrollExtent: estimatedMaxScrollOffset,
|
|
paintExtent: paintExtent,
|
|
cacheExtent: cacheExtent,
|
|
maxPaintExtent: estimatedMaxScrollOffset,
|
|
// Conservative to avoid flickering away the clip during scroll.
|
|
hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,
|
|
);
|
|
|
|
// We may have started the layout while scrolled to the end, which would not
|
|
// expose a new child.
|
|
if (estimatedMaxScrollOffset == endScrollOffset)
|
|
childManager.setDidUnderflow(true);
|
|
childManager.didFinishLayout();
|
|
}
|
|
}
|