Flutter AlertDialog For Android Habiter

195 阅读3分钟

基于 flutter_smart_dialog 封装。

如果你习惯了 Android 开发时的体验,喜欢 Material 的设计,那么可以用这个封装好的 AlertDialog,尽可能还原了当初 material_dialogs 的使用习惯。

如果你有更好的建议或实现方式,或者觉得目前的实现有问题,不妨在评论区留下你的意见~

AlertDialog

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

/// show alert dialog
showAlertDialog({
  String? title,
  String? message,
  Widget? content,
  String? textInput,
  TextInputType? textInputType,
  String? textInputHint,
  int? textInputMaxLength,
  bool cancelable = true,
  bool cancelableOutside = true,
  bool? secondaryConfirm,
  String? secondaryConfirmText,
  String? negativeText,
  void Function()? onNegative,
  String? positiveText,
  void Function(dynamic)? onPositive,
  List<String>? listItems,
  bool? listItemsSingleChoice,
  int? listItemsSelection,
  bool? listItemMultiChoice,
  List<int>? listItemMultiChoiceSelection,
  void Function(String text, int index)? onListItemSelected,
  void Function(List<String> texts, List<int> indexs)? onListItemsMultiSelected,
}) {
  bool containsMessage = message != null || content != null;
  bool secondaryConfirmChecked = secondaryConfirm ?? false;
  bool containsActionButtons = onNegative != null || onPositive != null || onListItemsMultiSelected != null;

  TextEditingController? textFieldController = textInput != null ? TextEditingController() : null;
  textFieldController?.text = textInput ?? '';
  textFieldController?.value = TextEditingValue(
    // 光标位置
    text: textInput ?? '',
    selection: TextSelection.fromPosition(TextPosition(
      affinity: TextAffinity.downstream,
      offset: textInput?.length ?? 0,
    )),
  );

  Widget buildTitleWidget() {
    assert(title != null);

    return Padding(
      padding: const EdgeInsets.only(
        left: AppDimens.pagePadding,
        top: AppDimens.itemPadding,
        right: AppDimens.pagePadding,
      ),
      child: Text(
        title!,
        style: const TextStyle(
          fontSize: 22,
        ),
      ),
    );
  }

  Widget buildMessageWidget() {
    assert(containsMessage);

    Widget buildInnerMessageWidget() {
      if (content != null) {
        return content;
      } else {
        return Text(message!);
      }
    }

    return Padding(
      padding: const EdgeInsets.only(
        left: AppDimens.pagePadding,
        top: AppDimens.itemPadding,
        right: AppDimens.pagePadding,
        bottom: AppDimens.itemPadding,
      ),
      child: buildInnerMessageWidget(),
    );
  }

  Widget buildTextInputWidget() {
    return Padding(
      padding: const EdgeInsets.only(
        left: AppDimens.pagePadding,
        top: AppDimens.itemPadding,
        right: AppDimens.pagePadding,
      ),
      child: TextField(
        controller: textFieldController,
        keyboardType: textInputType,
        textAlign: TextAlign.start,
        autofocus: true, // 自动获取焦点
        obscureText: false, // 隐藏输入内容
        minLines: 1, // 要让 maxLines 生效的同时,让输入框默认显示1行的UI,需要设置 minLines = 1
        maxLines: 6,
        maxLength: textInputMaxLength ?? 100,
        maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
        enabled: true,
        decoration: InputDecoration(
          // 悬浮 label
          labelText: textInputHint,
          floatingLabelBehavior: FloatingLabelBehavior.auto,
          floatingLabelAlignment: FloatingLabelAlignment.start,
          // hint
          hintText: textInputHint,
          // 背景填充
          filled: true,
          // 非紧凑的
          isDense: false,
          enabled: true,
        ),
      ),
    );
  }

  Widget buildSecondaryConfirmWidget() {
    return Builder(
      builder: (context) {
        return GestureDetector(
          onTap: () {
            secondaryConfirmChecked = !secondaryConfirmChecked;
            (context as Element).markNeedsBuild();
          },
          child: Padding(
            padding: const EdgeInsets.only(
              left: AppDimens.pagePadding,
              top: AppDimens.itemPadding,
              right: AppDimens.pagePadding,
            ),
            child: CheckboxLabel(
              value: secondaryConfirmChecked,
              label: secondaryConfirmText ?? 'secondary confirm',
              onChanged: (bool? newValue) {
                secondaryConfirmChecked = newValue ?? false;
                // 标记 Widget 树需要重新 Build,以刷新 UI。
                // 该方法来自于:https://book.flutterchina.club/chapter7/dailog.html#_7-7-4-%E5%AF%B9%E8%AF%9D%E6%A1%86%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86
                (context as Element).markNeedsBuild();
              },
            ),
          ),
        );
      },
    );
  }

  int radioButtonGroupValue = listItemsSelection ?? -1;
  List<int> currentMultiSelection = listItemMultiChoiceSelection ?? [];

  /// 更新多选选中状态
  updateMultiSelection(int index, BuildContext context) {
    if (currentMultiSelection.contains(index)) {
      currentMultiSelection.remove(index);
    } else {
      currentMultiSelection.add(index);
    }
    (context as Element).markNeedsBuild();
  }

  Widget buildListItemsWidget(BuildContext context) {
    assert(listItems != null);

    Widget buildItemWidget(int index, String item) {
      if (listItemsSingleChoice == true) {
        // 单选
        return RadioLabel(
          value: index,
          groupValue: radioButtonGroupValue,
          label: item,
          onChanged: <int>(value) {
            radioButtonGroupValue = value;
            (context as Element).markNeedsBuild();
          },
        );
      } else if (listItemMultiChoice == true) {
        // 多选
        return CheckboxLabel(
          value: currentMultiSelection.contains(index),
          label: item,
          onChanged: (value) {
            updateMultiSelection(index, context);
          },
        );
      } else {
        // 列表
        return Row(
          children: [
            Expanded(child: Text(item)),
          ],
        );
      }
    }

    // SingleChildScrollView 嵌套在 Column 中,所以使用 Expanded 包裹,否则无效,并且 Expanded 需要在最外层。
    return Expanded(
      // InWell 水波纹效果需要 Material 组件支持
      child: Material(
        color: Colors.transparent,
        child: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: Column(
            children: listItems!.asMap().entries.map(
              (entry) {
                int index = entry.key;
                String item = entry.value;
                return InkWell(
                  borderRadius: BorderRadius.circular(AppDimens.radius), // 水波纹样式
                  onTap: () {
                    if (onListItemSelected != null) {
                      // 单选:list items or single choice
                      if (listItemsSingleChoice == true) {
                        // 刷新单选状态UI
                        radioButtonGroupValue = index;
                        (context as Element).markNeedsBuild();
                      }

                      if (listItemMultiChoice != true) {
                        // 返回选中
                        onListItemSelected.call(item, index);
                        dismissDialog();
                      } else {
                        // 更新多选选中状态
                        updateMultiSelection(index, context);
                      }
                    }
                  },
                  child: Padding(
                    padding: const EdgeInsets.only(
                      top: AppDimens.itemPadding,
                      bottom: AppDimens.itemPadding,
                      left: AppDimens.pagePadding,
                      right: AppDimens.pagePadding,
                    ),
                    child: buildItemWidget(index, item),
                  ),
                );
              },
            ).toList(),
          ),
        ),
      ),
    );
  }

  Widget buildActionButtonsWidget() {
    return Padding(
      padding: const EdgeInsets.all(AppDimens.itemPadding),
      child: OverflowBar(
        alignment: MainAxisAlignment.end,
        overflowAlignment: OverflowBarAlignment.end,
        overflowDirection: VerticalDirection.down,
        spacing: 0, // 未 overflow 时按钮之间的间距
        overflowSpacing: 0, // 如果 overflow 了,overflow 后按钮之间的间距
        children: [
          // 使用 TextButton,由于存在点击效果,所以默认显示时看上去有 padding 的样子,所以我们不需要再设置 overflow 的 spacing
          if (onNegative != null) ...[
            TextButton(
              child: Text(negativeText ?? S.current.cancel),
              onPressed: () {
                SmartDialog.dismiss();
                onNegative.call();
              },
            ),
          ],

          if (onPositive != null || onListItemsMultiSelected != null) ...[
            TextButton(
              child: Text(positiveText ?? S.current.confirm),
              onPressed: () {
                SmartDialog.dismiss();
                onPositive?.call(textFieldController?.text);

                // 返回多选选择
                onListItemsMultiSelected?.call(
                  listItems!
                      .asMap()
                      .entries
                      .where((entry) => currentMultiSelection.contains(entry.key))
                      .map((entry) => entry.value)
                      .toList(),
                  currentMultiSelection..sort((a, b) => a.compareTo(b)),
                );
              },
            ),
          ]
        ],
      ),
    );
  }

  // show dialog
  SmartDialog.show(
    builder: (context) {
      return Padding(
        // 设置 dialog 距离屏幕的 padding,这里设置的值取自 Flutter Dialog 内部的实现
        padding: MediaQuery.viewInsetsOf(context) +
            const EdgeInsets.symmetric(
              horizontal: 46.0, // Flutter 默认40,横屏时靠近状态栏太近了,所以我们改大点
              vertical: 64.0, // Flutter 默认24,顶部可能会超出到状态栏,所以我们改大点
            ),
        child: ConstrainedBox(
          // 设置 Dialog 最小宽度280,取自 Flutter Dialog 内部的实现
          constraints: const BoxConstraints(
            minWidth: 280,
          ),
          // 背景
          child: DecoratedBox(
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(AppDimens.radius), // 圆角
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.start,
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min, // Column 高度自适应
              children: [
                // title
                if (title != null) buildTitleWidget(),
                // message
                if (containsMessage) buildMessageWidget(),
                // text input
                if (textInput != null) buildTextInputWidget(),
                // secondary check box
                if (secondaryConfirm != null) buildSecondaryConfirmWidget(),
                // list items options
                if (listItems != null) buildListItemsWidget(context),
                // action buttons
                if (containsActionButtons) buildActionButtonsWidget(),
              ],
            ),
          ),
        ),
      );
    },
    // Dialog 显示位置
    alignment: Alignment.center,
    // 点击外部空白处是否可以关闭
    clickMaskDismiss: cancelableOutside,
    // 点击穿透
    usePenetrate: false,
    // 动画
    useAnimation: true,
    animationType: SmartAnimationType.centerFade_otherSlide,
    animationTime: const Duration(milliseconds: 220),
    // 防抖
    debounce: true,
    // true: 多次调用 show,不产生多个 Dialog;false:产生多个 Dialog
    keepSingle: false,
    // Dialog 关闭时的回调
    onDismiss: () {},
    // 不使用系统风格的 Dialog
    useSystem: false,
    // 拦截返回键类型:(normal)触发返回按键时,自动关闭 Dialog,(block)触发返回按键时,拦截不做任何处理
    backType: cancelable ? SmartBackType.normal : SmartBackType.block,
  );
}

使用

                    showAlertDialog(
                      content: const Text("message"),
                      onPositive: (v) {},
                    );