Flutter速来系列23、关于Sliver的故事:列表,自定义Header,铺满屏幕,折叠

2,412 阅读13分钟

一、常见滚定组件

Flutter提供了多种滚动组件,可以用于处理各种滚动效果。

Sliver的子组件都能滚动,但并不是所有能滚动的组件都是Sliver子组件。比如,ListView和Grid就不是Sliver子组件。

重要说3遍

ListView和Grid就 不是 Sliver子组件。

ListView和Grid就 不是 Sliver子组件。

ListView和Grid就 不是 Sliver子组件。

在 Flutter 中,可滚动组件通常由三个角色组成

  • Scrollable:一个可滚动组件的基类,它定义了一个可滚动组件所需要的基本行为和接口。比如ListView和GridView

  • Viewport:表示一个视口,它用来显示可滚动组件中的内容。Viewport 可以是一个矩形,也可以是任意形状,它负责将滚动区域中的内容渲染到屏幕上。

  • Sliver:一个 Scrollable 的子组件,它是用来描述可滚动区域中的一段可滚动内容的。Sliver 可以是一个矩形,也可以是任意形状,它可以包含多个子组件,例如 SliverList、SliverGrid 等。


Sliver的子组件及其作用

Sliver的子组件都能滚动

Sliver子组件描述
SliverAppBar可折叠的应用栏,随着滚动进行展开和收起
SliverList垂直的线性列表,用于显示动态数量的列表项
SliverGrid二维网格布局,可在水平和垂直方向上滚动
SliverToBoxAdapter将普通的非Sliver组件包装为Sliver组件,用于在Sliver布局中使用
SliverFixedExtentList与SliverList类似,但所有列表项的高度都是固定的,可提高性能
SliverPersistentHeader创建一个持久化的Header,始终可见,并可包含其他子组件
SliverPadding为子组件提供填充,控制子组件与边界之间的间距
SliverOpacity设置子组件的透明度,可根据滚动位置或其他条件调整子组件的显示效果
SliverAnimatedList动态、带动画效果的列表,用于在滚动视图中显示数据的变化
SliverAnimatedOpacity根据滚动位置或其他条件,以动画的方式调整子组件的透明度

二、Flutter 中的 Sliver

在 Flutter 中,Sliver 是一种特殊的 Widget,它可以用于创建可滚动的、高性能的列表或网格。相比于普通的列表或网格,使用 Sliver 可以提高滑动性能,减少内存占用,并且可以支持更多的交互效果。

Sliver 的基本概念

在 Flutter 中,Sliver 是指一种可以滚动的可视区域,它可以有多个子节点,每个子节点可以是一个 Widget 或者一个 LayoutBuilder。根据子节点的类型和滚动方向的不同,可以将 Sliver 分为以下几种类型:

  • SliverAppBar:一个可以随着滚动渐变、折叠、固定在顶部或底部的 AppBar。
  • SliverList:一个垂直方向的可滚动列表。
  • SliverGrid:一个网格布局的可滚动列表。
  • SliverToBoxAdapter:一个包含单个子节点的 Sliver,可以用于将一个普通的 Widget 包装成一个可滚动的 Widget。
  • SliverFillRemaining:一个占满剩余空间的 Sliver,通常用于在 CustomScrollView 中填充屏幕剩余的空间。

使用 Sliver 创建可滚动列表

在 Flutter 中,创建一个可滚动的列表通常需要使用 ListView 或 GridView。但是这些 Widget 的性能并不总是最优,尤其是在列表项较多时。相比之下,使用 Sliver 可以更好地控制列表项的渲染和排布,从而提高性能。

创建 SliverList

SliverList 是一个用于显示可滚动列表的 Sliver 组件,它可以高效地渲染大量的列表项,并且可以和其他 Sliver 组件一起使用,构建复杂的可滚动布局。与普通的列表组件不同,SliverList 不会提前将所有列表项都渲染出来,而是在滚动时动态地渲染当前可见的部分,从而节省内存和渲染时间。

