Flutter - Expand bottomNavigationBar by swiping or pressing the floatingActionButton

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(() {});
    });
  }
}

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.


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 .

Tags:

Dart

Flutter