Adaptive size layout support for horizontal and vertical lists of Flutter tips

Does this topic look a bit abstract today? List nesting again? Didn't you share "Various Fancy Nesting of ListView and PageView" before ? So what's different about the adaptive size layout support this time?

It is only needed in some strange scenarios.

First, let's look at the following piece of code. The basic logic is: we hope verticalthat ListVieweach Item in is self-adaptive in size according to its content, and there will be horizontalsuch ListViewa child in Item.

horizontalListViewWe also hope that it can childrenadapt to its own size. So what do you think is wrong with this code? Does it work properly?


Widget build(BuildContext context) {
    
    
  return Scaffold(
    appBar: AppBar(
      title: new Text(""),
    ),
    extendBody: true,
    body: Container(
      color: Colors.white,
      child: ListView(
        children: [
          ListView(
            scrollDirection: Axis.horizontal,
            children: List<Widget>.generate(50, (index) {
    
    
              return Padding(
                padding: EdgeInsets.all(2),
                child: Container(
                  color: Colors.blue,
                  child: Text(List.generate(
                          math.Random().nextInt(10), (index) => "TEST\n")
                      .toString()),
                ),
              );
            }),
          ),
          Container(
            height: 1000,
            color: Colors.green,
          ),
        ],
      ),
    ),
  );
}

The answer is no, because verticalin nestedListView , and the horizontal does not specify the height, and the vertical does not specify , so we will get the error shown in the following figure:horizontalListViewListViewListViewitemExtent

Why is there such a problem? To put it simply, we all know that Flutter is a layout process Sizethat passes child needs to determine its own size through the constraints of the parent, and then the parent SizeDetermine its own size based on the returned by child .

Those who are interested in this part can read "Take you to understand different Flutter"

But it is a bit special for the sliding control, because the sliding control needs to be "infinite" in theory on the main axis of its sliding direction, so for the sliding control, it needs to have a fixed size of "window". That is, ViewPortthis "window" needs to have a size in the direction of the main axis.

For example ListView, under normal circumstances there is one ViewPort, and then SliverListbuild , and then ViewPortmove correspondingly under the "window" through gestures, so as to achieve the effect of sliding the list.

If you are interested, you can read "A Different Angle Takes You to Understand the Implementation of Sliding Lists in Flutter"

Then let's go back to the nested question verticalabove :ListViewhorizontalListView

  • Because the vertical ListViewis not set itemExtent, each of its children will not have a fixed height, because our requirement is that each Item adapts to its height according to its own needs.
  • The horizontal one ListViewdoes not have a clear height, and the vertical ListViewheight theory of the parent is "infinite height", so the horizontal one ListViewcannot be calculated to obtain an effective height.

In addition, ListViewunlike Row/ Column and other controls, childrenthe theory is "infinite", and the parts that are not displayed are generally not laid out and drawn, so it is not possible to calculate the height of all controls like Row/ Column to determine its own height .

So what are the ways to break it? Currently, two solutions are available.

SingleChildScrollView

As shown in the following code, the easiest thing to do first is to ListViewreplace SingleChildScrollView, because there ListViewis SingleChildScrollViewonly one child, so its is ViewPortalso special.

return Scaffold(
  appBar: AppBar(
    title: new Text("ControllerDemoPage"),
  ),
  extendBody: true,
  body: Container(
    color: Colors.white,
    child: ListView(
      children: [
        SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: List<Widget>.generate(50, (index) {
    
    
              return Padding(
                padding: EdgeInsets.all(2),
                child: Container(
                  color: Colors.blue,
                  child: Text(List.generate(
                          math.Random().nextInt(10), (index) => "TEST\n")
                      .toString()),
                ),
              );
            }),
          ),
        ),
        Container(
          height: 1000,
          color: Colors.green,
        ),
      ],
    ),
  ),
)

In SingleChildScrollViewthe _RenderSingleChildViewportof , the size of the child can be easily obtained through child!.layoutafter , and then the combined Rowheight of all the children can be calculated in conjunction with , so that a horizontal list effect can be achieved.

After running, the result is shown in the figure below. You can see that the vertical and ListView horizontal SingleChildScrollVieware rendered correctly, but there is an "uneven" height layout at this time.

As shown in the following code, we only need to Rownest IntrinsicHeightto align its internal height, because IntrinsicHeightduring layout, it will call child's in advance getMaxIntrinsicHeightto obtain the child's height, and modify the constraint information passed to the child by the parent.

SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: IntrinsicHeight(
    child: Row(
      children: List<Widget>.generate(50, (index) {
    
    
        return Padding(
          padding: EdgeInsets.all(2),
          child: Container(
            alignment: Alignment.bottomCenter,
            color: Colors.blue,
            child: Text(List.generate(
                    math.Random().nextInt(10), (index) => "TEST\n")
                .toString()),
          ),
        );
      }),
    ),
  ),
),

