HarmonyOS鸿蒙Next中Flutter框架多设备开发指导-弹出框场景

HarmonyOS鸿蒙Next中Flutter框架多设备开发指导-弹出框场景

1.1 场景概述

在移动应用开发中,不同设备的屏幕形态各异,例如刘海屏、全面屏、折叠屏等,系统状态栏、导航栏、软键盘等元素亦会占据屏幕空间。业务上常需在页面中央弹出自定义内容的模态层(表单、说明、确认操作等),若宽高写死或未结合横向与纵向断点、顶部系统避让区与软键盘,易出现小屏溢出、大屏留白失衡,或键盘顶起后输入区不可见等问题。本文将详细介绍本示例中居中弹窗与底层栅格背景的协同方式、弹窗尺寸分档、可用高度收缩与键盘上移等实现要点。

1.1.1 使用场景

用户需要在不离开当前页的前提下完成简短操作或阅读说明;弹窗应大致居于视窗中部,并在键盘弹出时仍尽量保持标题、可滚动内容与底部按钮可操作。下面是不同横向断点下与弹窗相关的布局示意(底层为示例用 GridRow / GridCol 占位色块;弹窗由按钮「触发自定义内容弹出窗」打开)。

横向断点 sm md lg xl
展示逻辑 背景栅格 4 列;弹窗宽度 320;弹窗目标高度 300 背景栅格 8 列;弹窗宽 500;目标高 400 背景栅格 12 列;弹窗宽 600;目标高 500 背景栅格 12 列;弹窗宽 700;目标高 600
展示布局

1.1.2 常见问题

居中弹窗类界面在窗口形态与输入场景多变时,若未统筹,容易出现以下问题:

  • 弹窗高度超过键盘弹出后的可视区,底部按钮或输入框被挡住
  • 仅按设计稿写死宽高,旋转、分屏后弹窗比例失调或与栅格背景不协调
  • 关闭弹窗后输入框仍聚焦,软键盘未收起
  • 未读取顶部系统避让区,竖屏刘海等设备上弹窗视觉上顶得过满

1.1.3 多设备适配

  • 适配点 1:用 Avoid.getWindowAvoidArea(AvoidAreaType.TYPE_SYSTEM) 换算顶部避让高度;用 Keyboard.addListener(‘keyboardDidShow’ / ‘keyboardDidHide’) 更新键盘高度;弹窗实际高度为 min(目标高度, 窗口高度 − 顶部避让高度 − 键盘高度)。

图 1-1手机竖屏下栅格背景与居中弹窗(未弹键盘)。

图 1-2手机竖屏键盘弹出后弹窗上移示意。

  • 适配点 2:在 centerSheetProps.containerStyle 中于键盘可见时施加 translateY:取 max(−keyboardHeight/2, calculateMaxTranslateY(…)),其中 calculateMaxTranslateY 根据弹窗高度与顶部安全区计算上移上限,避免弹窗顶部低于安全区。横屏下可对照观察同一套逻辑。

图 1-3手机横屏下居中弹窗。

图 1-4手机横屏键盘弹出

  • 适配点 3:弹窗类型为 SHEET_TYPE.CENTER,自定义 width / height、圆角 20、半透明遮罩 rgba(0,0,0,0.5),内容区为 ScrollView + TextInput + 确认/取消,关闭时 onStateChange 在索引为 −1 时对输入框 blur。

图 2-1PC / 特大宽下整页与居中弹窗。

1.2 开发指导

1.2.1 Flutter开发

1.2.1.1 关键能力

需要判断横竖屏、折叠屏场景下,显示不同的UI效果,确保对话框高度在合理范围内。

1.2.1.2 指导案例

在横竖屏、不通屏幕宽度自动调节弹窗宽高、内部按钮UI效果

1.2.1.3 示例代码

