简介
什么是 CustomScrollView
CustomScrollView 是一个可滚动视图,但与 ListView 或 GridView 不同的是:
它可以同时容纳多种类型的滚动内容(Sliver),而不是只能是一种(线性列表)。
它是构建复杂滚动布局(例如:头部 + 列表 + 网格 + 吸顶标题)的基础。
关键概念:Sliver
CustomScrollView 的 children 不是普通 Widget,而是 Sliver。
Sliver 是 Flutter 中可滚动区域的最小单位。
常见的 Sliver 类型:
SliverList:线性列表SliverGrid:网格SliverToBoxAdapter:把普通 Widget 转成 SliverSliverAppBar:可折叠的 AppBarSliverPadding:为 Sliver 添加内边距
案例
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: CustomScrollDemo());
}
}
class CustomScrollDemo extends StatelessWidget {
const CustomScrollDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
// ① 可折叠的 SliverAppBar
SliverAppBar(
floating: true,
expandedHeight: 200,
flexibleSpace: const FlexibleSpaceBar(
title: Text('Sliver 示例'),
background: FlutterLogo(),
),
),
// ② 普通 Widget
SliverToBoxAdapter(
child: Container(
height: 100,
color: Colors.amber,
alignment: Alignment.center,
child: const Text('我是普通 Widget'),
),
),
// ③ 网格
SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
color: Colors.blue[100 * ((index % 8) + 1)],
alignment: Alignment.center,
child: Text('Grid $index'),
),
childCount: 8,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
),
),
// ④ 列表
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item $index')),
childCount: 20,
),
),
],
),
);
}
}
Sliver
SliverList
在 CustomScrollView 中显示一个垂直线性列表,类似 ListView,但只适用于 Sliver 体系。
常用属性:
delegate:必填,用来构建子项。支持两种:SliverChildBuilderDelegate:懒加载构建,适合大量数据(推荐)。SliverChildListDelegate:一次性传入子 widget 列表,适合少量固定数据
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(title: Text('Item $index'));
},
childCount: 20, // 子项数量
),
)
SliverGrid
显示网格布局的子项,类似 GridView,但用于 Sliver 体系。
常用属性:
delegate:同SliverList,用于构建子项。gridDelegate:定义网格布局规则,常见两种:SliverGridDelegateWithFixedCrossAxisCountcrossAxisCount:每行列数mainAxisSpacing:主轴(垂直)间距crossAxisSpacing:交叉轴(水平)间距childAspectRatio:子项宽高比
SliverGridDelegateWithMaxCrossAxisExtentmaxCrossAxisExtent:子项最大宽度,自动计算列数
SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
color: Colors.blue[100 * ((index % 8) + 1)],
alignment: Alignment.center,
child: Text('Grid $index'),
),
childCount: 8,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
),
),
SliverToBoxAdapter
在 Sliver 列表中嵌入一个普通 Widget,例如 Banner、广告、标题等。
常用属性:
child:唯一属性,放入要显示的普通 widget。
SliverToBoxAdapter(
child: Container(
height: 100,
color: Colors.amber,
alignment: Alignment.center,
child: const Text('我是普通 Widget'),
),
),
SliverAppBar
可随滚动展开/折叠的顶部 AppBar,常用于头图+标题的折叠效果。
常用属性:
title:标题pinned:是否固定在顶部(滑动到顶部后不消失)floating:是否可以在下滑时快速显示snap:与floating搭配使用,下滑时立即弹出expandedHeight:展开时的高度flexibleSpace:展开区域的内容(通常是FlexibleSpaceBar)
SliverAppBar(
pinned: true,
floating: true,
snap: true,
expandedHeight: 200,
flexibleSpace: const FlexibleSpaceBar(
title: Text('SliverAppBar 示例'),
background: FlutterLogo(),
),
)
FlexibleSpaceBar
title:顶部栏展开/收起时显示的标题 Widget。通常是Text,会随 AppBar 折叠动画缩放/位移background:展开区域的背景 Widget,常用来放图片、渐变背景等centerTitle:是否将标题居中显示(默认取决于平台:iOS 居中,Android 左对齐)titlePadding:标题的内边距 (EdgeInsets),用于微调标题位置collapseMode:控制背景随滚动的折叠模式CollapseMode.parallax(默认,视差滚动)CollapseMode.pin(固定)CollapseMode.none(不滚动)
stretchModes:当SliverAppBar.stretch: true时有效,控制拉伸时的动画效果StretchMode.zoomBackground(背景放大)StretchMode.blurBackground(背景模糊)StretchMode.fadeTitle(标题淡入淡出)
案例:如何理解展开与收起
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const NewsPage(),
theme: ThemeData(useMaterial3: true),
);
}
}
class NewsPage extends StatelessWidget {
const NewsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
pinned: true, // 折叠后顶部固定
floating: true, // 下拉时显示
snap: true, // 快速下拉时显示
expandedHeight: 200.0, // 展开高度
flexibleSpace: FlexibleSpaceBar(
title: const Text('今日头条'),
background: Image.network(
'https://picsum.photos/800/400',
fit: BoxFit.cover,
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('新闻内容 #$index')),
childCount: 100,
),
),
],
),
);
}
}
pinned: true的作用就是可以让背景图折叠,但是title还可以显示
floating: true的作用是:当你往上滚动一点点内容后再往下轻轻一拉,AppBar 会慢慢「浮现」出来,而不是等你滑回到列表顶部才出现。如果没有floating,SliverAppBar只能在完全滚回顶部时才会重新出现。
snap: true必须和floating: true一起用。
floating: true+snap: true:AppBar 会立刻完整地弹出到全展开,而不是慢慢跟手指滑出。也就是只用floating: true,弹出的速度会比较慢。
SliverPadding
给单个 Sliver 添加内边距(类似 Padding)。
常用属性:
padding:EdgeInsets类型,定义内边距sliver:要包裹的子 Sliver
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item $index')),
childCount: 5,
),
),
)
不能放普通的widget,否则报错
CustomScrollView.slivers 这个属性期望的是 一组 Sliver 类型的子组件,而大多数普通 Widget(比如 GetBuilder、Container、Text)并不是 Sliver。
如果你直接放一个普通的 Widget,框架会报运行时错误(assert),因为 RenderBox 不能直接放在 RenderSliver 体系中。
RenderBox was not laid out: RenderObject expected a Sliver but got a RenderBox.
如果果想在 slivers 中使用普通 Widget(包括 GetBuilder),应该 用 SliverToBoxAdapter 包起来:
// 轮播广告
Widget _buildBanner() {
return GetBuilder<HomeController>(
id: "home_banner",
builder: (_) {
return CarouselWidget(
items: controller.bannerItems,
currentIndex: controller.bannerCurrentIndex,
onPageChanged: controller.onChangeBanner,
height: 190.w,
);
})
.clipRRect(all: AppRadius.image)
.sliverToBoxAdapter() // 转成 sliver: SliverToBoxAdapter
.sliverPaddingHorizontal(AppSpace.page);
}
Widget sliverToBoxAdapter({
Key? key,
}) =>
SliverToBoxAdapter(key: key, child: this);
CustomScrollView 的常用属性
基本属性
| 属性 | 类型 | 作用 |
|---|---|---|
slivers |
List<Widget> |
必填,放入要滚动的 sliver 组件(如 SliverList、SliverGrid、SliverAppBar) |
scrollDirection |
Axis |
滚动方向,默认 Axis.vertical |
reverse |
bool |
是否反向滚动(true = 列表倒序) |
controller |
ScrollController |
自定义滚动控制器(监听滚动、控制滚动位置等) |
primary |
bool |
是否是主滚动视图,默认 true(有多个滚动区域时可以设为 false) |
布局与滚动物理
| 属性 | 类型 | 作用 |
|---|---|---|
shrinkWrap |
bool |
是否根据内容收缩高度,默认 false(通常保持默认提升性能) |
physics |
ScrollPhysics |
控制滚动行为(如 BouncingScrollPhysics、NeverScrollableScrollPhysics) |
scrollBehavior |
ScrollBehavior? |
自定义滚动行为(如去除滚动阴影效果) |
性能与缓存
| 属性 | 类型 | 作用 |
|---|---|---|
cacheExtent |
double |
预加载区域的长度(视窗外的缓存区) |
dragStartBehavior |
DragStartBehavior |
拖动开始行为,默认 start |
keyboardDismissBehavior |
ScrollViewKeyboardDismissBehavior |
滚动时是否隐藏键盘,默认 manual |
clipBehavior |
Clip |
超出边界是否裁剪,默认 Clip.hardEdge |
案例
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: CustomScrollDemo());
}
}
class CustomScrollDemo extends StatelessWidget {
const CustomScrollDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('CustomScrollView 属性演示')),
body: CustomScrollView(
scrollDirection: Axis.vertical, // 纵向滚动
reverse: true, // 反向
primary: true, // 作为主滚动视图
shrinkWrap: false, // 不收缩,提升性能
physics: const BouncingScrollPhysics(), // iOS 弹性滚动效果
cacheExtent: 300.0, // 预加载 300 像素范围的内容
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag, // 拖动时关闭键盘
clipBehavior: Clip.hardEdge, // 超出边界裁剪
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
debugPrint('开始构建: item $index');
return ListTile(title: Text('Item #$index'));
}, childCount: 50),
),
],
),
);
}
}