The running effect is as follows. It can be seen that the heights of all horizontal items are the same at this time, but this solution also has two fatal problems:

  • SingleChildScrollViewHere is the height Rowcalculated , that is, all children need to be calculated at one time during layout. If the list is too long, performance loss will occur
  • IntrinsicHeightThe process of calculating the layout will be time-consuming, and may reach O(N²). Although Flutter has cached this part of the calculation results, it does not hinder its time-consuming.

UnboundedListView

The second solution is to customize based ListViewon . Didn’t we say ListViewthat won’t count the size of children like Rowthat ? Then we can customize a UnboundedListViewto count.

This part of the idea first came from Github: https://gist.github.com/vejmartin/b8df4c94587bdad63f5b4ff111ff581c

First of all, we ListViewdefine UnboundedListView , mixinthrough the overridecorresponding Viewportand Sliver, that is:

  • buildChildLayoutReplace SliverListthe in with our customUnboundedSliverList
  • buildViewportReplace Viewportthe in with our customUnboundedViewport
  • buildSliversProcess logic in Paddingand SliverPaddingreplace with customUnboundedSliverPadding
class UnboundedListView = ListView with UnboundedListViewMixin;


/// BoxScrollView 的基础上
mixin UnboundedListViewMixin on ListView {
    
    
  
  Widget buildChildLayout(BuildContext context) {
    
    
    return UnboundedSliverList(delegate: childrenDelegate);
  }

  
  Widget buildViewport(
    BuildContext context,
    ViewportOffset offset,
    AxisDirection axisDirection,
    List<Widget> slivers,
  ) {
    
    
    return UnboundedViewport(
      axisDirection: axisDirection,
      offset: offset,
      slivers: slivers,
      cacheExtent: cacheExtent,
    );
  }

  
  List<Widget> buildSlivers(BuildContext context) {
    
    
    Widget sliver = buildChildLayout(context);
    EdgeInsetsGeometry? effectivePadding = padding;
    if (padding == null) {
    
    
      final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
      if (mediaQuery != null) {
    
    
        // Automatically pad sliver with padding from MediaQuery.
        final EdgeInsets mediaQueryHorizontalPadding =
            mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
        final EdgeInsets mediaQueryVerticalPadding =
            mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
        // Consume the main axis padding with SliverPadding.
        effectivePadding = scrollDirection == Axis.vertical
            ? mediaQueryVerticalPadding
            : mediaQueryHorizontalPadding;
        // Leave behind the cross axis padding.
        sliver = MediaQuery(
          data: mediaQuery.copyWith(
            padding: scrollDirection == Axis.vertical
                ? mediaQueryHorizontalPadding
                : mediaQueryVerticalPadding,
          ),
          child: sliver,
        );
      }
    }

    if (effectivePadding != null)
      sliver =
          UnboundedSliverPadding(padding: effectivePadding, sliver: sliver);
    return <Widget>[sliver];
  }
}

The next step is to implement UnboundedViewport, the same routine:

  • Firstly, Viewportbased on , by modifyingcreateRenderObject to ourRenderViewPortUnboundedRenderViewport
  • Based on the custom logic of RenderViewportadding andperformLayout , it is actually adding a parameter, which is obtained through statistics inlayoutChildSequenceunboundedSizeRenderSliver
class UnboundedViewport = Viewport with UnboundedViewportMixin;
mixin UnboundedViewportMixin on Viewport {
    
    
  
  RenderViewport createRenderObject(BuildContext context) {
    
    
    return UnboundedRenderViewport(
      axisDirection: axisDirection,
      crossAxisDirection: crossAxisDirection ??
          Viewport.getDefaultCrossAxisDirection(context, axisDirection),
      anchor: anchor,
      offset: offset,
      cacheExtent: cacheExtent,
    );
  }
}

class UnboundedRenderViewport = RenderViewport
    with UnboundedRenderViewportMixin;
