performLayout method

  1. @override
void performLayout()
override

Do the work of computing the layout for this render object.

Do not call this function directly: call layout instead. This function is called by layout when there is actually work to be done by this render object during layout. The layout constraints provided by your parent are available via the constraints getter.

If sizedByParent is true, then this function should not actually change the dimensions of this render object. Instead, that work should be done by performResize. If sizedByParent is false, then this function should both change the dimensions of this render object and instruct its children to layout.

In implementing this function, you must call layout on each of your children, passing true for parentUsesSize if your layout information is dependent on your child's layout information. Passing true for parentUsesSize ensures that this render object will undergo layout if the child undergoes layout. Otherwise, the child can change its layout information without informing this render object.

Implementation

@override
void performLayout() {
  childManager.didStartLayout();
  childManager.setDidUnderflow(false);

  _clearIfNeed();

  final _CrossAxisChildrenData crossAxisChildrenData = _CrossAxisChildrenData(
    gridDelegate: _gridDelegate,
    constraints: constraints,
    leadingChildren: _previousCrossAxisChildrenData?.leadingChildren,
  );

  final BoxConstraints childConstraints = constraints.asBoxConstraints(
      crossAxisExtent:
          _gridDelegate.getChildUsableCrossAxisExtent(constraints));

  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;

  int leadingGarbage = 0;
  int trailingGarbage = 0;
  bool reachedEnd = false;

  // This algorithm in principle is straight-forward: find the leading children
  // that overlaps the given scrollOffset base on [_previousCrossAxisChildrenData],
  // creating more children after the shortest one of the previous row/column,
  // 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;
      _previousCrossAxisChildrenData = null;
      childManager.didFinishLayout();
      return;
    }
  }

  // zmt
  handleCloseToTrailingBegin(_gridDelegate.closeToTrailing);
  // In case,the itemCount is changed, clear all,
  final SliverWaterfallFlowParentData firstChildParentData =
      firstChild!.parentData as SliverWaterfallFlowParentData;
  // In case of the itemCount is changed, clear all leading children,
  // avoid calculate with dirty leading children.
  if (firstChildParentData.index == 0) {
    crossAxisChildrenData.clear();
  }

  // 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;

  // 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 (childScrollOffset(earliestUsefulChild!) == null) {
      earliestUsefulChild = childAfter(firstChild!);
      leadingChildrenWithoutLayoutOffset += 1;
    }
    // We should be able to destroy children with null layout offset safely,
    // because they are likely outside of viewport
    collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
    assert(firstChild != null);
  }

  // Find the last child that is at or before the scrollOffset.
  earliestUsefulChild = firstChild;

  if (crossAxisChildrenData.maxLeadingLayoutOffset! > scrollOffset) {
    RenderBox? child = firstChild;
    // Add children from min index to max index of leading to
    // make sure indexes are continuous.
    final int? maxLeadingIndex = crossAxisChildrenData.maxLeadingIndex;
    while (child != null && maxLeadingIndex! > indexOf(child)) {
      child = childAfter(child);
    }
    final int? minLeadingIndex = crossAxisChildrenData.minLeadingIndex;
    while (child != null && minLeadingIndex! < indexOf(child)) {
      crossAxisChildrenData.insertLeading(
          child: child, paintExtentOf: paintExtentOf);
      child = childBefore(child);
    }

    while (crossAxisChildrenData.maxLeadingLayoutOffset! > scrollOffset) {
      // We have to add children before the earliestUsefulChild.
      earliestUsefulChild =
          insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);

      if (earliestUsefulChild == null) {
        final SliverWaterfallFlowParentData data =
            firstChild!.parentData as SliverWaterfallFlowParentData;
        assert(data.index == 0);
        // In case of some child is changed small and we ran out of children
        // before reaching the scroll offset.
        // We must clear data, avoid calculate with dirty leading children.
        firstChild!.layout(childConstraints, parentUsesSize: true);
        earliestUsefulChild = firstChild;
        leadingChildWithLayout = earliestUsefulChild;
        trailingChildWithLayout ??= earliestUsefulChild;
        crossAxisChildrenData.clear();
        break;
      }

      crossAxisChildrenData.insertLeading(
          child: earliestUsefulChild, paintExtentOf: paintExtentOf);

      final SliverWaterfallFlowParentData data =
          earliestUsefulChild.parentData as SliverWaterfallFlowParentData;
      // item after leadings
      if (data.layoutOffset == null) {
        continue;
      }
      // firstChildScrollOffset may contain double precision error
      if (data.layoutOffset! < -precisionErrorTolerance) {
        // 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.
        // do this work incrementally, instead of all at once,
        // find first child and clear all,
        // avoid calculate with dirty leading children.
        while (earliestUsefulChild != null) {
          assert(firstChild == earliestUsefulChild);
          earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints,
              parentUsesSize: true);
        }
        final SliverWaterfallFlowParentData data =
            firstChild!.parentData as SliverWaterfallFlowParentData;
        assert(data.index == 0);
        crossAxisChildrenData.clear();
        earliestUsefulChild = firstChild;
      }

      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);

  // Make sure we've laid out at least one child.
  if (leadingChildWithLayout == null) {
    earliestUsefulChild!.layout(
        _gridDelegate.getLastChildLayoutType(indexOf(earliestUsefulChild)) !=
                LastChildLayoutType.none
            ? constraints.asBoxConstraints()
            : 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);
  crossAxisChildrenData.insert(
    child: child,
    childTrailingLayoutOffset: childTrailingLayoutOffset,
    paintExtentOf: paintExtentOf,
  );

  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;
    final LastChildLayoutType lastChildLayoutType =
        _gridDelegate.getLastChildLayoutType(index);
    final BoxConstraints currentConstraints =
        lastChildLayoutType != LastChildLayoutType.none
            ? constraints.asBoxConstraints()
            : childConstraints;

    if (!inLayoutRange) {
      if (child == null || indexOf(child!) != index) {
        // We are missing a child. Insert it (and lay it out) if possible.

        child = insertAndLayoutChild(
          currentConstraints,
          after: trailingChildWithLayout,
          parentUsesSize: true,
        );
        if (child == null) {
          // We have run out of children.
          return false;
        }
      } else {
        // Lay out the child.
        child!.layout(currentConstraints, parentUsesSize: true);
      }
      trailingChildWithLayout = child;
    }
    assert(child != null);
    //zmt
    final SliverWaterfallFlowParentData childParentData =
        child!.parentData as SliverWaterfallFlowParentData;
    //zmt

    crossAxisChildrenData.insert(
      child: child!,
      childTrailingLayoutOffset: childTrailingLayoutOffset,
      paintExtentOf: paintExtentOf,
    );

    assert(childParentData.index == index);
    return true;
  }

  final List<int> leadingGarbages = <int>[];

  // Find the first child that ends after the scroll offset.
  while (childTrailingLayoutOffset(child!) < scrollOffset) {
    leadingGarbage += 1;
    leadingGarbages.add(index);
    if (!advance()) {
      assert(leadingGarbage == childCount);
      assert(child == null);
      // 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 max scroll offset correction.

      // we want to make sure we keep the trailingChildren around so we know the end scroll offset
      // if _previousCrossAxisChildrenData is null, we should re-calculate it from index 0.
      if (_previousCrossAxisChildrenData != null) {
        final int? minTrailingIndex =
            _previousCrossAxisChildrenData!.minTrailingIndex;
        for (final int index in leadingGarbages) {
          if (index >= minTrailingIndex!) {
            leadingGarbage -= 1;
          }
        }
      }

      collectGarbage(leadingGarbage, 0);

      final double extent =
          crossAxisChildrenData.maxChildTrailingLayoutOffset!;

      geometry = SliverGeometry(
        scrollExtent: extent,
        paintExtent: 0.0,
        maxPaintExtent: extent,
      );
      //_previousCrossAxisChildrenData = null;
      return;
    }
  }

  if (leadingGarbage > 0) {
    // Make sure the leadings are after the scroll offset
    while (
        crossAxisChildrenData.minChildTrailingLayoutOffset! < scrollOffset) {
      if (!advance()) {
        final int? minTrailingIndex = crossAxisChildrenData.minTrailingIndex;
        // The indexes are continuous, make sure they are less than minTrailingIndex.
        for (final int index in leadingGarbages) {
          if (index >= minTrailingIndex!) {
            leadingGarbage--;
          }
        }
        leadingGarbage = max(0, leadingGarbage);
        break;
      }
    }
    crossAxisChildrenData.setLeading();
  }

  if (child != null) {
    final int? crossAxisCount = _gridDelegate.getCrossAxisCount(constraints);
    while (
        // Now find the first child that ends after our end.
        crossAxisChildrenData.minChildTrailingLayoutOffset! <
                targetEndScrollOffset
            // Make sure leading children are all laid out.
            ||
            crossAxisChildrenData.leadingChildren.length < crossAxisCount! ||
            crossAxisChildrenData.leadingChildren.length > childCount ||
            (child!.parentData as SliverWaterfallFlowParentData).index! <
                crossAxisCount - 1) {
      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);
  //zmt
  callCollectGarbage(
    collectGarbage: _gridDelegate.collectGarbage,
    leadingGarbage: leadingGarbage,
    trailingGarbage: trailingGarbage,
  );

  assert(debugAssertChildListIsNonEmptyAndContiguous());
  double estimatedMaxScrollOffset;
  //zmt
  double endScrollOffset =
      _gridDelegate.getLastChildLayoutType(indexOf(lastChild!)) ==
              LastChildLayoutType.none
          ? crossAxisChildrenData.maxChildTrailingLayoutOffset!
          : childTrailingLayoutOffset(lastChild!);

  if (reachedEnd) {
    estimatedMaxScrollOffset = endScrollOffset;
  } else {
    estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
      constraints,
      firstIndex: indexOf(firstChild!),
      lastIndex: indexOf(lastChild!),
      leadingScrollOffset: childScrollOffset(firstChild!),
      trailingScrollOffset: endScrollOffset,
    );
    //zmt
    assert(estimatedMaxScrollOffset >=
        endScrollOffset - childScrollOffset(firstChild!)!);
  }

  ///zmt
  final double result =
      handleCloseToTrailingEnd(closeToTrailing, endScrollOffset);
  if (result != endScrollOffset) {
    endScrollOffset = result;
    estimatedMaxScrollOffset = result;
  }

  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;
  //zmt
  callViewportBuilder(viewportBuilder: _gridDelegate.viewportBuilder);

  // fix hittest
  if (closeToTrailing) {
    paintExtent += closeToTrailingDistance;
  }

  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();

  // Save, for next layout.
  _previousCrossAxisChildrenData = crossAxisChildrenData;
}