// Copyright 2014 The Flutter 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() { final SliverConstraints constraints = this.constraints; 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; RenderBox? earliestUsefulChild = firstChild; // 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. if (childScrollOffset(firstChild!) == null) { int leadingChildrenWithoutLayoutOffset = 0; while (earliestUsefulChild != null && childScrollOffset(earliestUsefulChild) == null) { earliestUsefulChild = childAfter(earliestUsefulChild); leadingChildrenWithoutLayoutOffset += 1; } // We should be able to destroy children with null layout offset safely, // because they are likely outside of viewport collectGarbage(leadingChildrenWithoutLayoutOffset, 0); // 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; } } } // Find the last child that is at or before the scrollOffset. 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! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = 0.0; if (scrollOffset == 0.0) { // 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); 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 = SliverGeometry( scrollOffsetCorrection: -scrollOffset, ); return; } } final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!); // firstChildScrollOffset may contain double precision error if (firstChildScrollOffset < -precisionErrorTolerance) { // 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, ); final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = 0.0; return; } final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = firstChildScrollOffset; assert(earliestUsefulChild == firstChild); leadingChildWithLayout = earliestUsefulChild; trailingChildWithLayout ??= earliestUsefulChild; } assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance); // If the scroll offset is at zero, we should make sure we are // actually at the beginning of the list. if (scrollOffset < precisionErrorTolerance) { // We iterate from the firstChild in case the leading child has a 0 paint // extent. while (indexOf(firstChild!) > 0) { final double earliestScrollOffset = childScrollOffset(firstChild!)!; // We correct one child at a time. If there are more children before // the earliestUsefulChild, we will correct it once the scroll offset // reaches zero again. earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true); assert(earliestUsefulChild != null); final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!); final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = 0.0; // We only need to correct if the leading child actually has a // paint extent. if (firstChildScrollOffset < -precisionErrorTolerance) { geometry = SliverGeometry( scrollOffsetCorrection: -firstChildScrollOffset, ); return; } } } // 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! as SliverMultiBoxAdaptorParentData; 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 = 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()); final 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 = 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(); } }