mixin UnboundedRenderViewportMixin on RenderViewport {
    
    
  
  bool get sizedByParent => false;

  double _unboundedSize = double.infinity;

  
  void performLayout() {
    
    
    BoxConstraints constraints = this.constraints;
    if (axis == Axis.horizontal) {
    
    
      _unboundedSize = constraints.maxHeight;
      size = Size(constraints.maxWidth, 0);
    } else {
    
    
      _unboundedSize = constraints.maxWidth;
      size = Size(0, constraints.maxHeight);
    }

    super.performLayout();

    switch (axis) {
    
    
      case Axis.vertical:
        offset.applyViewportDimension(size.height);
        break;
      case Axis.horizontal:
        offset.applyViewportDimension(size.width);
        break;
    }
  }

  
  double layoutChildSequence({
    
    
    required RenderSliver? child,
    required double scrollOffset,
    required double overlap,
    required double layoutOffset,
    required double remainingPaintExtent,
    required double mainAxisExtent,
    required double crossAxisExtent,
    required GrowthDirection growthDirection,
    required RenderSliver? advance(RenderSliver child),
    required double remainingCacheExtent,
    required double cacheOrigin,
  }) {
    
    
    crossAxisExtent = _unboundedSize;
    var firstChild = child;

    final result = super.layoutChildSequence(
      child: child,
      scrollOffset: scrollOffset,
      overlap: overlap,
      layoutOffset: layoutOffset,
      remainingPaintExtent: remainingPaintExtent,
      mainAxisExtent: mainAxisExtent,
      crossAxisExtent: crossAxisExtent,
      growthDirection: growthDirection,
      advance: advance,
      remainingCacheExtent: remainingCacheExtent,
      cacheOrigin: cacheOrigin,
    );

    double unboundedSize = 0;
    while (firstChild != null) {
    
    
      if (firstChild.geometry is UnboundedSliverGeometry) {
    
    
        final UnboundedSliverGeometry childGeometry =
            firstChild.geometry as UnboundedSliverGeometry;
        unboundedSize = math.max(unboundedSize, childGeometry.crossAxisSize);
      }
      firstChild = advance(firstChild);
    }
    if (axis == Axis.horizontal) {
    
    
      size = Size(size.width, unboundedSize);
    } else {
    
    
      size = Size(unboundedSize, size.height);
    }

    return result;
  }
}

Next, we inherit and SliverGeometrycustomize one UnboundedSliverGeometry, mainly by adding a crossAxisSizeparameter to record the current statistics of the sub-axis height, so that the above ViewPortcan be obtained.

class UnboundedSliverGeometry extends SliverGeometry {
    
    
  UnboundedSliverGeometry(
      {
    
    SliverGeometry? existing, required this.crossAxisSize})
      : super(
          scrollExtent: existing?.scrollExtent ?? 0.0,
          paintExtent: existing?.paintExtent ?? 0.0,
          paintOrigin: existing?.paintOrigin ?? 0.0,
          layoutExtent: existing?.layoutExtent,
          maxPaintExtent: existing?.maxPaintExtent ?? 0.0,
          maxScrollObstructionExtent:
              existing?.maxScrollObstructionExtent ?? 0.0,
          hitTestExtent: existing?.hitTestExtent,
          visible: existing?.visible,
          hasVisualOverflow: existing?.hasVisualOverflow ?? false,
          scrollOffsetCorrection: existing?.scrollOffsetCorrection,
          cacheExtent: existing?.cacheExtent,
        );

  final double crossAxisSize;
}

As shown in the following code, we finally SliverListimplement UnboundedSliverList, which is also the core logic, mainly to implement performLayoutthe part of the code. We need to add custom logic to some nodes on the basis of the original code to count each node participating in the layout. Item height, so as to get a maximum value.

The code looks very long, but in fact we have added very little.

class UnboundedSliverList = SliverList with UnboundedSliverListMixin;
mixin UnboundedSliverListMixin on SliverList {
    
    
  
  RenderSliverList createRenderObject(BuildContext context) {
    
    
    final SliverMultiBoxAdaptorElement element =
        context as SliverMultiBoxAdaptorElement;
    return UnboundedRenderSliverList(childManager: element);
  }
}

class UnboundedRenderSliverList extends RenderSliverList {
    
    
  UnboundedRenderSliverList({
    
    
    required RenderSliverBoxChildManager childManager,
  }) : super(childManager: childManager);

  // See RenderSliverList::performLayout
  
  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;
    BoxConstraints childConstraints = constraints.asBoxConstraints();
    int leadingGarbage = 0;
    int trailingGarbage = 0;
    bool reachedEnd = false;

    if (constraints.axis == Axis.horizontal) {
    
    
      childConstraints = childConstraints.copyWith(minHeight: 0);
    } else {
    
    
      childConstraints = childConstraints.copyWith(minWidth: 0);
    }

    double unboundedSize = 0;

    // should call update after each child is laid out
    updateUnboundedSize(RenderBox? child) {
    
    
      if (child == null) {
    
    
        return;
      }
      unboundedSize = math.max(
          unboundedSize,
          constraints.axis == Axis.horizontal
              ? child.size.height
              : child.size.width);
    }

    unboundedGeometry(SliverGeometry geometry) {
    
    
      return UnboundedSliverGeometry(
        existing: geometry,
        crossAxisSize: unboundedSize,
      );
    }

