Fix last element of ListView to the bottom of screen
Asked Answered
D

2

13

I am trying to implement a custom navigation drawer using Flutter. I would like to attach log out option to the bottom of the drawer. The problem is that number of elements above log out option is unknow (from 3 to 17).

So if these widgets take half of the space of a drawer, then log out option will be on the bottom, and if there is too much of them and you have to scroll to see them all, then the log out option will be simply the last.

I am also trying to give the first two options a green background color. Which widget tree would you recommend me? I had a thought about the ListView widget, it takes List of widgets as an argument in constructor.

Therefore I can solve the different background color for the first two items. But I still can't figure out how to attach the log out option to the bottom. In this case it's at the bottom of drawer, but it can happen, that other options will be bigger than screen size and in that case, it should be placed at the bottom of whole list.

EDIT: I've add a design to the question. The logout option is the one called Odhlášení. In this case it's at the bottom of drawer, but it can happen, that other options will be bigger than the screen size and in that case, it should be placed at the bottom of whole list.

Design: Design example

Diazonium answered 28/3, 2019 at 7:46 Comment(4)
What have you tried? Any code?Oma
Hey, I don't have any code yet. I am just thinking about the layout and I can't figure anything out. I also didn't ask for any specific code, all I need is a hint about what widgets to use.Diazonium
You can always upload your image to an image hosting site (like Imgur) and just attach the link to it.Stitt
Thanks for the update, I now understand what you mean @EllaGogoStitt
S
32

You can simply use ListView to manage the "17" navigation options. Wrap this ListView inside an Column. The ListView will be the first child of the Column the second child, therefore placing at the bottom, will be your logout action.

If you are using transparent widgets (like ListTile) inside your ListView to display the navigation options, you can simply wrap it inside a Container. The Container, besides many other widgets, allows you to set a new background color with its color attribute.

Using this approach the widget tree would look like the following:

- Column                 // Column to place your LogutButton always below the ListView
  - ListView             // ListView to wrap all your navigation scrollable
    - Container          // Container for setting the color to green
      - GreenNavigation
    - Container
      - GreenNavigation
    - Navigation
    - Navigation
    - ...
  - LogOutButton

Update 1 - Sticky LogOutButton : To achieve the LogOutButton sticking to the end of the ListView you'll neeed to do two things:

  1. Replace the Expanded with an Flexible
  2. Set shrinkWrap: true inside the ListView

Update 2 - Spaced LogOutButton until large List: Achieving the described behavior is a more difficult step. You'll have to check if the ListView exceeds the screen and is scrollable.

To do this I wrote this short snippet:

  bool isListLarge() {
    return controller.positions.isNotEmpty && physics.shouldAcceptUserOffset(controller.position);
  }

It will return true if the ListView exceeds its limitations. Now we can refresh the state of the view, depending on the result of isListViewLarge. Below again a full code example.


Standalone code example (Update 2: Spaced LogOutButton until large List):

Demo Update 2

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        drawer: MyDrawer(),
      ),
    );
  }
}

class MyDrawer extends StatefulWidget {
  @override
  _MyDrawerState createState() => _MyDrawerState();
}

class _MyDrawerState extends State<MyDrawer> {
  ScrollController controller = ScrollController();
  ScrollPhysics physics = ScrollPhysics();

  int entries = 4;

  @override
  Widget build(BuildContext context) {
    Widget logout = IconButton(
        icon: Icon(Icons.exit_to_app),
        onPressed: () => {setState(() => entries += 4)});

    List<Widget> navigationEntries = List<int>.generate(entries, (i) => i)
        .map<Widget>((i) => ListTile(
              title: Text(i.toString()),
            ))
        .toList();

    if (this.isListLarge()) {  // if the List is large, add the logout to the scrollable list
      navigationEntries.add(logout);
    }

    return Drawer(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,  // place the logout at the end of the drawer
        children: <Widget>[
          Flexible(
            child: ListView(
              controller: controller,
              physics: physics,
              shrinkWrap: true,
              children: navigationEntries,
            ),
          ),
          this.isListLarge() ? Container() : logout // if the List is small, add the logout at the end of the drawer
        ],
      ),
    );
  }

  bool isListLarge() {
    return controller.positions.isNotEmpty && physics.shouldAcceptUserOffset(controller.position);
  }
}

Standalone code example (Update 1: Sticky LogOutButton):

Demo Updated

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        drawer: MyDrawer(),
      ),
    );
  }
}

class MyDrawer extends StatefulWidget {
  @override
  _MyDrawerState createState() => _MyDrawerState();
}

class _MyDrawerState extends State<MyDrawer> {
  int entries = 4;

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: <Widget>[
          Flexible(
            child: ListView(
              shrinkWrap: true,
              children: List<int>.generate(entries, (i) => i)
                  .map((i) => ListTile(
                        title: Text(i.toString()),
                      ))
                  .toList(),
            ),
          ),
          IconButton(
              icon: Icon(Icons.exit_to_app),
              onPressed: () => {setState(() => entries += 4)})
        ],
      ),
    );
  }
}

Standalone code example (Old: Sticking to bottom):

