📖 什么是 CustomScrollView?

CustomScrollView 是 Flutter 中最强大的滚动组件,它可以将多种不同类型的滚动组件(列表、网格、固定头部等)组合在一起,实现复杂的滚动效果。

核心特点

  • ✅ 组合多种滚动组件
  • ✅ 实现吸顶效果(Sticky Header)
  • ✅ 支持折叠头部(AppBar)
  • ✅ 统一管理滚动行为
  • ✅ 性能优秀(懒加载)

简单理解

想象一个购物 App 首页:顶部有大图横幅、下面是分类网格、再下面是商品列表。CustomScrollView 就像一个"超级容器",把这些不同的内容无缝组合在一起滚动。


🎯 基本概念

Sliver 是什么?

Sliver 是"薄片"的意思,在 Flutter 中指可以滚动的组件片段。

常用的 Sliver 组件:

  • SliverAppBar - 可折叠的头部
  • SliverList - 列表
  • SliverGrid - 网格
  • SliverToBoxAdapter - 普通组件包装器
  • SliverPadding - 带内边距的容器
  • SliverFillRemaining - 填充剩余空间

📋 基本用法

最简单的例子

CustomScrollView(
  slivers: [
    SliverToBoxAdapter(
      child: Container(
        height: 200,
        color: Colors.blue,
        child: Center(child: Text('顶部区域')),
      ),
    ),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(title: Text('项目 $index')),
        childCount: 20,
      ),
    ),
  ],
)

💡 10个实用案例

案例1:基础组合(固定内容 + 列表)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            // 固定的头部
            SliverToBoxAdapter(
              child: Container(
                height: 200,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    colors: [Colors.blue, Colors.purple],
                  ),
                ),
                child: Center(
                  child: Text(
                    '欢迎使用',
                    style: TextStyle(color: Colors.white, fontSize: 32),
                  ),
                ),
              ),
            ),
            // 列表
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Card(
                    margin: EdgeInsets.all(10),
                    child: ListTile(
                      leading: CircleAvatar(child: Text('${index + 1}')),
                      title: Text('列表项 ${index + 1}'),
                      subtitle: Text('这是第 ${index + 1} 个项目'),
                    ),
                  );
                },
                childCount: 20,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

应用场景:个人主页、设置页面


案例2:可折叠的 AppBar

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            // 可折叠的 AppBar
            SliverAppBar(
              expandedHeight: 200,
              pinned: true,  // 固定在顶部
              flexibleSpace: FlexibleSpaceBar(
                title: Text('可折叠标题'),
                background: Image.network(
                  'https://picsum.photos/400/200',
                  fit: BoxFit.cover,
                ),
              ),
            ),
            // 内容列表
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Container(
                    height: 80,
                    margin: EdgeInsets.all(10),
                    color: Colors.primaries[index % Colors.primaries.length],
                    child: Center(
                      child: Text(
                        '项目 ${index + 1}',
                        style: TextStyle(color: Colors.white, fontSize: 20),
                      ),
                    ),
                  );
                },
                childCount: 30,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

应用场景:文章详情、商品详情、个人资料页


