Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter - Expand bottomNavigationBar by swiping or pressing the floatingActionButton

Tags:

flutter

dart

I have a main widget called DashboardWidget. Inside it, I have a Scaffold with BottomNavigationBar and a FloatingActionButton:

enter image description here

Now, I want to make a widget that would be dragged from the bottom by:

  1. Swiping up with the finger.
  2. Pressing on FloatingActionButton.

In other words, I want to expand the BottomNavigationBar.

Here's a design concept in case I was unclear.

The problem is, I'm not sure where to start to implement that. I've thought about removing the BottomNavigationBar and create a custom widget that can be expanded, but I'm not sure if it's possible either.

like image 406
Eliya Cohen Avatar asked May 08 '19 13:05

Eliya Cohen


People also ask

How do you make the bottom tab bar in flutter?

// This widget is the root of your application. Now create a stateful widget and name it as HomePage. Return a Scaffold from the HomePage class. Now this Scaffold is the main element that contains our bottom navigation bar.


3 Answers

Output:

enter image description here


I used a different approach and did it without AnimationController, GlobalKey etc, the logic code is very short (_handleClick).

I only used 4 variables, simple and short!

void main() => runApp(MaterialApp(home: HomePage()));

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  static double _minHeight = 80, _maxHeight = 600;
  Offset _offset = Offset(0, _minHeight);
  bool _isOpen = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xFFF6F6F6),
      appBar: AppBar(backgroundColor: Color(0xFFF6F6F6), elevation: 0),
      body: Stack(
        alignment: Alignment.bottomCenter,
        children: <Widget>[
          Align(
            alignment: Alignment.topLeft,
            child: FlatButton(
              onPressed: _handleClick,
              splashColor: Colors.transparent,
              textColor: Colors.grey,
              child: Text(_isOpen ? "Back" : ""),
            ),
          ),
          Align(child: FlutterLogo(size: 300)),
          GestureDetector(
            onPanUpdate: (details) {
              _offset = Offset(0, _offset.dy - details.delta.dy);
              if (_offset.dy < _HomePageState._minHeight) {
                _offset = Offset(0, _HomePageState._minHeight);
                _isOpen = false;
              } else if (_offset.dy > _HomePageState._maxHeight) {
                _offset = Offset(0, _HomePageState._maxHeight);
                _isOpen = true;
              }
              setState(() {});
            },
            child: AnimatedContainer(
              duration: Duration.zero,
              curve: Curves.easeOut,
              height: _offset.dy,
              alignment: Alignment.center,
              decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(30),
                    topRight: Radius.circular(30),
                  ),
                  boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.5), spreadRadius: 5, blurRadius: 10)]),
              child: Text("This is my Bottom sheet"),
            ),
          ),
          Positioned(
            bottom: 2 * _HomePageState._minHeight - _offset.dy - 28, // 56 is the height of FAB so we use here half of it.
            child: FloatingActionButton(
              child: Icon(_isOpen ? Icons.keyboard_arrow_down : Icons.add),
              onPressed: _handleClick,
            ),
          ),
        ],
      ),
    );
  }

  // first it opens the sheet and when called again it closes.
  void _handleClick() {
    _isOpen = !_isOpen;
    Timer.periodic(Duration(milliseconds: 5), (timer) {
      if (_isOpen) {
        double value = _offset.dy + 10; // we increment the height of the Container by 10 every 5ms 
        _offset = Offset(0, value);
        if (_offset.dy > _maxHeight) {
          _offset = Offset(0, _maxHeight); // makes sure it does't go above maxHeight
          timer.cancel();
        }
      } else {
        double value = _offset.dy - 10; // we decrement the height by 10 here
        _offset = Offset(0, value);
        if (_offset.dy < _minHeight) {
          _offset = Offset(0, _minHeight); // makes sure it doesn't go beyond minHeight
          timer.cancel();
        }
      }
      setState(() {});
    });
  }
}
like image 126
CopsOnRoad Avatar answered Nov 15 '22 11:11

