CustomScrollView可滚动视图

简介

什么是 CustomScrollView

CustomScrollView 是一个可滚动视图,但与 ListViewGridView 不同的是:

它可以同时容纳多种类型的滚动内容(Sliver),而不是只能是一种(线性列表)。

它是构建复杂滚动布局(例如:头部 + 列表 + 网格 + 吸顶标题)的基础。

关键概念:Sliver

CustomScrollViewchildren 不是普通 Widget,而是 Sliver

Sliver 是 Flutter 中可滚动区域的最小单位。

常见的 Sliver 类型:

  • SliverList:线性列表
  • SliverGrid:网格
  • SliverToBoxAdapter:把普通 Widget 转成 Sliver
  • SliverAppBar:可折叠的 AppBar
  • SliverPadding:为 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:定义网格布局规则,常见两种:
    • SliverGridDelegateWithFixedCrossAxisCount
      • crossAxisCount:每行列数
      • mainAxisSpacing:主轴(垂直)间距
      • crossAxisSpacing:交叉轴(水平)间距
      • childAspectRatio:子项宽高比
    • SliverGridDelegateWithMaxCrossAxisExtent
      • maxCrossAxisExtent:子项最大宽度,自动计算列数
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 会慢慢「浮现」出来,而不是等你滑回到列表顶部才出现。如果没有 floatingSliverAppBar 只能在完全滚回顶部时才会重新出现。

snap: true 必须和 floating: true 一起用。

floating: true + snap: true:AppBar 会立刻完整地弹出到全展开,而不是慢慢跟手指滑出。也就是只用floating: true ,弹出的速度会比较慢。

SliverPadding

单个 Sliver 添加内边距(类似 Padding)。

常用属性

  • paddingEdgeInsets 类型,定义内边距
  • 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(比如 GetBuilderContainerText并不是 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 组件(如 SliverListSliverGridSliverAppBar
scrollDirection Axis 滚动方向,默认 Axis.vertical
reverse bool 是否反向滚动(true = 列表倒序)
controller ScrollController 自定义滚动控制器(监听滚动、控制滚动位置等)
primary bool 是否是主滚动视图,默认 true(有多个滚动区域时可以设为 false

布局与滚动物理

属性 类型 作用
shrinkWrap bool 是否根据内容收缩高度,默认 false(通常保持默认提升性能)
physics ScrollPhysics 控制滚动行为(如 BouncingScrollPhysicsNeverScrollableScrollPhysics
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),
          ),
        ],
      ),
    );
  }
}

×

喜欢就点赞,疼爱就打赏