如何创建从侧面打开的模态底板

时间:2020-08-29 13:54:58

标签: flutter flutter-layout

我正在尝试克隆股票android计算器应用程序。我不知道如何实现在右侧打开的可拉式抽屉。

下面是一个gif,它显示了我在说什么: https://imgur.com/a/hnYLTyA

2 个答案:

答案 0 :(得分:5)

  • 使用Stack将抽屉放置在计算器屏幕顶部。

  • 使用Positioned作为抽屉,并根据其拉出的数量设置其left参数。

  • 最初将抽屉的left参数设置到屏幕末端。

  • 在拉动GestureDetectoronPanUpdate时更改位置。

  • 根据抽屉的位置更改抽屉图标。

  • 对于计算器屏幕上的暗淡效果,请使用ModalBarrier。用Opacity小部件包裹它,并根据抽屉的拉出量设置其opacity参数。

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

结果:

res

答案 1 :(得分:2)

enter image description here

Flutter默认使用本机键盘,而您要做的是制作自己的自定义键盘。

您需要做什么:

  1. 防止显示键盘。 (https://github.com/flutter/flutter/issues/16863
  2. 创建自己的自定义键盘小部件
  3. 在贴上用于关闭和打开时添加动画。
  4. 添加具有“自动结束”功能的拖动功能(不知道如何正确地用英语说,我的意思是如果您将拖动放在中间会发生什么情况。)
  5. 添加点击。

我已经举了一个简单的例子。

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;
  }
}