abhirham
abhirham

Reputation: 191

How to create a modal bottom sheet which opens from the side

I am trying to clone the stock android calculator app. I am not able to figure out how to implement the pullable drawer which opens on the right side.

Here is a gif which shows what I am talking about: https://i.sstatic.net/Db8HM.jpg

Upvotes: 8

Views: 7748

Answers (3)

Matias de Andrea
Matias de Andrea

Reputation: 359

You can use a side sheet from Material design. Here you have a package to implement this. You can see the code of this package here

Upvotes: 1

Kherel
Kherel

Reputation: 16225

enter image description here

Flutter use native keyboards by default, and what you want to do is make your own custom keyboard.

What you need to do:

  1. prevent showing keyboard. (https://github.com/flutter/flutter/issues/16863)
  2. create your own custom keybord widget
  3. Add animation on taping for close and open.
  4. Add dragging functionality with 'auto ending' (don't know how to say it in English correctly, I mean what would happen if you drop dragging in the middle).
  5. Add tapping.

I've made a simple example.

import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        body: SafeArea(
          child: MyStatefulWidget(),
        ),
      ),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  MyStatefulWidget({Key key}) : super(key: key);

  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  TextEditingController _controller;
  NoKeyboardEditableTextFocusNode focusNode;
  bool isKeyboardOpen = false;

  void initState() {
    super.initState();
    focusNode = NoKeyboardEditableTextFocusNode();
    focusNode.addListener(() {
      setState(() {
        isKeyboardOpen = focusNode.hasFocus;
      });
    });
    _controller = TextEditingController(text: 'tap here');
  }

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

  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraintes) {
          var maxHeight = constraintes.maxHeight;
          return Column(
            children: [
              AnimatedContainer(
                height: isKeyboardOpen ? maxHeight - 300 : maxHeight,
                duration: Duration(milliseconds: 300),
                child: Center(
                  child: GestureDetector(
                    onTap: () {
                      setState(() {
                        isKeyboardOpen = true;
                      });
                    },
                    child: NoKeyboardEditableText(
                      noKeyboardEditableTextFocusNode: focusNode,
                      controller: _controller,
                      cursorColor: Colors.green,
                      selectionColor: Colors.red,
                      style: TextStyle(
                          fontStyle: FontStyle.normal,
                          fontSize: 30.0,
                          color: Colors.black),
                    ),
                  ),
                ),
              ),
              AnimatedContainer(
                height: isKeyboardOpen ? 300 : 0,
                duration: Duration(milliseconds: 300),
                color: Colors.red,
                child: _CustomKeybord(
                    onAdd: (v) => _controller.value =
                        TextEditingValue(text: _controller.value.text + v)),
              ),
            ],
          );
        },
      ),
    );
  }
}

class _CustomKeybord extends StatefulWidget {
  _CustomKeybord({Key key, this.onAdd}) : super(key: key);

  final Function(String value) onAdd;
  @override
  __CustomKeybordState createState() => __CustomKeybordState();
}

class __CustomKeybordState extends State<_CustomKeybord> {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Stack(
          children: [
            Positioned(
              left: 0,
              top: 0,
              bottom: 0,
              child: Container(
                width: constraints.maxWidth * 0.9,
                height: constraints.maxHeight,
                child: _FirstLayerKeybord(
                  onAdd: widget.onAdd,
                ),
              ),
            ),
            Positioned(
              right: 0,
              top: 0,
              bottom: 0,
              child: _SecondLayerKeybord(
                onAdd: widget.onAdd,
              ),
            ),
          ],
        );
      },
    );
  }
}

class _SecondLayerKeybord extends StatefulWidget {
  const _SecondLayerKeybord({
    Key key,
    @required this.onAdd,
  }) : super(key: key);

  final Function(String value) onAdd;

  @override
  __SecondLayerKeybordState createState() => __SecondLayerKeybordState();
}

