You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中实现从半屏模态Bottom Sheet切换至全屏Scaffold的方案咨询

实现思路

这个需求我之前做过类似的实现,其实核心就是用Flutter自带的DraggableScrollableSheet组件,再配合状态控制来限制拖动行为、切换界面样式:

  • 初始状态设置弹窗最小/最大高度为半屏(比如0.4)和全屏(1.0),展示热门选项的ListView
  • 通过监听拖动事件,当用户将弹窗拖到接近全屏的阈值时,锁定弹窗高度为1.0,同时把最小高度也设为1.0,禁止向下拖动返回半屏
  • 全屏状态下切换为带AppBar的Scaffold结构,让ListView可以正常滚动,完全符合Scaffold的交互逻辑
完整代码示例

下面是可直接运行的完整代码,包含主页面触发按钮和弹窗的实现:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Modal Bottom Sheet Demo')),
        body: const Center(
          child: ElevatedButton(
            onPressed: _showModalSheet,
            child: Text('打开底部弹窗'),
          ),
        ),
      ),
    );
  }

  static void _showModalSheet(BuildContext context) {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => const _CustomModalSheet(),
    );
  }
}

class _CustomModalSheet extends StatefulWidget {
  const _CustomModalSheet();

  @override
  State<_CustomModalSheet> createState() => _CustomModalSheetState();
}

class _CustomModalSheetState extends State<_CustomModalSheet> {
  // 半屏高度比例
  static const _halfScreenRatio = 0.4;
  // 全屏高度比例
  static const _fullScreenRatio = 1.0;
  // 触发全屏的阈值(拖动到超过80%高度时锁定全屏)
  static const _fullScreenThreshold = 0.8;

  double _minChildSize = _halfScreenRatio;
  double _maxChildSize = _fullScreenRatio;
  bool _isFullScreen = false;

  @override
  Widget build(BuildContext context) {
    return NotificationListener<DraggableScrollableNotification>(
      onNotification: (notification) {
        // 监听拖动进度,extent范围0-1
        final currentRatio = notification.extent;
        if (!_isFullScreen && currentRatio >= _fullScreenThreshold) {
          setState(() {
            _isFullScreen = true;
            // 锁定最小高度为全屏,彻底禁止向下拖动
            _minChildSize = _fullScreenRatio;
          });
        }
        return true;
      },
      child: DraggableScrollableSheet(
        initialChildSize: _halfScreenRatio,
        minChildSize: _minChildSize,
        maxChildSize: _maxChildSize,
        builder: (context, scrollController) {
          return Container(
            decoration: const BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
            ),
            child: _isFullScreen
                ? _buildFullScreenScaffold(scrollController)
                : _buildHalfScreenList(scrollController),
          );
        },
      ),
    );
  }

  // 半屏状态:展示热门选项的ListView
  Widget _buildHalfScreenList(ScrollController scrollController) {
    return Column(
      children: [
        // 顶部拖动指示器,提示用户可以拖动
        const Padding(
          padding: EdgeInsets.symmetric(vertical: 12),
          child: SizedBox(
            width: 40,
            height: 4,
            child: DecoratedBox(
              decoration: BoxDecoration(
                color: Colors.grey,
                borderRadius: BorderRadius.all(Radius.circular(2)),
              ),
            ),
          ),
        ),
        Expanded(
          child: ListView.builder(
            controller: scrollController,
            itemCount: 10,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('热门选项 ${index + 1}'),
                onTap: () {
                  Navigator.pop(context);
                  // 这里可以添加选项选择后的逻辑
                },
              );
            },
          ),
        ),
      ],
    );
  }

  // 全屏状态:类Scaffold结构,带AppBar和完整滚动列表
  Widget _buildFullScreenScaffold(ScrollController scrollController) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('全部选项'),
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () => Navigator.pop(context),
        ),
      ),
      body: ListView.builder(
        controller: scrollController,
        itemCount: 50,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('全部选项 ${index + 1}'),
            onTap: () {
              Navigator.pop(context);
              // 这里可以添加选项选择后的逻辑
            },
          );
        },
      ),
    );
  }
}
关键细节说明
  1. DraggableScrollableSheet核心配置:

    • 必须配合showModalBottomSheet的isScrollControlled: true,否则弹窗无法延伸到全屏
    • 初始initialChildSize设为半屏比例,minChildSize和maxChildSize控制可拖动的范围
  2. 拖动锁定逻辑:

    • 用NotificationListener捕获拖动进度,当超过阈值时切换全屏状态
    • 把minChildSize设为1.0后,弹窗就被彻底锁定在全屏,无法再向下拖动
  3. 界面状态切换:

    • 半屏状态保留拖动指示器,符合底部弹窗的交互习惯
    • 全屏状态切换为Scaffold,带AppBar和完整列表,完全贴合用户对全屏页面的预期
  4. 滚动控制器复用:

    • 将DraggableScrollableSheet提供的scrollController传递给两个ListView,保证拖动和滚动的行为连贯不冲突

这样实现的效果完全符合你的需求:初始半屏展示热门选项,向上拖动到一定程度自动切换为全屏Scaffold,全屏后只能滚动列表,无法拖回半屏,点击关闭按钮即可退出弹窗。

内容的提问来源于stack exchange,提问作者AverageChau

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 05:52:32