4.8 KiB
4.8 KiB
【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 色数组)。
五、踩坑记录
- snapFactors 是屏高比例,不是固定像素——改屏幕/横竖屏后停靠点自动适配,别写死 300.0 这类像素值;
- 想"盖住背景不可交互"的场景(确认弹窗)别用 PersistentSnapSheet,用
showModalBottomSheet(见 note_picker_demo.md 的选图弹层); - 面板内滚动列表必须用 Expanded 包裹,否则列表高度 = 内容高度,滚不动;
- 手柄区域
HitTestBehavior.opaque保证空白处也能接住拖拽手势; - 真实项目把
_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 包的使用。