下面是一个简单的例子,演示如何使用 SliverList 显示一个包含 50 个列表项的可滚动列表:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SliverList Demo',
      home: Scaffold(
        body: CustomScrollView(
          slivers: <Widget>[
            SliverList(
              delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  print('Building item $index'); // 打印日志
                  return ListTile(
                    title: Text('Item $index'),
                    leading: CircleAvatar(child: Text('$index')),
                  );
                },
                childCount: 50,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

image.png

在这个例子中,我们创建了一个 CustomScrollView,其中包含了一个 SliverListSliverList 使用 SliverChildBuilderDelegate 来构建列表项,它会根据 childCount 属性的值来确定列表项的数量。在 SliverChildBuilderDelegate 中,我们可以使用 BuildContext 和 index 参数来构建每个列表项。在这个例子中,我们为每个列表项添加了一个圆形图标,以及一个显示编号的文本。当我们向上或向下滚动列表时,SliverList 会动态地渲染当前可见的列表项,从而保证了滚动的流畅性和性能。

注意看日志的打印

创建 SliverGrid

SliverGrid 是一个用于显示网格布局的 Sliver 组件,它可以高效地渲染大量的网格项,并且可以和其他 Sliver 组件一起使用,构建复杂的可滚动布局。与普通的网格布局组件不同,SliverGrid 不会提前将所有网格项都渲染出来,而是在滚动时动态地渲染当前可见的部分,从而节省内存和渲染时间。

下面是一个例子,演示如何使用 SliverGrid 显示一个包含 50 个格子的网格布局:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SliverGrid Demo',
      home: Scaffold(
        body: CustomScrollView(
          slivers: <Widget>[
            // 创建一个包含 50 个格子的 SliverGrid
            SliverGrid(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                // 指定每行包含 3 个格子
                crossAxisCount: 3,
              ),
              delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  // 构建格子
                  return Container(
                    color: Colors.blue[100 * (index % 9 + 1)],
                    alignment: Alignment.center,
                    child: Text('Grid $index'),
                  );
                },
                childCount: 50, // 格子数量
              ),
            ),
          ],
        ),
      ),
    );
  }
}

image.png

三、Sliver 的高级用法

除了基本的 Sliver 类型外,Flutter 还提供了一些高级的 Sliver 类型,例如 SliverPersistentHeaderSliverFillViewport 和 SliverOverlapInjector

三.1、 使用 SliverPersistentHeader 创建自定义 Header

SliverPersistentHeader 是一个可以自定义的 Header,它可以随着滚动渐变、折叠、固定在顶部或底部,并且可以包含任意的子节点。

与普通的头部或底部组件不同,SliverPersistentHeader 可以随着滚动而动态地改变自身的高度,从而实现更加灵活的布局效果。

