126 lines
4.8 KiB
Markdown
126 lines
4.8 KiB
Markdown
# 【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 包的使用。
|