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条ListTileAppBar共用同一个滚动

常见的Sliver速查表

名称作用典型参数
SliverList单列/多列列表delegate
SliverFixedExtentList高度固定列表,性能更好itemExtent
SliverGrid网格gridDelegate
SliverPadding给 sliver 加 paddingpadding
SliverToBoxAdapter把普通 Widget 包成 sliverchild
SliverAppBar可折叠/吸顶 AppBarpinned / 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,滚动位置统一

滚动监听与下拉刷新

  1. 监听滚动位置
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,
  ...
)
  1. 下拉刷新
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
})

属性

名称类型默认值说明
sliversList<Widget>const []要展示的 sliver 列表
controllerScrollController?null控制滚动位置、监听事件
scrollDirectionAxisAxis.vertical滚动方向
reverseboolfalse是否反向(从底部/右侧开始)
physicsScrollPhysics?null滚动物理效果(Bouncing/Clamping/NeverScrollable…)
shrinkWrapboolfalse是否按内容高度收缩(很少用,影响性能)
cacheExtentdouble?null预渲染区域(提高滚动流畅度)
anchordouble0.0起始锚点(0.0 顶部,1.0 底部)
keyboardDismissBehaviorScrollViewKeyboardDismissBehaviormanual滚动时键盘行为(onDrag / manual)
restorationIdString?null状态恢复 id

滚动控制相关对象

对象常用属性 / 方法作用
ScrollControlleroffset当前滚动像素
position获取 ScrollPosition
animateTo(offset, …)动画滚动到指定位置
jumpTo(offset)立即跳转
addListener / removeListener监听滚动
ScrollPositionpixels当前像素
maxScrollExtent最大可滚动距离
minScrollExtent最小可滚动距离
userScrollDirection用户滚动方向(idle / forward / reverse)
ensureVisible(renderObject, …)确保某个 RenderBox 可见

tips

  • 想控制滚动 → 用 ScrollController
  • 想加 AppBar / 吸顶 → 用 SliverAppBar / SliverPersistentHeader
  • 想加列表 → 用 SliverList / SliverGrid
  • 想加普通 Widget → 用 SliverToBoxAdapter