Miguel Ruivo
Miguel Ruivo

Reputation: 17756

How to create a scroll view with fixed footer with Flutter?

I would like to create a view that has to have a Column with a scroll view (e.g. something like SingleChildScrollView) and a footer regardless of the screen size. If the screen is big enough, it will use the empty space between the scroll and the footer, if not, it will expand and only make the widget above the footer scrollable.

It's more or less like Listview with scrolling Footer at the bottom but with a diference that I want the keyboard to overflow the footer and it also should stay in place.

Something like

example

return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: <Widget>[
          SingleChildScrollView(
            child: Padding(
              padding: const EdgeInsets.only(left: 30.0, right: 30.0, top: 80.0),
              child: Form(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: <Widget>[
                   // Multiple widgets and form fields
                  ],
                ),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.only(top: 50.0),
            child: SafeArea(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: <Widget>[
                  // Footer widgets
                ],
              ),
            ),
          )
        ],
      ),
    );

Upvotes: 22

Views: 27182

Answers (8)

Dim Dim
Dim Dim

Reputation: 1

Tricky, but sticky.

The only way I found in a few hours.

P.S. I need a header card, a message list, and a submit message footer, so SingleChildScrollView doesn't seem to be the best.

return CustomScrollView(
  controller: controller,
  slivers: [
    YourAwesomeContent(),
    SliverLayoutBuilder(
      builder: (context, constraints) {
        final minHeight = constraints.remainingPaintExtent - controller.offset;

        return SliverToBoxAdapter(
          child: ConstrainedBox(
            constraints: BoxConstraints(
              minHeight: minHeight > 0 ? minHeight : 0,
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [YourAwesomeFooter()],
            ),
          ),
        );
      },
    ),
  ],
);

Upvotes: 0

Prakash pratap Singh
Prakash pratap Singh

Reputation: 113

To create a footer with scrollable screen we can use stack widget with ListView.builder and SingleChildScrollview as:

    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")),
          ),
        ],
      ),
    );

Upvotes: 1

Fernando Batista
Fernando Batista

Reputation: 169

The accepted solution works in many cases, but it becomes tricky when you want to use something like a ListView because it can't provide an intrinsic height. I tried to find some different solution, and turns out I could, and it seems more flexible. I managed to solve this situation using slivers. Where the content is inside a sliver, and the footer is also inside a sliver.

Tip: Watch "The Boring Flutter Development Show, Ep. 12", which is all about slivers.

return Scaffold(
  body: CustomScrollView(
    shrinkWrap: true,
    slivers: [
      SliverToBoxAdapter(
        child: Column(
          children: [
            //content widgets
          ],
        ),
      ),
      SliverFillRemaining(
        hasScrollBody: false,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            //footer widgets,
          ],
        ),
      ),
    ],
  ),
);

Upvotes: 11

abhay tripathi
abhay tripathi

Reputation: 4022

For those who were looking to implement just footer with scrollview in a simple way, below code might help :

Scaffold(
      appBar: buildAppBar('Some cool appbar'),
      body: Column(
        children: [
          Expanded(
            child: SingleChildScrollView(
              child: Column(
                children: [
                  PackageCard(),
                  PackageCard(),
                  PackageCard(),
                ],
              ),
            ),
          ),
          Container(
            child: Text('Your super cool Footer'),
            color: Colors.amber,
          )
        ],
      ),
    );

Visual representation:-

---Column
    |
    |---Expanded--
                 |-SingleChildScrollView (column /YOUR SCROLLABLE VIEW)
    |
    |-Container(YOUR FOOTER)

I used expanded with SinglechildScrollView over here

Upvotes: 39

Sander Roest
Sander Roest

Reputation: 859

Although the accepted answer seems to work on mobile devices, problems occur when the width is (much) bigger than the height. When that happens, the IntrinsicHeight acts like an AspectRatio, and the height increases so the footer is pushed off the screen.

I think the problem is with the definition used by the IntrinsicHeight of its internal workings:

... instead size itself to a more reasonable height.

I can confirm that @Rémi's solutions works also in those cases.

It deserves to be a standard widget provided by the Flutter framework.

Upvotes: 0

Levi Chimezie
Levi Chimezie

Reputation: 61

How I solved this was to wrap the fixed Footer and The SingleChildScrollView in a Stack widget then align the Footer accordingly.


class ScrollableFooter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        SingleChildScrollView(
          child: Container(
            padding: EdgeInsets.all(5),
            child: Column(
              children: <Widget>[
                // Your body widgets here
              ],
            ),
          ),
        ),
        Align(
          alignment: Alignment.bottomCenter,
          child: // Your fixed Footer here,
        ),
      ],
    );
  }
}

Upvotes: 6

Miguel Ruivo
Miguel Ruivo

Reputation: 17756

Even though the Rémi answer being right, I've actually found an easier way to achieve what I was looking for by just combining the LayoutBuilder with the IntrinsicHeight.

class ScrollableFooter extends StatelessWidget {
  @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>[
               // Your body widgets here
                Expanded(
                  child: Align(
                    alignment: Alignment.bottomCenter,
                    child: // Your footer widget
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    });
  }
}

Upvotes: 17

R&#233;mi Rousselet
R&#233;mi Rousselet

Reputation: 277037

The difficulty is that Column and SingleChildScrollView have a hard time working together because one needs constraints and the other removes them.

The trick is to use a CustomMultiChildLayout and do the calculations yourself. Helped by MediaQuery to obtain the size of the keyboard, so that the footer can disappear to leave more room for the content.

Here's a reusable widget that does it for you:

class FooterLayout extends StatelessWidget {
  const FooterLayout({
    Key key,
    @required this.body,
    @required this.footer,
  }) : super(key: key);

  final Container body;
  final Container footer;

  @override
  Widget build(BuildContext context) {
    return CustomMultiChildLayout(
      delegate: _FooterLayoutDelegate(MediaQuery.of(context).viewInsets),
      children: <Widget>[
        LayoutId(
          id: _FooterLayout.body,
          child: body,
        ),
        LayoutId(
          id: _FooterLayout.footer,
          child: footer,
        ),
      ],
    );
  }
}

enum _FooterLayout {
  footer,
  body,
}

class _FooterLayoutDelegate extends MultiChildLayoutDelegate {
  final EdgeInsets viewInsets;

  _FooterLayoutDelegate(this.viewInsets);

  @override
  void performLayout(Size size) {
    size = Size(size.width, size.height + viewInsets.bottom);
    final footer =
        layoutChild(_FooterLayout.footer, BoxConstraints.loose(size));

    final bodyConstraints = BoxConstraints.tightFor(
      height: size.height - max(footer.height, viewInsets.bottom),
      width: size.width,
    );

    final body = layoutChild(_FooterLayout.body, bodyConstraints);

    positionChild(_FooterLayout.body, Offset.zero);
    positionChild(_FooterLayout.footer, Offset(0, body.height));
  }

  @override
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) {
    return true;
  }
}

Used as such:

FooterLayout(
  body: body,
  footer: footer,
),

Upvotes: 7

Related Questions