flutter / mirrors / flutter / refs/tags/v0.4.0 / . / packages / flutter / lib / src / rendering / sliver_list.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, | |

); | |

geometry = new SliverGeometry( | |

scrollExtent: estimatedMaxScrollOffset, | |

paintExtent: paintExtent, | |

cacheExtent: cacheExtent, | |

maxPaintExtent: estimatedMaxScrollOffset, | |

// Conservative to avoid flickering away the clip during scroll. | |

hasVisualOverflow: endScrollOffset > targetEndScrollOffset || 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(); | |

} | |

} |