class __SecondLayerKeybordState extends State<_SecondLayerKeybord>
    with TickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    )..addListener(_listener);
  }

  void _listener() {
    if (!_controller.isAnimating) {
      setState(() {
        isOpen = _controller.isCompleted && _controller.value == 1;
      });
    }
  }

  void onTap() {
    _controller.isCompleted ? _controller.reverse() : _controller.forward();
  }

  bool isOpen = false;

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

  double _currentX;

  _onDrag(details) {
    var maxWidth = MediaQuery.of(context).size.width;

    var x = details.globalPosition.dx;
    _currentX = x;
    var v = math.max(0.0, 1 - (_currentX / maxWidth - 0.5) * 2);
    _controller.value = v;
  }

  _onDragEnd(_) {
    if (_controller.value > .5) {
      _controller.animateTo(1);
    } else {
      _controller.animateTo(0);
    }
  }

  @override
  Widget build(BuildContext context) {
    var maxWidth = MediaQuery.of(context).size.width;

    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Container(
          width: maxWidth,
          child: Stack(
            children: [
              Positioned(
                left: 0,
                right: 0,
                top: 0,
                bottom: 0,
                child: IgnorePointer(
                  child: Opacity(
                    opacity: 0.3 * _controller.value,
                    child: Container(
                      color: Colors.black,
                    ),
                  ),
                ),
              ),
              Positioned(
                left: maxWidth * 0.9 - (_controller.value * maxWidth * 0.45),
                bottom: 0,
                top: 0,
                child: Container(
                  decoration: BoxDecoration(
                    border: Border.all(
                      color: Colors.blueAccent,
                    ),
                  ),
                  width: maxWidth * 0.6,
                  child: Row(
                    mainAxisSize: MainAxisSize.max,
                    children: [
                      GestureDetector(
                        onTap: onTap,
                        onPanDown: (details) {
                          _currentX = details.globalPosition.dx;
                        },
                        onPanStart: _onDrag,
                        onPanUpdate: _onDrag,
                        onPanEnd: _onDragEnd,
                        child: Container(
                          alignment: Alignment.center,
                          color: Colors.blue,
                          width: maxWidth * 0.1,
                          child: Icon(
                            isOpen
                                ? Icons.keyboard_arrow_right
                                : Icons.keyboard_arrow_left,
                            color: Colors.white,
                          ),
                        ),
                      ),
                      buildColumn(['INV', 'sin', 'ln', 'π', '(']),
                      buildColumn(['Deg', 'cos', 'log', 'e', ')']),
                    ],
                  ),
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  Column buildColumn(List<String> listBtns) {
    return Column(
      children: listBtns
          .map((btnText) => Expanded(
                child: RaisedButton(
                  color: Colors.blue,
                  onPressed: () => widget.onAdd(btnText),
                  child: Text(
                    btnText,
                    style: TextStyle(color: Colors.white),
                  ),
                ),
              ))
          .toList(),
    );
  }
}

class _FirstLayerKeybord extends StatelessWidget {
  const _FirstLayerKeybord({
    Key key,
    @required this.onAdd,
  }) : super(key: key);

  final Function(String value) onAdd;

  final primaryButtons = const [
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '9'],
    ['0', '.', '='],
  ];

  @override
  Widget build(BuildContext context) {
    return Column(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.center,
        children: primaryButtons
            .map((row) => Expanded(
                  child: Row(
                    mainAxisSize: MainAxisSize.max,
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: row
                        .map((e) => Expanded(
                              child: RaisedButton(
                                onPressed: () => onAdd(e),
                                child: Text(e),
                              ),
                            ))
                        .toList(),
                  ),
                ))
            .toList());
  }
}

class NoKeyboardEditableText extends EditableText {
  NoKeyboardEditableText({
    @required TextEditingController controller,
    @required TextStyle style,
    @required Color cursorColor,
    bool autofocus = false,
    Color selectionColor,
    @required NoKeyboardEditableTextFocusNode noKeyboardEditableTextFocusNode,
  }) : super(
          controller: controller,
          focusNode: noKeyboardEditableTextFocusNode,
          style: style,
          cursorColor: cursorColor,
          autofocus: autofocus,
          selectionColor: selectionColor,
          backgroundCursorColor: Colors.black,
        );

  @override
  EditableTextState createState() {
    return NoKeyboardEditableTextState();
  }
}

class NoKeyboardEditableTextState extends EditableTextState {
  @override
  void requestKeyboard() {
    FocusScope.of(context).requestFocus(widget.focusNode);
  }
}

class NoKeyboardEditableTextFocusNode extends FocusNode {
  @override
  bool consumeKeyboardToken() {
    return false;
  }
}

Upvotes: 2

Mobina
Mobina

Reputation: 7119

  • Use Stack to position the drawer on top of calculator screen.

  • Use Positioned for the drawer and set its left parameter according to the amount that it's pulled.

  • Set the left parameter of the drawer to the end of the screen initially.

  • Use GestureDetector and onPanUpdate to change the position when it's pulled.

  • Change the drawer icon according to the position of the drawer.

  • For the dim effect on the calculator screen, use a ModalBarrier. Wrap it with an Opacity widget and set its opacity parameter according to the amount the drawer is pulled.

  static double _offset = 30;
  double _drawerLeft = 400;
  IconData _drawerIcon = Icons.arrow_back_ios;
  bool _init = true;

  @override
  Widget build(BuildContext context) {
    if (_init) {
      _drawerLeft = MediaQuery.of(context).size.width - _offset;
      _init = false;
    }
    return Scaffold(
      body: Align(
        alignment: Alignment.bottomCenter,
        child: FractionallySizedBox(
          widthFactor: 1,
          heightFactor: 0.5,
          child: Stack(
            fit: StackFit.expand,
            children: <Widget>[
              Positioned.fill(
                child: Container(
                  color: Colors.grey[200],
                  child: Center(
                      child: Text(
                    'text',
                    style: TextStyle(fontSize: 32),
                  )),
                ),
              ),
              Positioned.fill(
                right: 0,
                child: Opacity(
                  opacity: 1 -
                      _drawerLeft /
                          (MediaQuery.of(context).size.width - _offset),
                  child:
                      ModalBarrier(dismissible: false, color: Colors.black87),
                ),
              ),
              Positioned(
                width: MediaQuery.of(context).size.width * 3 / 4,
                top: 0,
                height: MediaQuery.of(context).size.height / 2,
                left: _drawerLeft,
                child: GestureDetector(
                    onPanUpdate: (details) {
                      _drawerLeft += details.delta.dx;
                      if (_drawerLeft <= MediaQuery.of(context).size.width / 4)
                        _drawerLeft = MediaQuery.of(context).size.width / 4;
                      if (_drawerLeft >=
                          MediaQuery.of(context).size.width - _offset)
                        _drawerLeft =
                            MediaQuery.of(context).size.width - _offset;
                      if (_drawerLeft <= MediaQuery.of(context).size.width / 3)
                        _drawerIcon = Icons.arrow_forward_ios;
                      if (_drawerLeft >=
                          MediaQuery.of(context).size.width - 2 * _offset)
                        _drawerIcon = Icons.arrow_back_ios;
                      setState(() {});
                    },
                    child: Container(
                      decoration: BoxDecoration(color: Colors.blue),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: <Widget>[
                          Padding(
                            padding: EdgeInsets.only(right: _offset),
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                              children: <Widget>[
                                Icon(
                                  _drawerIcon,
                                  color: Colors.white,
                                ),
                              ],
                            ),
                          ),
                          Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: <Widget>[
                              Text(
                                'text',
                                style: TextStyle(
                                    color: Colors.white, fontSize: 32),
                              )
                            ],
                          )
                        ],
                      ),
                    )),
              ),
            ],
          ),
        ),
      ),
    );
  }

Result:

res

Upvotes: 6

Related Questions