Files
App.K12.Student.Miniapp_Flu…/docs/demos/smooth_sheet_demo.md
T
2026-09-04 15:22:20 +08:00

4.8 KiB
Raw Blame History

【Demo】常驻多停靠底部面板(PersistentSnapSheet)

源文件:lib/pages/demo/smooth_sheet_demo.dart | 分类:组件封装 一句话:模拟"附近学校"地图页 —— 背景可交互 + 底部常驻拖拽面板,3 个停靠点自动吸附。


一、这个 Demo 解决什么问题

地图类页面常见的交互:底部常驻一个可拖拽面板,像抽屉一样有多个停靠位置。 本页模拟教育 App 的"附近学校"页面:

停靠点 比例 显示内容
收起 12% 屏高 只有手柄 + "附近学校"标题
半屏 50% 屏高 前 2 所学校 + "查看全部"
展开 85% 屏高 完整学校列表,内部 ListView 可滚动

关键交互:面板自由拖拽、松手自动吸附最近停靠点;背景(地图)始终可交互(非模态);面板内列表滚到顶继续下拉可收起面板。

核心组件:PersistentSnapSheet(lib/core/components/persistent_snap_sheet.dart),是对 smooth_sheets 包的封装。 对照 Vue:showBottomSheet / 抽屉组件的"常驻版"——不挡背景、可多档位。


二、怎么运行 / 演示内容

runApp(const MaterialApp(home: SmoothSheetDemoPage()));
  • 拖拽面板松手 → 观察自动吸附最近停靠点;
  • 收起状态点地图背景 → 背景仍可交互(非模态);
  • 展开面板后滚动列表到底再下拉 → 面板收起;
  • 顶部 3 个按钮可代码控制面板跳到指定停靠点。

三、核心知识点(新手向)

1. 三行配置:停靠点比例

/// 3 个停靠点:底部收起 / 半屏 / 大半屏
static const List<double> _snapFactors = [0.12, 0.5, 0.85];

snapFactors 是占屏高的比例(0.12 = 12% 屏高),组件负责换算成像素并做吸附动画。

2. 结构:Stack 背景层 + 面板层

Scaffold(
  body: Stack(
    children: [
      _buildMapBackground(context),   // 背景层:模拟地图(真实项目换地图 SDK)
      PersistentSnapSheet(            // 面板层:常驻面板
        snapFactors: _snapFactors,
        child: Column(
          children: [
            _buildHandle(context),    // 手柄区域
            Expanded(child: _buildSheetContent(context)), // 面板内容
          ],
        ),
      ),
    ],
  ),
)

3. 经典拖拽指示条(36×4 圆角小横条)

Container(
  width: 36.w,
  height: 4.h,
  decoration: BoxDecoration(
    color: const Color(0xFFD1D5DB),
    borderRadius: BorderRadius.circular(2.r),
  ),
)

这是 iOS/微信风格的"可拖拽暗示":看到它就明白面板能拉。

4. 非模态:面板盖住背景,但不拦截背景交互

PersistentSnapSheet 是常驻布局组件(不是 showModalBottomSheet 弹窗), 面板下面的地图照常能点——这是"常驻面板"和"模态弹层"的本质区别。


四、关键代码剖析

/// 面板内部内容:分割线 + 可滚动学校列表(与面板手势联动)
Expanded(
  child: ListView.separated(
    padding: EdgeInsets.symmetric(horizontal: 16.w),
    itemCount: _schools.length,
    separatorBuilder: (_, _) => Divider(height: 1.h, color: const Color(0xFFF1F5F9)),
    itemBuilder: (context, index) => _SchoolListTile(
      school: _schools[index],
      index: index,
    ),
  ),
),
  • 面板里放 Expanded + ListView:面板多高,列表多高;
  • 列表滚到顶部继续下拉 → 手势被组件接住 → 面板收起(smooth_sheets 的核心联动,本页零代码实现);
  • 列表项 _SchoolListTile 用图标 + 双行文本 + 导航按钮组成,颜色按 index % 8 轮换(8 色数组)。

五、踩坑记录

  1. snapFactors 是屏高比例,不是固定像素——改屏幕/横竖屏后停靠点自动适配,别写死 300.0 这类像素值;
  2. 想"盖住背景不可交互"的场景(确认弹窗)别用 PersistentSnapSheet,用 showModalBottomSheet(见 note_picker_demo.md 的选图弹层);
  3. 面板内滚动列表必须用 Expanded 包裹,否则列表高度 = 内容高度,滚不动;
  4. 手柄区域 HitTestBehavior.opaque 保证空白处也能接住拖拽手势;
  5. 真实项目把 _buildMapBackground 换成高德/百度地图 SDK 组件即可,面板层完全不用动。

六、扩展思考

  • 对比模态弹层:showModalBottomSheet 的用法见 note_picker_demo.md(PickerService 内部);
  • 面板里的列表复用 list_demo.md 的 ListView.separated 知识点;
  • 面板内容进阶:搜索框 + 学校筛选(联动 input_demo.md 的 TextField 知识);
  • 组件本体 PersistentSnapSheet 在 lib/core/components/persistent_snap_sheet.dart——读它 = 学 smooth_sheets 包的使用。