void _showCustomDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (context) {
      final mediaQuery = MediaQuery.of(context);
      final screenSize = mediaQuery.size;
      final viewInsets = mediaQuery.viewInsets; // 键盘高度等
      final padding = mediaQuery.padding; // 状态栏/导航栏
      
      // 计算可用高度和宽度
      final usableHeight = screenSize.height - padding.top - padding.bottom - viewInsets.bottom;
      final usableWidth = screenSize.width;
      // 定义对话框的最大宽度和高度限制
      final maxWidth = min(usableWidth * 0.8, 400.0);
      final maxHeight = usableHeight * 0.9;
      final minHeight = 180.0;
      
      // 判断是否“疑似折叠”:宽高比接近 2:1 且宽度较大(横屏+分屏)
      bool isLikelyHalfFolded = false;
      if (screenSize.width > screenSize.height) {
        // 横屏情况下
        final aspectRatio = screenSize.width / screenSize.height;
        if (aspectRatio > 1.8 && screenSize.width > 600) {
          isLikelyHalfFolded = true;
        }
      }
      
      // 根据是否疑似折叠状态计算对话框高度
      double dialogHeight = isLikelyHalfFolded
        ? usableHeight * 0.6
        : usableHeight * 0.6;
      // 确保对话框高度在合理范围内
      dialogHeight = dialogHeight.clamp(minHeight, maxHeight);
      final focusNode = FocusNode();
      final radius = _getResponsiveValue(
        screenWidth: screenSize.width,
        base: 24.0,
        sm: 24.0,
        md: 24.0,
        lg: 12.0,
      );
      
      final titleSize = _getResponsiveValue(
        screenWidth: screenSize.width,
        base: 18.0,
        sm: 15.0,
        md: 15.0,
        lg: 20.0,
      );
      
      return Dialog(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radius)),
        child: SizedBox(
          width: maxWidth,
          height: dialogHeight,
          child: Column(
            children: [
              // 标题区 (占对话框高度的7%)
              Container(
                height: dialogHeight * 0.08,
                alignment: Alignment.center,
                child: Text(
                  '标题',
                  style: TextStyle(fontSize: titleSize),
                ),
              ),
              // 内容区 (占对话框高度的80%)
              Expanded(
                flex: 80,
                child: Center(
                  child: Container(
                    width: maxWidth * 0.8,
                    color: const Color(0xFF9DC3E3),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.start,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: [
                        const Padding(
                          padding: EdgeInsets.only(top: 2),
                          child: Text('内容区'),
                        ),
                        
                        const SizedBox(height: 2),
                        SizedBox(
                          width: double.infinity,
                          child: Container(
                            decoration: BoxDecoration(
                              color: Colors.white.withOpacity(0.2),
                              borderRadius: BorderRadius.circular(16.0),
                            ),
                            child: TextField(
                              focusNode: focusNode,
                              decoration: const InputDecoration(
                                filled: false,
                                border: InputBorder.none,
                                contentPadding: EdgeInsets.all(10),
                              ),
                              
                              onEditingComplete: () {
                                FocusScope.of(context).unfocus();
                              },
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              // 按钮区 (占对话框高度的10%)
              Container(
                height: dialogHeight * 0.12,
                padding: EdgeInsets.only(top: dialogHeight * 0.01,bottom: dialogHeight * 0.01),
                child: Center(
                  child: Container(
                    width: maxWidth * 0.8,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        ElevatedButton(
                          onPressed: () => Navigator.pop(context),
                          style: ElevatedButton.styleFrom(
                            backgroundColor: Colors.blue[600],
                            shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(radius),
                            ),
                          ),
                          child: const Text('确认',style: TextStyle(color: Colors.white),),
                        ),
                        ElevatedButton(
                          onPressed: () => Navigator.pop(context),
                          style: ElevatedButton.styleFrom(
                            backgroundColor: Colors.blue[600],
                            shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(radius),
                            ),
                          ),
                          child: const Text('取消',style: TextStyle(color: Colors.white),),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      );
    },
  );
}

弹出框的Sample示例代码地址:example,开发者可以通过该地址查看完整的示例代码,并根据自己的需求进行修改和扩展。


更多关于HarmonyOS鸿蒙Next中Flutter框架多设备开发指导-弹出框场景的实战教程也可以访问 https://www.itying.com/category-92-b0.html

2 回复

在鸿蒙Next中,Flutter弹出框需通过ohos_flutter插件调用ArkUI能力。使用showDialog时,利用MediaQuery.of(context).size获取屏幕尺寸,动态调整对话框宽高。多设备适配建议结合LayoutBuilderOrientationBuilder识别横竖屏及折叠状态。对折叠屏,需监听windowSizeChange事件,在State中重建弹出框布局。

更多关于HarmonyOS鸿蒙Next中Flutter框架多设备开发指导-弹出框场景的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在HarmonyOS Next 上使用 Flutter 开发居中弹出框时,核心挑战来自多样化的屏幕形态和系统 UI 占据的空间。该示例展示了通过 MediaQuery 动态感知屏幕尺寸、状态栏/导航栏安全区和键盘高度,结合断点自适应弹窗宽高的方案。

关键适配逻辑均体现在 _showCustomDialog 方法中:

  1. 可用区域计算usableHeight = screenSize.height - padding.top - padding.bottom - viewInsets.bottom,扣除了顶部状态栏、底部导航栏及键盘弹出占用的高度,确保弹窗不超出可视范围。
  2. 尺寸分档:使用 _getResponsiveValue 根据屏幕宽度返回不同圆角、字号,并限定最大宽度为 min(usableWidth * 0.8, 400.0),最大高度为 usableHeight * 0.9,避免小屏溢出、大屏留白过多。
  3. 键盘避让:代码通过 MediaQuery.of(context).viewInsets 获取键盘高度,并在计算可用高度时扣除该值,无需额外监听即可让弹窗随键盘上移,保持输入区可见。
  4. 折叠/横屏适配:补充了“疑似折叠”判断(横屏且宽高比>1.8且宽度>600时,按可选比例调整高度),保证在分屏或折叠屏展开姿态下的协调性。
  5. 焦点处理onEditingComplete 调用 FocusScope.of(context).unfocus(),避免关闭弹窗后键盘残留或输入框持续聚焦的问题。

整体上,这段代码提供了一种不依赖固定尺寸、响应式配合系统避让区的居中弹窗实现,适用于手机、折叠屏、平板乃至大屏 PC 等多种设备。完整示例可在 GitCode 查看。

回到顶部