Scroll
SingleChildScrollView 是单子组件可滚动容器,适用于内容可能超出屏幕、需要滚动的场景。ScrollController 用于控制滚动位置和监听滚动事件。
构造函数
dart
SingleChildScrollView({
Key? key, // 组件标识
Axis scrollDirection = Axis.vertical, // 滚动方向
bool reverse = false, // 是否反向滚动
EdgeInsetsGeometry? padding, // 内边距
bool primary, // 是否使用 PrimaryScrollController
ScrollPhysics? physics, // 滚动物理效果
ScrollController? controller, // 滚动控制器
Widget? child, // 子组件
DragStartBehavior dragStartBehavior = DragStartBehavior.start, // 拖拽行为
Clip clipBehavior = Clip.hardEdge, // 裁剪行为
String? restorationId, // 状态恢复 ID
})属性速查
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
scrollDirection | Axis | vertical | 滚动方向 |
reverse | bool | false | 是否反向 |
padding | EdgeInsetsGeometry? | null | 内边距 |
physics | ScrollPhysics? | 平台默认 | 滚动物理效果 |
controller | ScrollController? | null | 滚动控制器 |
primary | bool | null | 是否使用 PrimaryScrollController |
child | Widget? | null | 子组件 |
常用值速查
scrollDirection — 滚动方向
| 值 | 说明 |
|---|---|
Axis.vertical | 垂直滚动(默认) |
Axis.horizontal | 水平滚动 |
physics — 滚动物理效果
| 值 | 说明 |
|---|---|
BouncingScrollPhysics() | iOS 风格:到边界后弹回 |
ClampingScrollPhysics() | Android 风格:到边界后停止,显示光晕 |
AlwaysScrollableScrollPhysics() | 始终可滚动(即使内容不足一屏) |
NeverScrollableScrollPhysics() | 禁止滚动 |
FixedExtentScrollPhysics() | 吸附到固定项(Picker 用) |
ScrollController
ScrollController 用于控制滚动位置和监听滚动事件,使用后必须在 dispose() 中释放。
构造函数
dart
ScrollController({
double initialScrollOffset = 0.0, // 初始滚动偏移
bool keepScrollOffset = true, // 是否保存滚动位置
String? debugLabel, // 调试标签
})属性与方法
| 属性/方法 | 类型 | 说明 |
|---|---|---|
offset | double | 当前滚动偏移(只读) |
position | ScrollPosition | 当前滚动位置信息 |
hasClients | bool | 是否有绑定的 ScrollView |
animateTo(offset, duration, curve) | Future<void> | 动画滚动到指定位置 |
jumpTo(offset) | void | 立即跳到指定位置(无动画) |
dispose() | void | 释放资源 |
滚动通知
使用 NotificationListener<ScrollNotification> 可以监听滚动事件,无需创建 ScrollController:
| 通知类型 | 说明 |
|---|---|
ScrollStartNotification | 开始滚动 |
ScrollUpdateNotification | 滚动更新 |
ScrollEndNotification | 结束滚动 |
OverscrollNotification | 过度滚动(到边界后继续拉) |
ScrollNotification | 所有滚动通知的基类 |
快速示例
基本可滚动页面
dart
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: const [
Text('内容1'),
Text('内容2'),
// ... 更多内容超出屏幕时自动滚动
],
),
)水平滚动
dart
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Container(width: 200, color: Colors.red),
Container(width: 200, color: Colors.blue),
Container(width: 200, color: Colors.green),
],
),
)用 ScrollController 控制滚动位置
dart
class ScrollExample extends StatefulWidget {
@override
State<ScrollExample> createState() => _ScrollExampleState();
}
class _ScrollExampleState extends State<ScrollExample> {
final _controller = ScrollController();
void _scrollToTop() {
_controller.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
void _scrollToBottom() {
_controller.animateTo(
_controller.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
ElevatedButton(onPressed: _scrollToTop, child: const Text('顶部')),
ElevatedButton(onPressed: _scrollToBottom, child: const Text('底部')),
],
),
Expanded(
child: SingleChildScrollView(
controller: _controller,
child: const Column(children: [/* ... */]),
),
),
],
);
}
}监听滚动位置
dart
final _controller = ScrollController();
@override
void initState() {
super.initState();
_controller.addListener(() {
print('当前偏移: ${_controller.offset}');
if (_controller.offset >= _controller.position.maxScrollExtent - 100) {
// 快到底部,加载更多
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}使用 NotificationListener 监听滚动
dart
NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is ScrollStartNotification) {
print('开始滚动');
} else if (notification is ScrollUpdateNotification) {
print('滚动中: ${notification.scrollDelta}'); // 本次滚动增量
} else if (notification is ScrollEndNotification) {
print('结束滚动');
}
return false; // false = 允许继续冒泡
},
child: SingleChildScrollView(
child: Column(children: const [/* ... */]),
),
)回到顶部按钮
dart
class BackToTopExample extends StatefulWidget {
@override
State<BackToTopExample> createState() => _BackToTopExampleState();
}
class _BackToTopExampleState extends State<BackToTopExample> {
final _controller = ScrollController();
bool _showBackToTop = false;
@override
void initState() {
super.initState();
_controller.addListener(() {
setState(() {
_showBackToTop = _controller.offset > 300;
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
SingleChildScrollView(
controller: _controller,
child: const Column(children: [/* ... */]),
),
if (_showBackToTop)
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton.small(
child: const Icon(Icons.arrow_upward),
onPressed: () => _controller.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
),
),
),
],
);
}
}常见错误
Column 内容溢出
dart
// ❌ 错误:Column 内容超出屏幕报溢出
Column(
children: [
TextField(...),
TextField(...),
TextField(...), // 键盘弹出后溢出
],
)
// ✅ 修复:用 SingleChildScrollView 包裹
SingleChildScrollView(
child: Column(
children: [
TextField(...),
TextField(...),
],
),
)SingleChildScrollView 内使用 Expanded
dart
// ❌ 错误:Expanded 需要无限空间,但 SingleChildScrollView 给了有限空间
SingleChildScrollView(
child: Column(
children: [
Expanded(child: ...), // 报错!
],
),
)
// ✅ 修复:使用固定高度或 SizedBox
SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 200, child: ...),
],
),
)ScrollController 未释放
dart
// ❌ 错误:忘记释放 Controller
final _controller = ScrollController();
// ✅ 必须在 dispose 中释放
@override
void dispose() {
_controller.dispose();
super.dispose();
}