下面是一个例子,演示如何使用 SliverPersistentHeader 实现一个固定在页面顶部的头部组件:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SliverPersistentHeader Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('SliverPersistentHeader Demo'),
        ),
        body: CustomScrollView(
          slivers: <Widget>[
            // 创建一个固定在页面顶部的 SliverPersistentHeader
            SliverPersistentHeader(
              pinned: true, // 固定在页面顶部
              delegate: _MyHeaderDelegate(),
            ),
            // 添加一个普通的 SliverList
            SliverList(
              delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  return ListTile(
                    title: Text('Item $index'),
                    leading: CircleAvatar(child: Text('$index')),
                  );
                },
                childCount: 50,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// 定义一个 SliverPersistentHeaderDelegate
class _MyHeaderDelegate extends SliverPersistentHeaderDelegate {
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    // 获取状态栏高度
    final double statusBarHeight = MediaQuery.of(context).padding.top;

    // 构建头部组件
    return Container(
      padding: EdgeInsets.only(top: statusBarHeight),
      color: Colors.blue,
      alignment: Alignment.center,
      child: Text('Header'),
    );
  }

  @override
  double get maxExtent => 100.0; // 最大高度

  @override
  double get minExtent => 50.0; // 最小高度

  @override
  bool shouldRebuild(_MyHeaderDelegate oldDelegate) {
    return false; // 不需要重新构建
  }
}

iShot_2023-07-11_21.03.22.gif

在这个例子中,我们创建了一个 CustomScrollView,其中包含了一个固定在页面顶部的 SliverPersistentHeader。我们使用 pinned 属性将头部组件固定在页面顶部,并使用 _MyHeaderDelegate 类来构建头部组件。在 _MyHeaderDelegate 中,我们实现了 buildmaxExtentminExtent 和 shouldRebuild 四个方法,分别用于构建头部组件、指定最大和最小高度以及控制是否需要重新构建头部组件。在这个例子中,我们只是简单地为头部组件添加了一个背景颜色和一个文本。当我们向上或向下滚动列表时,头部组件会随着滚动而动态地改变自身的高度,从而实现了更加灵活的布局效果

不够吗?再来一个例子

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SliverPersistentHeader Demo',
      home: Scaffold(
        body: CustomScrollView(
          slivers: <Widget>[
            // 创建一个固定在页面顶部的 SliverPersistentHeader
            SliverPersistentHeader(
              pinned: true, // 固定在页面顶部
              delegate: _MyHeaderDelegate(),
            ),
            // 添加一个普通的 SliverList
            SliverList(
              delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  return ListTile(
                    title: Text('Item $index'),
                    leading: CircleAvatar(child: Text('$index')),
                  );
                },
                childCount: 50,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// 定义一个 SliverPersistentHeaderDelegate
class _MyHeaderDelegate extends SliverPersistentHeaderDelegate {
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    // 计算当前 Header 的高度
    double height = maxExtent - shrinkOffset;
    if (height < minExtent) {
      height = minExtent;
    }

    // 计算当前 Header 的背景颜色
    final double alpha = (maxExtent - height) / (maxExtent - minExtent);
    final Color backgroundColor = Colors.blue.withOpacity(alpha);

    // 构建 Header
    return Stack(
      fit: StackFit.expand,
      children: [
        // 背景图片
        Image.network(
          'https://picsum.photos/id/1/800/600',
          fit: BoxFit.cover,
        ),
        // 渐变遮罩层
        Container(
          decoration: BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                backgroundColor.withOpacity(0.5),
                backgroundColor,
              ],
            ),
          ),
        ),
        // 折叠的内容
        Positioned(
          top: 100 - shrinkOffset,
          left: 16.0,
          right: 16.0,
          child: Opacity(
            opacity: 1 - alpha,
            child: Text(
              'Header',
              style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold, color: Colors.white),
            ),
          ),
        ),
      ],
    );
  }

  @override
  double get maxExtent => 200.0; // 最大高度

  @override
  double get minExtent => 50.0; // 最小高度

  @override
  bool shouldRebuild(_MyHeaderDelegate oldDelegate) {
    return false; // 不需要重新构建
  }
}

iShot_2023-07-11_21.05.05.gif

SliverPersistentHeader 对比 NestedScrollView

其实,NestedScrollView 也能实现类似的功能。

选择使用 SliverPersistentHeader 还是 NestedScrollView 取决于你的实际需求。下面是一些参考因素:

  • 如果你只需要一个简单的固定在顶部的 Header,那么使用 SliverPersistentHeader 可能更加简单明了。
  • 如果你需要在 Header 和内容之间添加一些复杂的交互逻辑,比如下拉刷新、上拉加载、折叠、渐变等效果,那么使用 NestedScrollView 可能更加灵活方便。
  • 如果你需要在页面中嵌套多个滚动组件,并且需要让它们进行联动,那么使用 NestedScrollView 是必要的选择。

总的来说,SliverPersistentHeader 和 NestedScrollView 都是非常强大和灵活的 Flutter 组件,可以帮助开发者实现各种复杂的布局效果。你可以根据自己的实际需求来选择使用哪一个。

三.2、 使用 SliverFillViewport 创建全屏的可滚动区域

SliverFillViewport 是 Flutter 中一个重要的 Widget,它的作用是使其子元素填充视口(也就是屏幕可见的部分)。

这个 Widget 最常见的用途是在 PageView 或者 CustomScrollView 里使用,用来创建用户可以左右滑动查看的各种 "pages" 或者 "cards"。

SliverFillViewport 的主要特点是它可以让其子元素以特定的方式来填充滚动视图。举例来说,如果你希望在用户滚动视图时,每个元素都可以占据滚动视图的整个视口,那么 SliverFillViewport 就是一个很好的选择。