案例3:列表 + 网格组合

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            // AppBar
            SliverAppBar(
              title: Text('购物中心'),
              floating: true,
            ),
            // 分类标题
            SliverToBoxAdapter(
              child: Padding(
                padding: EdgeInsets.all(15),
                child: Text(
                  '热门分类',
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
              ),
            ),
            // 网格
            SliverGrid(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Container(
                    decoration: BoxDecoration(
                      color: Colors.primaries[index % Colors.primaries.length],
                      borderRadius: BorderRadius.circular(10),
                    ),
                    child: Center(
                      child: Text(
                        '分类${index + 1}',
                        style: TextStyle(color: Colors.white, fontSize: 18),
                      ),
                    ),
                  );
                },
                childCount: 6,
              ),
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                mainAxisSpacing: 10,
                crossAxisSpacing: 10,
                childAspectRatio: 1.0,
              ),
            ),
            // 商品标题
            SliverToBoxAdapter(
              child: Padding(
                padding: EdgeInsets.all(15),
                child: Text(
                  '推荐商品',
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
              ),
            ),
            // 商品列表
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Card(
                    margin: EdgeInsets.symmetric(horizontal: 15, vertical: 8),
                    child: ListTile(
                      leading: Container(
                        width: 60,
                        height: 60,
                        color: Colors.grey[300],
                        child: Icon(Icons.shopping_bag),
                      ),
                      title: Text('商品 ${index + 1}'),
                      subtitle: Text(${(index + 1) * 99}'),
                      trailing: Icon(Icons.arrow_forward_ios, size: 16),
                    ),
                  );
                },
                childCount: 20,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

应用场景:电商首页、分类页面


案例4:吸顶效果(Sticky Header)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              title: Text('吸顶效果'),
              pinned: true,
            ),
            // 第一组
            _buildStickyHeader('分组 A'),
            _buildList(10, Colors.red),
            // 第二组
            _buildStickyHeader('分组 B'),
            _buildList(10, Colors.blue),
            // 第三组
            _buildStickyHeader('分组 C'),
            _buildList(10, Colors.green),
          ],
        ),
      ),
    );
  }

  Widget _buildStickyHeader(String title) {
    return SliverPersistentHeader(
      pinned: true,
      delegate: _StickyHeaderDelegate(
        child: Container(
          color: Colors.grey[300],
          padding: EdgeInsets.all(15),
          child: Text(
            title,
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
        ),
      ),
    );
  }

  Widget _buildList(int count, Color color) {
    return SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          return Container(
            height: 60,
            margin: EdgeInsets.all(5),
            color: color.withOpacity(0.3),
            child: Center(child: Text('项目 ${index + 1}')),
          );
        },
        childCount: count,
      ),
    );
  }
}

class _StickyHeaderDelegate extends SliverPersistentHeaderDelegate {
  final Widget child;

  _StickyHeaderDelegate({required this.child});

  
  Widget build(context, double shrinkOffset, bool overlapsContent) {
    return child;
  }

  
  double get maxExtent => 50;

  
  double get minExtent => 50;

  
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    return false;
  }
}

应用场景:通讯录、分组列表


