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

126 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【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` / 抽屉组件的"常驻版"——不挡背景、可多档位。
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: SmoothSheetDemoPage()));
```
- 拖拽面板松手 → 观察自动吸附最近停靠点;
- 收起状态点地图背景 → 背景仍可交互(非模态);
- 展开面板后滚动列表到底再下拉 → 面板收起;
- 顶部 3 个按钮可代码控制面板跳到指定停靠点。
---
## 三、核心知识点(新手向)
### 1. 三行配置:停靠点比例
```dart
/// 3 个停靠点:底部收起 / 半屏 / 大半屏
static const List<double> _snapFactors = [0.12, 0.5, 0.85];
```
`snapFactors` 是**占屏高的比例**(0.12 = 12% 屏高),组件负责换算成像素并做吸附动画。
### 2. 结构:Stack 背景层 + 面板层
```dart
Scaffold(
body: Stack(
children: [
_buildMapBackground(context), // 背景层:模拟地图(真实项目换地图 SDK)
PersistentSnapSheet( // 面板层:常驻面板
snapFactors: _snapFactors,
child: Column(
children: [
_buildHandle(context), // 手柄区域
Expanded(child: _buildSheetContent(context)), // 面板内容
],
),
),
],
),
)
```
### 3. 经典拖拽指示条(36×4 圆角小横条)
```dart
Container(
width: 36.w,
height: 4.h,
decoration: BoxDecoration(
color: const Color(0xFFD1D5DB),
borderRadius: BorderRadius.circular(2.r),
),
)
```
这是 iOS/微信风格的"可拖拽暗示":看到它就明白面板能拉。
### 4. 非模态:面板盖住背景,但不拦截背景交互
`PersistentSnapSheet` 是**常驻布局组件**(不是 `showModalBottomSheet` 弹窗),
面板下面的地图照常能点——这是"常驻面板"和"模态弹层"的本质区别。
---
## 四、关键代码剖析
```dart
/// 面板内部内容:分割线 + 可滚动学校列表(与面板手势联动)
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](note_picker_demo.md) 的选图弹层);
3. 面板内滚动列表必须用 **Expanded 包裹**,否则列表高度 = 内容高度,滚不动;
4. 手柄区域 `HitTestBehavior.opaque` 保证空白处也能接住拖拽手势;
5. 真实项目把 `_buildMapBackground` 换成高德/百度地图 SDK 组件即可,面板层完全不用动。
---
## 六、扩展思考
- 对比模态弹层:`showModalBottomSheet` 的用法见 [note_picker_demo.md](note_picker_demo.md)(PickerService 内部);
- 面板里的列表复用 [list_demo.md](list_demo.md) 的 `ListView.separated` 知识点;
- 面板内容进阶:搜索框 + 学校筛选(联动 [input_demo.md](input_demo.md) 的 TextField 知识);
- 组件本体 `PersistentSnapSheet` 在 `lib/core/components/persistent_snap_sheet.dart`——读它 = 学 smooth_sheets 包的使用。