Listview with scrolling Footer at the bottom
Asked Answered
C

5

19

I'm trying to create a scrolling list widgets which takes displays a few items and has the ability to add a footer at the bottom which scrolls along.

If the scrolling list doesn't take up the complete height, e.g. there are only 2 items, the footer should still appear at the bottom of the screen. Here are some sketches of what I'm trying to achieve

non page filling listview page filling listview

I tried calculating the vertical size that the listview needs but that'd mean that I need the know the height of the children at build time. Is there a better way?

EDIT
I'm trying to achieve the exact same thing as here but with Flutter of course.

Convey answered 2/4, 2018 at 23:37 Comment(0)
J
30

You will need to create a custom RenderBox for this. As there's no widgets which supports this out of the box.

SliverFillRemaining comes pretty close. But it's sizing/scrolling behavior is different then what you'd expect. As, if present, will almost always make the Scrollable... scrollable.

Instead, we can copy paste the sources of SliverFillRemaining. And make some edits

class SliverFooter extends SingleChildRenderObjectWidget {
  /// Creates a sliver that fills the remaining space in the viewport.
  const SliverFooter({super.child});

  @override
  RenderSliverFooter createRenderObject(BuildContext context) =>
      RenderSliverFooter();
}

class RenderSliverFooter extends RenderSliverSingleBoxAdapter {
  @override
  void performLayout() {
    final extent =
        constraints.remainingPaintExtent - math.min(constraints.overlap, 0.0);
    double childGrowthSize = .0; // added
    if (child != null) {
      // changed maxExtent from 'extent' to double.infinity
      child!.layout(
        constraints.asBoxConstraints(minExtent: extent),
        parentUsesSize: true,
      );
      childGrowthSize = constraints.axis == Axis.vertical
          ? child!.size.height
          : child!.size.width; // added
    }
    final paintedChildSize =
        calculatePaintOffset(constraints, from: 0.0, to: extent);
    assert(
        paintedChildSize.isFinite,
        'The calculated paintedChildSize '
        '$paintedChildSize for child $child is not finite.');
    assert(
        paintedChildSize >= 0.0,
        'The calculated paintedChildSize was '
        '$paintedChildSize but is not greater than or equal to zero. '
        'This can happen if the child is too big in which case it '
        'should be sized down or if the SliverConstraints.scrollOffset '
        'was not correct.');
    geometry = SliverGeometry(
      // used to be this : scrollExtent: constraints.viewportMainAxisExtent,
      scrollExtent: math.max(extent, childGrowthSize),
      paintExtent: paintedChildSize,
      maxPaintExtent: paintedChildSize,
      hasVisualOverflow: extent > constraints.remainingPaintExtent ||
          constraints.scrollOffset > 0.0,
    );
    if (child != null) {
      setChildParentData(child!, constraints, geometry!);
    }
  }
}

Here I changed one 3 things

  • Unconstrained the child maxExtent. Because if there's no more screen-space available, that would enforce a height of 0 to the footer.
  • changed SliverGeometry scrollExtent from "full screen height" to "actual available size". So that it actually only fill the remaining visible space. Not fill the screen.
  • added a minimum value to that same scrollExtent, equal to the actual footer height. So that if there's no more space remaining in the viewport, the children is simply added without any spacing around it.

We can now use it inside our CustomScrollView as usual.

End result :

    CustomScrollView(
      slivers: <Widget>[
        SliverFixedExtentList(
          itemExtent: 42.0,
          delegate: SliverChildBuilderDelegate((context, index) {
            return SizedBox.expand(
              child: Card(),
            );
          }, childCount: 42),
        ),
        SliverFooter(
          child: Align(
            alignment: Alignment.bottomCenter,
            child: Container(
              height: 42.0,
              color: Colors.red,
            ),
          ),
        ),
      ],
    ),
Jugular answered 3/4, 2018 at 1:48 Comment(7)
You should consider making a pull request for this. This should be a part of the Flutter repository imo.Convey
Agreed. I'll do it.Doralyn
@RémiRousselet Thanks for the code, works well. When there is a input field in one of the sliverlist, after minimizing the keyboard, the footer snaps back to the bottom but the remaining list all end up at the top with lot of space between the last list item and the footer, is there anyway to lower the list also.Glare
how to achieve a real footer that stays there, even if there are many items above?Railroader
@RémiRousselet Have you created the PR yet? how did it turn out?Sawyere
There is no enough upvotes for your answer... Thank you so much...Gyrfalcon
@RémiRousselet as I see now, they didn't accept your PR :) (or you didn't created it unfortunately) Thank you for the great solution!Parlance
H
10

This can be achieved by using a SingleChildScrollView with special constraints, as explained here.

Take a look at the example below:

@override
Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (BuildContext context, BoxConstraints constraints) {
      return SingleChildScrollView(
        child: ConstrainedBox(
          constraints: constraints.copyWith(
            minHeight: constraints.maxHeight,
            maxHeight: double.infinity,
          ),
          child: IntrinsicHeight(
            child: Column(
              children: <Widget>[
                Container(height: 200, color: Colors.blue),
                Container(height: 200, color: Colors.orange),
                Container(height: 200, color: Colors.green),
                Container(height: 50, color: Colors.pink),
                Expanded(
                  child: Align(
                    alignment: Alignment.bottomCenter,
                    child: Container(
                      width: double.infinity,
                      color: Colors.red,
                      padding: EdgeInsets.all(12.0),
                      child: Text('FOOTER', textAlign: TextAlign.center),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    },
  );
}

This builds the following layout:

layout

If you change the height of the pink container to something larger, say 500, you will see the footer also scrolls with the entire list.

Thanks to Simon Lightfoot for his help pointing me in the right direction!

Hautbois answered 7/4, 2019 at 20:59 Comment(1)
Way simpler than the accepted answer, and works!Brownson
S
4

To add Footer in ListView with Builder

  1. Just add +1 in itemCount for example: itemCount:list.length + 1
  2. And while returning customWidget check the condition list.length == index just return a SizedBox(height:"required height of footer ").

Code:

ListView.builder(
itemCount: list.length+1,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
  return GestureDetector(
      onTap: () {
        if (list.length == index) {
          return;
        }
        Navigator.push(
            context,
            MaterialPageRoute(
                builder: (context) => PlotDetail(list[index])));
      },
      child: list.length != index?PlotListItemWidget(list[index]):SizedBox(height: 100,));
   },
);
Scirrhous answered 12/9, 2021 at 20:52 Comment(0)
C
0

Here I've created flat_list widget which has similar specifications as in React Native's FlatList. Below will simply work.

FlatList(
+  listFooterWidget: const Footer(),
  data: items.value,
  buildItem: (item, index) {
    var person = items.value[index];

    return ListItemView(person: person);
  },
),
Christeenchristel answered 12/11, 2022 at 15:2 Comment(0)
R
0

We can use stack to build a sticky footer inside a scrollable widget like this:

Scaffold(
      body:Stack(
        alignment: Alignment.bottomCenter, 
        children: [
          ListView.builder(
            itemCount: 1,
            itemBuilder: (context, index) => SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Container( // Scrollable contents here
                color: Colors.red,
                height: 3000,
                width: 1000,
              ),
            ),
          ),
          Container( // Footer
            height:50,
            color:Colors.blue[900],
            width:MediaQuery.of(context).size.width,
            child:Center(child:Text("Footer")),
          ),
        ],
      ),
    );

All the scrollable contents will be inside Listview.builder and the content of the footer will be outside of it.

Roll answered 21/4, 2023 at 3:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.