# 【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 _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 包的使用。