Demo

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        drawer: MyDrawer(),
      ),
    );
  }
}

class MyDrawer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: <Widget>[
          Expanded(
            child: ListView(
              children: List<int>.generate(40, (i) => i + 1)
                  .map((i) => ListTile(
                        title: Text(i.toString()),
                      ))
                  .toList(),
            ),
          ),
          IconButton(icon: Icon(Icons.exit_to_app), onPressed: () => {})
        ],
      ),
    );
  }
}
Stitt answered 28/3, 2019 at 9:17 Comment(13)
Hi, thanks very much for your answer, but it's not what I am trying to achieve. In your example log out option is above other options, I want it to be on the same layer as everything else. If other options on he list exceed screen height, then log out option will be last element on that list. But if there is let's say only 4 options, then log out option will still be the last, but also it will be attached to the bottom of the screen. Do you understand me?Diazonium
Okay, no problem we can simply use shrinkWrap. I'll update my answer.Stitt
(and replace the Expanded with an FlexibleStitt
Thanks for update, we'll its definitely closer to what I want.. I managed to upload design of drawer to imgur. Here's the link imgur.com/a/nFK4ap0Diazonium
So which questions leaves my answer open to you? I had a look at your design and the only real difference I see is the green background at the top?Stitt
The difference I see is in the initial state of your gif. The log out option sticks to the list instead of being attached to the bottom of the screen. This behavior you implemented is desired only if options before log out exceed the height of the screen.Diazonium
Would it be sufficient to wrap IconButton in Alignment widget and set alignment to bottom left?Diazonium
This makes it a bit more complicated, the ListView has an infnite height, therefore there is not bottom.Stitt
I am not a native speaker, so it's hard to explain what I want to achieve. If you could wait a few hours, I can upload more pictures that will show what I need. Because now I am probably just wasting your time as you try to implement things I don't need. Sorry for that.Diazonium
I already understood what you are trying to achieve, I think I've found a solution. Give me 10min and I'll update my answer.Stitt
Thank you, this is exactly what I wanted! And I am glad that I asked, cause I would never figure this one out on my own.Diazonium
Hey, so I've just implemented your solution for myself and found out, that it does not work, if you start with more items (e.g. entries = 20). For some reason controller.positions.isNotEmpty() returns false, and therefore also isListLarge() method. Any thoughts how to overcome this problem?Diazonium
I'll have a look at it tomorrow.Stitt
N
0

I know my answer might be late, but this is for other people looking for another simple opinion in the same matter with a simple widget meant for that.

Short Answer:

You can just use the Spacer widget which fills the remaining space automatically without the need to put your widgets on different layers.

Long Answer:

You can still use a SizedBox with a height of double.maxFinite (which was the first thing I thought of) but it would throw your widget out of the viewport with a renderflex error so it's not that great in our scenario and we don't want any errors.

Or you can use a Stack widget and align your logout button in the bottomCenter, but we'd be adding more code to maintain.

So with these solutions there is always a catch.

Result

This is the full code of my drawer, feel free to use it as you like:

Note: Some might find it a bit excessive to separate the names, widgets and icons in separate maps, but this is my way of doing it since it becomes very painful to maintain the codebase when you have more pages or if you want to make quick changes the the name, the icons or the widgets, since the styling will get in the way and you'll have to scroll every time to find your ListTile.

So feel free to edit the code if you find the structure a bit overkill for your scenario.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class AppDrawer extends StatelessWidget {
  AppDrawer({super.key});
  final Map<String, Widget> pages = {
      "Page 1": const Placeholder(),
      "Page 2": const Placeholder(),
      "Page 3": const Placeholder(),
      "Page 4": const Placeholder(),
      "Page 5": const Placeholder(),
    };
    final Map<String, IconData> icons = {
      "Page 1": Icons.home_filled,
      "Page 2": Icons.edit_document,
      "Page 3": CupertinoIcons.building_2_fill,
      "Page 4": Icons.all_inbox_rounded,
      "Page 5": CupertinoIcons.mail_solid,
    };

  @override
  Widget build(BuildContext context) {
    List<Widget> drawerContent = [
      const UserAccountsDrawerHeader(
        accountName: Text("black-purple"),
        accountEmail: Text("[email protected]"),
        currentAccountPicture: CircleAvatar(),
      ),
      ...pages.keys
          .map(
            (page) => ListTile(
              title: Text(page),
              leading: Icon(
                icons[page],
              ),
              onTap: () => Get.to(pages[page]),
            ),
          )
          .toList(),
      const Spacer(),
      ListTile(
        title: const Text("Logout"),
        leading: const Icon(Icons.logout),
        onTap: () {},
      )
    ];
    return Drawer(
      child: Column(children: drawerContent),
    );
  }
}

Hope this helps :)

Helpful packages I user while making this simple example (in case you still haven't had the chance to use them in one of your apps):

  • Responsive Sizer To get the exact amount of screen height or width you want (example: 20.h == 20% of screen height / 13.w == 13% of screen width)
  • GetX To easily navigate between pages with as simple as typing Get.to(YourWidget())

Here is a quick screenshot to show you the result I got.

Nereus answered 20/6, 2023 at 1:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.