Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I detect if a widget will overflow its constraints before it is rendered?

I have a PageView and I want to use it to show an indefinite amount of widgets in columns. I'm trying to add widgets to a column until it would overflow, and then build the next page beginning with the widget that would have overflowed.

Diagram of problem

So far, this is what I've hacked together in an effort to get something working:

class OverflowSliverChildDelegate extends SliverChildDelegate {
  OverflowSliverChildDelegate({
    @required this.itemBuilder,
  }) : assert(itemBuilder != null),
       super();

  /// Used to build individual items to slot into pages.
  final IndexedWidgetBuilder itemBuilder;

  @override
  Widget build(BuildContext context, int index) {
    return LayoutBuilder(
      builder: (context, constraints) {
        // todo: handle this better
        if (remainingHeight <= 0) throw Error();

        var itemIndex = 0;
        return PageView.builder(
          itemBuilder: (context, pageIndex) {
            final remainingHeight = constraints.maxHeight;
            return Column(
              // todo: move this generator to a separate method
              children: () sync* {
                // Build widgets until we run out of vertical space.
                while (remainingHeight > 0) {
                  // todo: layout widget and get its height
                  final widget = itemBuilder(context, itemIndex++);

                  // remainingHeight -= widget.height

                  yield widget;
                }
              }(),
            );
          },
        );
      },
    );
  }
}

If I was only concerned with text, I could use a TextPainter to layout the text and get the height. Is there a way to do this for arbitrary widgets (which may or may not have children of their own)?


Update

After looking into the Offstage widget, I tried this but Flutter is asserting that I don't call the size getter from outside the object. I felt so close to a solution too.

          while (remainingHeight > 0) {
              final widget = itemBuilder(context, itemIndex++);

              final renderObj = ConstrainedBox(
                child: widget,
                constraints: constraints.copyWith(
                  maxHeight: remainingHeight,
                ),
              ).createRenderObject(context);

              renderObj.layout(constraints);

              remainingHeight -= renderObj.size.height;

              if (remainingHeight > 0) {
                yield widget;
              }
            }
like image 872
Alex Meuer Avatar asked Sep 20 '25 22:09

Alex Meuer


1 Answers

I ended up digging into Flutter's source code for the sliver list (essentially the backend for the ListView widget) and I was able to figure out how it decided when it had enough children to fill the viewport and the "cache extent".

I've started a fresh Flutter project in order to try different approaches but I've cleaned it up a little and published it to the pub.dev: https://pub.dev/packages/overflow_page_view

The result is a PageView which builds a CustomScrollView for each page. I've straight-up copied the code for the RenderSliver stuff from the Flutter source and changed the parts where it lays out its children. Ideally, I would have liked to have found a way to pass custom SliverConstraints to a ListView or other sliver widget.


As for answering the question "How can I detect if a widget will overflow its constraints before it is rendered?":

I copied the code for RenderSliverList into my own class (NoCacheRenderSliverList) and began tweaking things. It should be noted that I used NeverScrollableScrollPhysics so that I didn't have to worry about scrolling (because I don't need it).

First off, at the top of the performLayout method, I changed the targetEndScrollOffset to be the remaining paint extent and not anything to do with the scroll offset or the cache extent. Only concerned with visible area here:

final double targetEndScrollOffset = this.constraints.remainingPaintExtent;

Next, I need to change the nested bool advance() method to return false when the bottom of the child goes beyond the edge of the viewport:

  assert(child != null);
  final SliverMultiBoxAdaptorParentData childParentData =
      child.parentData as SliverMultiBoxAdaptorParentData;
  childParentData.layoutOffset = endScrollOffset;
  assert(childParentData.index == index);
  // In the original source, this is assigned directly to endScrollOffset
  final tmp = childScrollOffset(child) + paintExtentOf(child);
  if (tmp > targetEndScrollOffset) {
    return false;
  }
  endScrollOffset = tmp;
  return true;

With the above code, we'll still end up showing a single child that extends beyond the viewport. My last change is to ensure that this child is garbage collected:

  // Finally count up all the remaining children and label them as garbage.
if (child != null) {
  // By commenting out this line, we ensure the final child is garbage collected.
  // child = childAfter(child);
  while (child != null) {
    trailingGarbage += 1;
    child = childAfter(child);
  }
}

And that's all there is to it. If anyone has any improvement or needs clarification I'm happy to update this.

like image 185
Alex Meuer Avatar answered Sep 22 '25 12:09

Alex Meuer