Flutter dialog (1) - showDialog的讲解

在应用开发中,或多或少都会遇到需要弹框的问题, 比如:需要用户确认,需要输入一些信息等等的问题,这就要用到 dialog 相关的概念了

而在 flutter 中,所有可以看见的都是 Widget,dialog 也不例外

不过和 android 或 iOS 中不同的一点是,Flutter 中 dialog 不是一个单独的类,而是一个可以由你自定义的 Widget

写在前面

首先为了方便,我定义了一个简单的方法用于构建按钮

  Widget buildButton(
    String text,
    Function onPressed, {
    Color color = Colors.white,
  }) {
    return FlatButton(
      color: color,
      child: Text(text),
      onPressed: onPressed,
    );
  }

showDialog


dialog 的方法签名是这样的

其中 context 和 builder 是必传项

builder 需要返回一个 Widget,这个 Widget 会被作为 dialog 展示在页面上

比如我简单的写了一个这个方法

    showDialog(
      context: context,
      builder: (ctx) {
        return Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              buildButton("返回1", () {}),
              buildButton("返回2", () {}),
            ],
          ),
        );
      },
    );

当我调用这个方法时,会得到这样的样式

这个就是最简单的方法,然后点击外部,dialog 会消失

添加关闭时的返回值

接着我给按钮添加具体的事件

修改代码为以下的样子

_showDialog() async {
    var result = await showDialog(
      context: context,
      builder: (ctx) {
        return Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              buildButton("返回1", () => Navigator.of(context).pop(1)),
              buildButton("返回2", () => Navigator.pop(context, 2)),
            ],
          ),
        );
      },
    );

    print("result = $result");
  }

然后分别点击 1 2 和外部让 dialog 消失
会得到以下的结果

不过这个只能让 dialog 显示固定的内容,如果你的 dialog 有内容变化,则使用这个方式就不行了,哪怕是调用 setState 也不会发生变化,这个是因为外部 State 的状态变化不会影响到 dialog 的内容,因为 dialog 是附着至 app 根部的,而不是附着于页面

结合 StatefulWidget 使用

所以我们 dialog 中也可以使用 StatefulWidget,如同一个页面一样,只是这个页面可能不是全屏的

我定义了一个简单的 CounterWidget


class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  var _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Material(
            child: Container(
              width: 100,
              height: 100,
              child: Text(
                _counter.toString(),
                style: TextStyle(fontSize: 40),
              ),
              alignment: Alignment.center,
            ),
            color: Colors.white,
          ),
          buildButton("+1", () => setState(() => _counter++)),
          buildButton("-1", () => setState(() => _counter--)),
        ],
      ),
    );
  }
}

并且调用

showDialog(context: context, builder: (ctx) => CounterWidget());

这里可以看到,一个带状态的控件也是可以被展示在 dialog 中的

结合 StatefulBuilder

在 flutter 中有一个类,叫 StatefulBuilder

这个类的 builder 构造中会给一个 state,这个 state 是一个方法,返回 void,传入参数是一个方法,听起来很绕

大概是这样用

var statefulBuilder = StatefulBuilder(
    builder: (ctx, state) {
        state(() {});
        return Container();
    },
);

看起来和 setState 很像

这里我模拟一个 progress 的变化,不过这个进度是由外部传入的

_showDialogWithStatefulBuilder() {
    var progress = 0.0;
    StateSetter ss;
    Timer.periodic(Duration(milliseconds: 300), (timer) {
      progress += 0.1;
      if (ss != null) {
        ss(() {});
      }
      if (progress >= 1) {
        timer.cancel();
        ss = null;
      }
    });
    var sb = StatefulBuilder(
      builder: (ctx, state) {
        ss = state;
        return Center(
          child: Container(
            height: 40,
            child: LinearProgressIndicator(
              backgroundColor: Colors.white,
              value: progress,
            ),
          ),
        );
      },
    );
    showDialog(context: context, builder: (ctx) => sb);
  }

这里只是简单的演示一个用法,实际应用中,进度条应该是可以多处复用的,应该使用 StatefulWidget 进行复用,而不是简易的使用 StatefulBuilder 来做这件事情,并且,应该在构建时传入 stream 并且监听 stream 为宜,而不应该使用这种 Timer 的形式

StatefulBuilder 应该用于弹出布局很特殊不太可能复用于其他地方的情况

使用 iOS 风格

有的同学可能要问了,你这演示都是 MD 风格的,我需要的是苹果风格的, 怎么办?

在 flutter 中,如果你需要 iOS 风格的,只需要使用 Cupertino 组件即可

void showCupertinoDialog() {
    var dialog = CupertinoAlertDialog(
      content: Text(
        "你好,我是你苹果爸爸的界面",
        style: TextStyle(fontSize: 20),
      ),
      actions: <Widget>[
        CupertinoButton(
          child: Text("取消"),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
        CupertinoButton(
          child: Text("确定"),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ],
    );

    showDialog(context: context, builder: (_) => dialog);
  }

带输入框的 dialog

 showHasInputDialog() {
    var widget = Center(
      child: Container(
        height: 40,
        width: double.infinity,
        child: Material(
          child: TextField(),
        ),
      ),
    );
    showDialog(context: context, builder: (_) => widget);
  }

根据软键盘自动变化位置

之前的输入框有一些问题,如果你的弹窗在底部,则弹出的输入框可能会被挡住

这里需要另一个方法来实现

import 'package:flutter/material.dart';
import 'dart:ui' as ui;

class InputDialog extends StatefulWidget {
  @override
  _InputDialogState createState() => _InputDialogState();
}

class _InputDialogState extends State<InputDialog> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    if (this.mounted) setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    var mediaQueryData = MediaQueryData.fromWindow(ui.window);
    return AnimatedContainer(
      color: Colors.transparent,
      duration: const Duration(milliseconds: 300),
      padding: EdgeInsets.only(bottom: mediaQueryData.viewInsets.bottom),
      child: Material(child: TextField()),
      alignment: Alignment.center,
    );
  }
}

定义一个 dialog 类,然后监听窗口的变化

然后在变化的时候动态的修改 padding,以达到输入框永远在界面中心的目的

后记

完整代码 github

第一篇主要讲了 showDialog 方法的一些使用方法和建议

以上

你可能感兴趣的:(flutter,flutter,dialog,showDialog,输入框,弹窗)