案例5:下拉刷新

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  List<String> items = List.generate(20, (i) => '项目 ${i + 1}');

  Future<void> _refresh() async {
    await Future.delayed(Duration(seconds: 2));
    setState(() {
      items = List.generate(20, (i) => '刷新后 ${i + 1}');
    });
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: RefreshIndicator(
          onRefresh: _refresh,
          child: CustomScrollView(
            slivers: [
              SliverAppBar(
                title: Text('下拉刷新'),
                floating: true,
              ),
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return Card(
                      margin: EdgeInsets.all(10),
                      child: ListTile(
                        title: Text(items[index]),
                        leading: Icon(Icons.star),
                      ),
                    );
                  },
                  childCount: items.length,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

应用场景:新闻列表、社交动态


案例6:填充剩余空间

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              title: Text('填充剩余空间'),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) => ListTile(title: Text('项目 ${index + 1}')),
                childCount: 5,
              ),
            ),
            // 填充剩余空间
            SliverFillRemaining(
              child: Container(
                color: Colors.blue[100],
                child: Center(
                  child: Text(
                    '这个区域会填充剩余空间',
                    style: TextStyle(fontSize: 18),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

应用场景:空状态页面、底部固定内容


案例7:带内边距的滚动

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              title: Text('带内边距'),
            ),
            SliverPadding(
              padding: EdgeInsets.all(20),
              sliver: SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return Container(
                      height: 80,
                      margin: EdgeInsets.only(bottom: 10),
                      decoration: BoxDecoration(
                        color: Colors.blue,
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: Center(
                        child: Text(
                          '项目 ${index + 1}',
                          style: TextStyle(color: Colors.white, fontSize: 20),
                        ),
                      ),
                    );
                  },
                  childCount: 20,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

应用场景:需要整体内边距的列表


案例8:多个网格

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              title: Text('多个网格'),
              floating: true,
            ),
            // 第一个网格 - 3列
            SliverPadding(
              padding: EdgeInsets.all(10),
              sliver: SliverGrid(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return Container(
                      decoration: BoxDecoration(
                        color: Colors.red[300],
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: Center(child: Text('A${index + 1}')),
                    );
                  },
                  childCount: 6,
                ),
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 3,
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                ),
              ),
            ),
            // 分隔标题
            SliverToBoxAdapter(
              child: Container(
                padding: EdgeInsets.all(15),
                child: Text('第二组', style: TextStyle(fontSize: 20)),
              ),
            ),
            // 第二个网格 - 2列
            SliverPadding(
              padding: EdgeInsets.all(10),
              sliver: SliverGrid(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return Container(
                      decoration: BoxDecoration(
                        color: Colors.blue[300],
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: Center(child: Text('B${index + 1}')),
                    );
                  },
                  childCount: 4,
                ),
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2,
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                  childAspectRatio: 1.5,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

应用场景:相册、商品展示


案例9:复杂的首页布局

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            // 1. 可折叠的头部
            SliverAppBar(
              expandedHeight: 200,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: Text('首页'),
                background: Container(
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      colors: [Colors.purple, Colors.blue],
                    ),
                  ),
                ),
              ),
            ),
            // 2. 轮播图区域
            SliverToBoxAdapter(
              child: Container(
                height: 150,
                margin: EdgeInsets.all(10),
                decoration: BoxDecoration(
                  color: Colors.orange[200],
                  borderRadius: BorderRadius.circular(10),
                ),
                child: Center(child: Text('轮播图', style: TextStyle(fontSize: 24))),
              ),
            ),
            // 3. 分类标题
            SliverToBoxAdapter(
              child: Padding(
                padding: EdgeInsets.all(15),
                child: Text('热门分类', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
              ),
            ),
            // 4. 分类网格
            SliverPadding(
              padding: EdgeInsets.symmetric(horizontal: 10),
              sliver: SliverGrid(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    final icons = [Icons.phone_android, Icons.laptop, Icons.watch, Icons.headphones];
                    final labels = ['手机', '电脑', '手表', '耳机'];
                    return Container(
                      decoration: BoxDecoration(
                        color: Colors.blue[100],
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Icon(icons[index], size: 40),
                          SizedBox(height: 5),
                          Text(labels[index]),
                        ],
                      ),
                    );
                  },
                  childCount: 4,
                ),
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 4,
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                ),
              ),
            ),
            // 5. 推荐标题
            SliverToBoxAdapter(
              child: Padding(
                padding: EdgeInsets.all(15),
                child: Text('为你推荐', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
              ),
            ),
            // 6. 推荐列表
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Card(
                    margin: EdgeInsets.symmetric(horizontal: 15, vertical: 5),
                    child: ListTile(
                      leading: Container(
                        width: 60,
                        height: 60,
                        decoration: BoxDecoration(
                          color: Colors.primaries[index % Colors.primaries.length],
                          borderRadius: BorderRadius.circular(8),
                        ),
                      ),
                      title: Text('商品 ${index + 1}'),
                      subtitle: Text('这是一个很棒的商品'),
                      trailing: Text(${(index + 1) * 99}', style: TextStyle(color: Colors.red, fontSize: 18)),
                    ),
                  );
                },
                childCount: 20,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

应用场景:电商首页、新闻首页


案例10:聊天界面(反向滚动)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('聊天')),
        body: Column(
          children: [
            Expanded(
              child: CustomScrollView(
                reverse: true,  // 反向滚动
                slivers: [
                  SliverList(
                    delegate: SliverChildBuilderDelegate(
                      (context, index) {
                        final isMe = index % 2 == 0;
                        return Align(
                          alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
                          child: Container(
                            margin: EdgeInsets.all(10),
                            padding: EdgeInsets.all(12),
                            decoration: BoxDecoration(
                              color: isMe ? Colors.blue[100] : Colors.grey[300],
                              borderRadius: BorderRadius.circular(15),
                            ),
                            child: Text('消息 ${index + 1}'),
                          ),
                        );
                      },
                      childCount: 20,
                    ),
                  ),
                ],
              ),
            ),
            // 输入框
            Container(
              padding: EdgeInsets.all(10),
              color: Colors.white,
              child: Row(
                children: [
                  Expanded(
                    child: TextField(
                      decoration: InputDecoration(
                        hintText: '输入消息...',
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(25),
                        ),
                      ),
                    ),
                  ),
                  SizedBox(width: 10),
                  CircleAvatar(
                    child: IconButton(
                      icon: Icon(Icons.send, color: Colors.white),
                      onPressed: () {},
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

应用场景:即时通讯、评论区


❓ 常见问题 FAQ

1. CustomScrollView 和 ListView 有什么区别?

特性 CustomScrollView ListView
灵活性 非常灵活,可组合多种组件 只能显示列表
使用难度 较复杂 简单
性能 优秀 优秀
适用场景 复杂布局 简单列表

2. 什么时候用 CustomScrollView?

使用场景:

  • ✅ 需要组合多种滚动组件(列表+网格)
  • ✅ 需要可折叠的 AppBar
  • ✅ 需要吸顶效果
  • ✅ 需要复杂的首页布局

不需要时:

  • ❌ 只是简单的列表 → 用 ListView
  • ❌ 只是简单的网格 → 用 GridView
  • ❌ 固定内容 → 用 SingleChildScrollView

3. SliverToBoxAdapter 是什么?

它是一个"适配器",可以把普通组件(Container、Text 等)包装成 Sliver。

// ❌ 错误:不能直接放普通组件
CustomScrollView(
  slivers: [
    Container(height: 100),  // 错误!
  ],
)

// ✅ 正确:用 SliverToBoxAdapter 包装
CustomScrollView(
  slivers: [
    SliverToBoxAdapter(
      child: Container(height: 100),  // 正确!
    ),
  ],
)

4. 如何实现吸顶效果?

使用 SliverPersistentHeader 配合 pinned: true

SliverPersistentHeader(
  pinned: true,  // 吸顶
  delegate: YourDelegate(),
)

5. SliverAppBar 的三种模式是什么?

// 1. pinned: true - 固定在顶部
SliverAppBar(pinned: true)

// 2. floating: true - 滚动时立即出现
SliverAppBar(floating: true)

// 3. snap: true - 快速展开/收起(需要配合 floating)
SliverAppBar(floating: true, snap: true)

6. 如何让 CustomScrollView 反向滚动?

CustomScrollView(
  reverse: true,  // 从底部开始
  slivers: [...],
)

7. SliverList 和 ListView 的区别?

// ListView - 独立使用
ListView.builder(
  itemBuilder: (context, index) => Widget(),
)

// SliverList - 在 CustomScrollView 中使用
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => Widget(),
  ),
)

8. 如何在 CustomScrollView 中添加下拉刷新?

RefreshIndicator 包裹:

RefreshIndicator(
  onRefresh: () async {
    // 刷新逻辑
  },
  child: CustomScrollView(
    slivers: [...],
  ),
)

📊 常用 Sliver 组件对比

Sliver 组件 用途 示例
SliverAppBar 可折叠头部 商品详情页
SliverList 列表 新闻列表
SliverGrid 网格 相册
SliverToBoxAdapter 普通组件包装 横幅、标题
SliverPadding 带内边距 整体留白
SliverFillRemaining 填充剩余空间 空状态
SliverPersistentHeader 吸顶头部 分组标题

🎓 总结

核心要点

  1. CustomScrollView 用于组合多种滚动组件
  2. 所有子组件必须是 Sliver 类型
  3. 普通组件需要用 SliverToBoxAdapter 包装
  4. SliverAppBar 可以实现折叠效果
  5. 性能优秀,支持懒加载

最佳实践

  • ✅ 复杂首页布局使用 CustomScrollView
  • ✅ 需要吸顶效果时使用 SliverPersistentHeader
  • ✅ 组合列表和网格时使用 CustomScrollView
  • ✅ 使用 SliverPadding 统一管理内边距
  • ❌ 简单列表不要过度使用
  • ❌ 不要嵌套多个 CustomScrollView

适用场景

  • 🛒 电商首页(轮播+分类+商品)
  • 📱 社交媒体(头部+动态+推荐)
  • 📰 新闻应用(头图+分类+列表)
  • 👤 个人主页(头像+信息+动态)
  • 📧 邮件列表(分组+吸顶)
  • 💬 聊天界面(反向滚动)

🔧 实用代码片段

快速创建 SliverList

SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => YourWidget(),
    childCount: 20,
  ),
)

快速创建 SliverGrid

SliverGrid(
  delegate: SliverChildBuilderDelegate(
    (context, index) => YourWidget(),
    childCount: 20,
  ),
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 10,
    crossAxisSpacing: 10,
  ),
)

快速创建吸顶标题

SliverPersistentHeader(
  pinned: true,
  delegate: _HeaderDelegate(
    child: Container(
      color: Colors.grey[300],
      child: Text('标题'),
    ),
  ),
)

提示:CustomScrollView 是 Flutter 中最强大的滚动组件,掌握它能让你实现各种复杂的滚动效果!建议从简单案例开始,逐步掌握各种 Sliver 组件的用法。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