简单的例子的例子
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SliverFillViewport 示例',
      home: Scaffold(
        appBar: AppBar(title: Text('SliverFillViewport 示例')),
        body: Container(
          width: 300,
          height: 400,
          color: Colors.grey,
          child: CustomScrollView(
            slivers: <Widget>[
              SliverFillViewport(
                viewportFraction: 0.2, // 子组件高度占满视口的比例
                delegate: SliverChildListDelegate(
                  [
                    Container(color: Colors.blue),
                    Container(color: Colors.green),
                    Container(color: Colors.yellow),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

在这个示例中,我们将 viewportFraction 属性设置为 0.2,表示子组件的高度占视口高度的 20%。

运行该代码,你将看到在 300x400 的容器内,子组件的高度只占据了视口高度的 20%,而其余空间则留白。

这个示例演示了将 viewportFraction 属性设置为不同值时,子组件占用视口高度的比例发生变化的效果。

image.png


再来一个例子
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

// 定义旅游景点类
class TouristAttraction {
  final String name; // 名称
  final String description; // 描述
  final Color color; // 背景色

  TouristAttraction({required this.name, required this.description, required this.color});
}

class MyApp extends StatelessWidget {
  // 旅游景点列表
  final List<TouristAttraction> attractions = [
    TouristAttraction(
      name: '埃菲尔铁塔',
      description:
      '埃菲尔铁塔是法国巴黎的一座铁塔,位于马斯菲尔德公园(Champ de Mars)内,是巴黎地标之一。铁塔的设计者是古斯塔夫·埃菲尔,铁塔的建造是为了纪念法国大革命一百周年。',
      color: Colors.lightBlue, // 设置该景点的颜色
    ),
    TouristAttraction(
      name: '自由女神像',
      description:
      '自由女神像是位于美国纽约港的一座巨型铜像,是纽约市的象征之一,也是美国和法国友谊的象征。这座雕像是由法国雕塑家弗雷德里克·奥古斯特·巴托尔迪设计并制作,1886年10月28日揭幕。',
      color: Colors.pink, // 设置该景点的颜色
    ),
    TouristAttraction(
      name: '泰姬陵',
      description:
      '泰姬陵是位于印度北部城市阿格拉的一座白色大理石陵墓,于17世纪由莫卧儿帝国皇帝沙贾汗为其逝去的爱妃慕塔芝·马哈尔而建造。泰姬陵被认为是世界上最美的建筑之一。',
      color: Colors.orange, // 设置该景点的颜色
    ),
    TouristAttraction(
      name: '长城',
      description:
      '长城是一道蜿蜒于中国北部的防御工事,由石头、砖头、土坯等材料砌成,是中国古代的一项伟大工程。长城的修建始于公元前7世纪,历经2000多年的修建和扩建,成为了世界上最长的城墙。',
      color: Colors.yellow, // 设置该景点的颜色
    ),
    TouristAttraction(
      name: '比萨斜塔',
      description:
      '比萨斜塔是意大利比萨市的一座独立的钟楼,以其明显的倾斜而闻名于世。斜塔的建造始于12世纪,由于斜塔的基础建设不够坚固,导致了斜塔的倾斜。',
      color: Colors.green, // 设置该景点的颜色
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SliverFillViewport 演示',
      home: Scaffold(
        body: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              title: Text('旅游景点'),
              floating: true,
            ),
            SliverFillViewport(
              delegate: SliverChildBuilderDelegate(
                // 构建子元素
                    (BuildContext context, int index) {
                  final attraction = attractions[index];
                  return Container(
                    padding: EdgeInsets.all(16.0),
                    color: attraction.color, // 设置背景色
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          attraction.name,
                          style: TextStyle(
                            fontSize: 24,
                            fontWeight: FontWeight.bold,
                            color: Colors.white,
                          ),
                        ),
                        SizedBox(height: 8),
                        Text(
                          attraction.description,
                          style: TextStyle(
                            fontSize: 16,
                            color: Colors.white,
                          ),
                        ),
                      ],
                    ),
                  );
                },
                childCount: attractions.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

iShot_2023-07-11_21.38.47.gif

在这个例子中,我们创建了一个 TouristAttraction 类,用于表示旅游景点的名称和描述。我们将多个景点添加到 attractions 列表中,并在 CustomScrollView 中使用 SliverFillViewport 来展示它们的介绍。在 SliverFillViewport 中,我们使用 SliverChildBuilderDelegate 来构建子元素,并通过 childCount 属性指定子元素的数量。在 SliverChildBuilderDelegate 的回调函数中,我们遍历 attractions 列表,并根据每个景点的名称和描述创建一个子元素。由于 SliverFillViewport 会铺满整个 Viewport,因此所有的子元素都会占用整个屏幕。最后,我们将 CustomScrollView 放到 Scaffold 的 body 中,并在 SliverAppBar 中设置标题和浮动属性。

三.3、 使用 SliverOverlapInjector 实现重叠效果

SliverOverlapInjector 是一个特殊的 Sliver Widget,用于在两个 Sliver 之间插入一个非滚动的 Widget,这个 Widget 可以覆盖在上一个 Sliver 的底部,同时也可以被下一个 Sliver 的内容覆盖。这个 Widget 主要用于解决两个 Sliver 之间的重叠问题。

最常见的使用场景是在 Flutter 的自定义滚动视图(CustomScrollView)中,通常在具有弹性头部(SliverAppBar)的列表上方添加额外的内容,这些内容将在列表滚动时滑入视图并在列表内容滚动到顶部时停止。

以下是一个简单的示例,用于演示如何使用 SliverOverlapInjector,我已添加详细的中文注释以帮助理解:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SliverOverlapInjector 示例',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: "SliverOverlapInjector 示例"),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey<NestedScrollViewState> _key = GlobalKey<NestedScrollViewState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        key: _key,
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return <Widget>[
            SliverAppBar(
              title: Text(widget.title),
              expandedHeight: 200.0,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                background: Image.network('https://picsum.photos/400/200', fit: BoxFit.cover),
              ),
            ),
          ];
        },
        body: Builder(
          builder: (BuildContext context) {
            return CustomScrollView(
              slivers: <Widget>[
                SliverOverlapAbsorber(
                  handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                  sliver: SliverList(
                    delegate: SliverChildBuilderDelegate(
                          (BuildContext context, int index) {
                        return ListTile(
                          title: Text('列表项 #$index'),
                        );
                      },
                      childCount: 50,
                    ),
                  ),
                ),
                SliverOverlapInjector(
                  handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

iShot_2023-07-12_10.38.42.gif

在这个示例中,SliverAppBar 提供了一个可以伸展的 AppBar,SliverOverlapAbsorber 吸收了其中的重叠部分,并通过一个 NestedScrollView.sliverOverlapAbsorberHandleFor 共享给 SliverOverlapInjector。这样,当我们在 CustomScrollView 中滚动时,SliverAppBar 下方的列表会根据滚动的情况逐渐滑入视图,形成重叠效果。

总结

在 Flutter 中,`Sliver以上是关于使用 Sliver 构建可滚动区域的基础介绍和示例代码,希望能够帮助你更好地理解和使用 Sliver 相关的 Widget。使用 Sliver 可以方便地构建出各种复杂的可滚动效果,同时也可以通过 SliverOverlapInjector 实现重叠效果,为用户提供更加丰富的交互体验。如果你有任何问题或疑问,欢迎继续提问。




可滚动布局模型和 Sliver布局模型 是两个东西吗

可滚动布局模型Sliver布局模型都是Flutter中用于实现可滚动性布局的布局模型,但它们的实现方式和使用方法略有不同。

可滚动布局模型(例如ListView、GridView、SingleChildScrollView等)是基于Scrollable类实现的,它们使用Viewport来显示子组件,并支持滚动、滑动和惯性等手势操作。这些组件通常使用较少的代码来实现常见的可滚动性布局模式。

Sliver布局模型则更为灵活,它的核心是使用Sliver来构建可滚动的子组件,可以自由组合和嵌套多种滚动组件以实现复杂的滚动效果。Sliver布局模型通常需要更多的布局代码和布局知识,但可以实现更高级和自定义的滚动效果。

可以将Sliver布局模型看作是可滚动布局模型的扩展,它提供了更多的自定义选项和更高级的滚动效果,例如可扩展的应用栏、流畅的滚动列表、复杂的网格布局等等。

总之,可滚动布局模型和Sliver布局模型都是Flutter中用于实现可滚动性布局的布局模型,开发者可以根据需要选择合适的布局模型来实现所需的滚动效果。