    // 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 = unboundedGeometry(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 = unboundedGeometry(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);
      updateUnboundedSize(earliestUsefulChild);
      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;
          updateUnboundedSize(earliestUsefulChild);
          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 = unboundedGeometry(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 = unboundedGeometry(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);
        updateUnboundedSize(earliestUsefulChild);
        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 = unboundedGeometry(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);
      updateUnboundedSize(earliestUsefulChild);
      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,
          );
          updateUnboundedSize(child);
          if (child == null) {
    
    
            // We have run out of children.
            return false;
          }
        } else {
    
    
          // Lay out the child.
          child!.layout(childConstraints, parentUsesSize: true);
          updateUnboundedSize(child!);
        }
        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 = unboundedGeometry(
          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 = unboundedGeometry(
      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();
  }
}

Although the above code is very long, in fact, many of them are RenderSliverListour own source code, as shown in the figure below, the only thing we really modify and add is this:

  • Increment updateUnboundedSizeand unboundedGeometryare used to record layout height and buildUnboundedSliverGeometry
  • SliverGeometry Change all original toUnboundedSliverGeometry
  • Call it after all the positions layoutinvolved updateUnboundedSize, because we can get the child after it is laid out Size, and then we can get their maximum value by statistics, which can be UnboundedSliverGeometryreturned ViewPort.

Finally, as shown in the following code, it will be UnboundedListViewadded to the vertical ListView at the beginning. After running, you can see that the height of the list itself is changing as you slide horizontally.

return Scaffold(
  appBar: AppBar(
    title: new Text("ControllerDemoPage"),
  ),
  extendBody: true,
  body: Container(
    color: Colors.white,
    child: ListView(
      children: [
        SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: IntrinsicHeight(
            child: Row(
              children: List<Widget>.generate(50, (index) {
    
    
                return Padding(
                  padding: EdgeInsets.all(2),
                  child: Container(
                    alignment: Alignment.bottomCenter,
                    color: Colors.blue,
                    child: Text(List.generate(
                            math.Random().nextInt(10), (index) => "TEST\n")
                        .toString()),
                  ),
                );
              }),
            ),
          ),
        ),
        UnboundedListView.builder(
            scrollDirection: Axis.horizontal,
            itemCount: 100,
            itemBuilder: (context, index) {
    
    
              print('$index');
              return Padding(
                padding: EdgeInsets.all(2),
                child: Container(
                  height: index * 1.0 + 10,
                  width: 50,
                  color: Colors.blue,
                ),
              );
            }),
        Container(
          height: 1000,
          color: Colors.green,
        ),
      ],
    ),
  ),
);

So does this meet our needs? As shown in the following code, if I modify the code as shown below, after running, you can see that the horizontal list at this time becomes jagged.

UnboundedListView.builder(
    scrollDirection: Axis.horizontal,
    itemCount: 100,
    itemBuilder: (context, index) {
    
    
      print('$index');
      return Container(
        padding: EdgeInsets.all(2),
        child: Container(
          width: 50,
          color: Colors.blue,
          alignment: Alignment.bottomCenter,
          child: Text(List.generate(
                  math.Random().nextInt(15), (index) => "TEST\n")
              .toString()),
        ),
      );
    }),

But at this time we can't solve it in a IntrinsicHeightsimilar way, because ListViewthe Items in are all dynamically processed, that is, the addition and destruction of Items within a specific cheap range need to be processed during layout. Specifically, performLayoutin andscrollOffset will be used to determine the layout of Items targetEndScrollOffsetscope.

As a result, when we access through firstChildthe linked list structure, we cannot layoutget the child before because it has not been added to the linked list at this time, and it is also limited by insertAndLayoutLeadingChildthe insertAndLayoutChildcoupling implementation and private method of and , which is inconvenient and simple Rewrite support.

But "there is no such thing as a path", since we can't layouthandle it before child, then we can do one more redundant layout layoutafter , as shown in the following code:

  • We first unboundedSizeextract as UnboundedRenderSliverLista global variable in
  • didFinishLayoutBefore , through firstChildthe linked list structure, re-pass layout(childConstraints.tighten(height: unboundedSize)the layout once more
  double unboundedSize = 0;

  // See RenderSliverList::performLayout
  
  void performLayout() {
    
    

    ····
    var tmpChild = firstChild;
    while (tmpChild != null) {
    
    
      tmpChild.layout(childConstraints.tighten(height: unboundedSize),
          parentUsesSize: true);
      tmpChild = childAfter(tmpChild);
    }

    childManager.didFinishLayout();
    ····
  }

After running, you can see that the lists are all aligned at this time, and the loss is that the child will have a double layout. For the performance loss here, compared SingleChildScrollViewwith implementation, you can choose which logic to use according to the actual scene. Of course, for performance considerations It is not necessary to give the horizontal ListViewa height, such an implementation is the optimal solution .

Well, this little trick is solved here. I don’t know if you have any ideas about similar implementations. If you have a better solution, please leave a message for discussion.

The complete code is available at: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/un_bounded_listview.dart

Guess you like

Origin blog.csdn.net/ZuoYueLiang/article/details/130368280