CustomScrollView
一个使用slivers创建自定义滚动效果的ScrollView
CustomScrollView是Flutter中功能最强大、最灵活的滚动容器。它把“滚动”这件事拆成3个核心概念:
ScrollController,控制滚动位置、监听滚动事件Viewport,可视区域(窗口)Slivers,真正绘制内容的“可滚动片段”
只要能把界面拆成若干个Sliver,就能把它们塞进同一个CustomScrollView,实现“一个滚动里嵌套多种布局”的效果(官方称之为“Sliver组合”)。
什么时候使用CustomScrollView
| 需求场景 | 是否推荐 |
|---|---|
| 需要把 AppBar、TabBar、List、Grid、瀑布流放在同一个滚动里 | ✅ |
| 想实现「吸顶/吸底」的 SliverAppBar、SliverPersistentHeader | ✅ |
| 需要监听滚动位置做动画、懒加载、下拉刷新 | ✅ |
| 只是简单的单列/多列列表 | ❌(直接用 ListView / GridView 即可) |
一个简单Demo
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
const SliverAppBar(
title: Text('CustomScrollView 入门'),
pinned: true, // 吸顶
expandedHeight: 200, // 展开高度
flexibleSpace: FlexibleSpaceBar(
background: FlutterLogo(),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item $index'),
),
childCount: 30,
),
),
],
),
);
}
}

运行效果:
- 顶部大Logo可折叠,折叠后标题栏吸顶
- 下方30条
ListTile与AppBar共用同一个滚动
常见的Sliver速查表
| 名称 | 作用 | 典型参数 |
|---|---|---|
| SliverList | 单列/多列列表 | delegate |
| SliverFixedExtentList | 高度固定列表,性能更好 | itemExtent |
| SliverGrid | 网格 | gridDelegate |
| SliverPadding | 给 sliver 加 padding | padding |
| SliverToBoxAdapter | 把普通 Widget 包成 sliver | child |
| SliverAppBar | 可折叠/吸顶 AppBar | pinned / floating / snap |
| SliverPersistentHeader | 自定义吸顶/吸底头 | delegate |
| SliverFillRemaining | 占满剩余空间 | hasScrollBody |
进阶: 组合多种布局
- 顶部大Banner(可折叠)
- 中间横向
ListView(不滚动,只是横向滑动) - 下方瀑布流
Grid - 最底下一个“加载更多”按钮
CustomScrollView(
slivers: [
// 1. 可折叠 Banner
SliverAppBar(
title: Text("Title"),
pinned: true,
expandedHeight: 200, // 展开高度
flexibleSpace: FlexibleSpaceBar(
background: FlutterLogo(),
),
),
// 2. 横向 ListView(用 SliverToBoxAdapter 包一层)
SliverToBoxAdapter(
child: SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (_, i) => Card(child: Text('Card $i')),
itemCount: 10,
),
),
),
// 3. 瀑布流
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.8,
),
delegate: SliverChildBuilderDelegate(
(_, i) => Image.network('https://picsum.photos/200?index=$i'),
childCount: 40,
),
),
),
// 4. 加载更多按钮
SliverToBoxAdapter(
child: TextButton(
onPressed: () {},
child: const Text('Load more'),
),
),
],
)

关键点:
- 横向
ListView本身不滚动,只是横向布局,因此不会与外层CustomScrollView冲突 - 所有
Sliver共用同一个ScrollController,滚动位置统一
滚动监听与下拉刷新
- 监听滚动位置
final ctrl = ScrollController();
void initState() {
super.initState();
ctrl.addListener(() {
final offset = ctrl.position.pixels;
final max = ctrl.position.maxScrollExtent;
if (offset >= max - 200) {
// 快到底了,触发加载更多
}
});
}
CustomScrollView(
controller: ctrl,
...
)
- 下拉刷新
CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
CupertinoSliverRefreshControl(
onRefresh: () async => await Future.delayed(const Duration(seconds: 1)),
),
...
],
)
性能Tips
- 大数据列表使用
SliverList+SliverChildBuilderDelegate,不要一次性生成所有子节点 - 固定高度列表使用
SliverFixedExtentList,避免测量 - 避免在
SliverToBoxAdapter里放可滚动的ListView(会冲突),除非设置NeverScrollableScrollPhysics - 嵌套场景使用
NestedScrollView+SliverOverlapAbsorber,但90%的需求CustomScrollView就能解决
构造函数
CustomScrollView.new({
Key? key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
ScrollBehavior? scrollBehavior,
bool shrinkWrap = false,
Key? center,
double anchor = 0.0,
double? cacheExtent,
SliverPaintOrder paintOrder = SliverPaintOrder.firstIsTop,
List<Widget> slivers = const <Widget>[],
int? semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior,
String? restorationId,
Clip clipBehavior = Clip.hardEdge,
HitTestBehavior hitTestBehavior = HitTestBehavior.opaque
})
属性
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| slivers | List<Widget> | const [] | 要展示的 sliver 列表 |
| controller | ScrollController? | null | 控制滚动位置、监听事件 |
| scrollDirection | Axis | Axis.vertical | 滚动方向 |
| reverse | bool | false | 是否反向(从底部/右侧开始) |
| physics | ScrollPhysics? | null | 滚动物理效果(Bouncing/Clamping/NeverScrollable…) |
| shrinkWrap | bool | false | 是否按内容高度收缩(很少用,影响性能) |
| cacheExtent | double? | null | 预渲染区域(提高滚动流畅度) |
| anchor | double | 0.0 | 起始锚点(0.0 顶部,1.0 底部) |
| keyboardDismissBehavior | ScrollViewKeyboardDismissBehavior | manual | 滚动时键盘行为(onDrag / manual) |
| restorationId | String? | null | 状态恢复 id |
滚动控制相关对象
| 对象 | 常用属性 / 方法 | 作用 |
|---|---|---|
| ScrollController | offset | 当前滚动像素 |
| position | 获取 ScrollPosition | |
| animateTo(offset, …) | 动画滚动到指定位置 | |
| jumpTo(offset) | 立即跳转 | |
| addListener / removeListener | 监听滚动 | |
| ScrollPosition | pixels | 当前像素 |
| maxScrollExtent | 最大可滚动距离 | |
| minScrollExtent | 最小可滚动距离 | |
| userScrollDirection | 用户滚动方向(idle / forward / reverse) | |
| ensureVisible(renderObject, …) | 确保某个 RenderBox 可见 |
tips
- 想控制滚动 → 用 ScrollController
- 想加 AppBar / 吸顶 → 用 SliverAppBar / SliverPersistentHeader
- 想加列表 → 用 SliverList / SliverGrid
- 想加普通 Widget → 用 SliverToBoxAdapter