CopsOnRoad


You can use the BottomSheet class. Here is a Medium-tutorial for using that, here is a youtube-tutorial using it and here is the documentation for the class.

The only difference from the tutorials is that you have to add an extra call method for showBottomSheet from your FloatingActionButton when it is touched.

Bonus: here is the Material Design page on how to use it.

like image 41
Haroun Hajem Avatar answered Nov 15 '22 09:11

Haroun Hajem


You can check this code, it is a complete example of how to start implementing this kind of UI, take it with a grain of salt.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:rxdart/rxdart.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Orination Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  bool _isOpen;
  double _dragStart;
  double _hieght;
  double _maxHight;
  double _currentPosition;
  GlobalKey _cardKey;
  AnimationController _controller;
  Animation<double> _cardAnimation;

  @override
  void initState() {
    _isOpen = false;
    _hieght = 50.0;
    _cardKey = GlobalKey();
    _controller =
        AnimationController(vsync: this, duration: Duration(milliseconds: 700));
    _cardAnimation = Tween(begin: _hieght, end: _maxHight).animate(
        CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
    );

    _controller.addListener(() {
      setState(() {
        _hieght = _cardAnimation.value;
      });
    });

    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(

      appBar: AppBar(
        elevation: 0.0,
        backgroundColor: Colors.transparent,
        titleSpacing: 0.0,
        title: _isOpen
            ? MaterialButton(
          child: Text(
            "Back",
            style: TextStyle(color: Colors.red),
          ),
          onPressed: () {
            _isOpen = false;
            _cardAnimation = Tween(begin: _hieght, end: 50.0).animate(
                CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
            );
            _controller.forward(from: 0.0);
          },
        )
            : Text(""),
      ),
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.keyboard_arrow_up),
          onPressed: () {
            final RenderBox renderBoxCard = _cardKey.currentContext
                .findRenderObject();
            _maxHight = renderBoxCard.size.height;
            _cardAnimation = Tween(begin: _hieght, end: _maxHight).animate(
                CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
            );
            _controller.forward(from: 0.0);
            _isOpen = true;
          }),
      body: Stack(
        key: _cardKey,
        alignment: Alignment.bottomCenter,
        children: <Widget>[
          Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.black12,
          ),
          GestureDetector(
            onPanStart: _onPanStart,
            onPanUpdate: _onPanUpdate,
            onPanEnd: _onPanEnd,
            child:Material(
              borderRadius: BorderRadius.only(
                    topRight: Radius.circular(16.0),
                    topLeft: Radius.circular(16.0),
              ),
              elevation: 60.0,
              color: Colors.white,
             // shadowColor: Colors.,
              child: Container(
                height: _hieght,

                child: Center(
                  child: Text("Hello, You can drag up"),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _onPanStart(DragStartDetails details) {
    _dragStart = details.globalPosition.dy;

    _currentPosition = _hieght;
  }

  void _onPanUpdate(DragUpdateDetails details) {
    final RenderBox renderBoxCard = _cardKey.currentContext.findRenderObject();
    _maxHight = renderBoxCard.size.height;
    final hieght = _currentPosition - details.globalPosition.dy + _dragStart;
    print(
        "_currentPosition = $_currentPosition _hieght = $_hieght hieght = $hieght");
    if (hieght <= _maxHight && hieght >= 50.0) {
      setState(() {
        _hieght = _currentPosition - details.globalPosition.dy + _dragStart;
      });
    }
  }

  void _onPanEnd(DragEndDetails details) {
    _currentPosition = _hieght;
    if (_hieght <= 60.0) {
      setState(() {
        _isOpen = false;
      });
    } else {
      setState(() {
        _isOpen = true;
      });
    }
  }
}

Edit: I modified the code by using Material Widget instead of A container with shadow for better performance,If you have any issue, please let me know .

like image 31
Saed Nabil Avatar answered Nov 15 '22 11:11

Saed Nabil