Flutter速来系列23-5、NestedScrollView,伸缩标题,伸缩背景,还有哪些自定义

667 阅读8分钟

一. 什么是 NestedScrollView?

iShot_2023-07-11_17.04.06.gif

NestedScrollView是Flutter中的一个组件,它允许在一个滚动视图内部嵌套其他滚动视图

NestedScrollView是一个可以嵌套多个可滚动视图的Flutter组件。

它允许我们在同一个页面上显示多个滚动视图,例如ListView、GridView、CustomScrollView等,使得我们可以在同一个页面上显示多种类型的内容。

NestedScrollView通常与SliverAppBar一起使用,可以创建一个带有可滚动标签栏的页面,或者创建一个带有可折叠顶部应用栏的页面。

换句话说,它提供了一个灵活的布局方式,可以同时支持垂直和水平方向的滚动,并且可以根据需要定制滚动效果。NestedScrollView的核心是Sliver Widget,它们以可定制的方式协同工作,构建出复杂的滚动布局。

二. NestedScrollView的基本结构

在理解NestedScrollView之前,我们需要熟悉一些基本概念。

NestedScrollView的整体结构可以被视为一个Widget树,其中包含了一个Header和一个Sliver列表。

  • Header: 部分通常包含了一些静态内容,如标题、背景图像等,

  • Sliver: Sliver则是滚动的部分,通常可以是一个SliverGrid或SliverList等。

通过调整SliverAppBar的配置,我们可以实现不同的滚动效果,例如折叠式标题栏可伸缩的背景图,SliverList、SliverGrid等。使用SliverList时,可以添加视差效果,使列表项在滚动时产生动画效果。

除了默认的Sliver组件外,我们还可以自定义Sliver列表,根据需求创建更加丰富多样的布局。

使用NestedScrollView的基本步骤 ✨

NestedScrollView(
  headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
    return <Widget>[
      // 在这里配置Header部分的SliverAppBar或其他Sliver组件
      SliverAppBar(
        title: Text('NestedScrollView Example'),
        // 更多配置项...
      ),
    ];
  },
  body: ListView.builder(
    // 配置Sliver列表的内容
    itemBuilder: (BuildContext context, int index) {
      return ListTile(
        title: Text('Item $index'),
      );
    },
    itemCount: 20,
  ),
);
  • headerSliverBuilder回调函数中,可以配置Header部分的SliverAppBar或其他Sliver组件

  • 在SliverAppBar中,可以通过配置titlebackgroundflexibleSpace等属性来定义Header的内容和样式。可以根据需要进行调整,例如添加标题、背景图像、导航按钮等。

  • body属性中,可以配置Sliver列表的内容。

    • 可以根据需要使用不同的Sliver组件,如SliverListSliverGrid等,来实现所需的布局。

这些是使用NestedScrollView的基本步骤。


三. 例子

三.1、简单的 折叠式标题

import 'package:flutter/material.dart';

class MyNestedScrollView extends StatefulWidget {
  @override
  _MyNestedScrollViewState createState() => _MyNestedScrollViewState();
}

class _MyNestedScrollViewState extends State<MyNestedScrollView> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    // 初始化 TabController
    _tabController = TabController(vsync: this, length: 3);
  }

  @override
  void dispose() {
    // 释放 TabController
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return <Widget>[
            // 顶部固定导航栏
            SliverAppBar(
              title: Text('NestedScrollView 示例'),
              pinned: true, // 固定导航栏
              floating: true, // 当下拉时,导航栏是否应该动画显示出来
              forceElevated: innerBoxIsScrolled, // 当滚动内容滚动时是否显示导航栏阴影
              bottom: TabBar(
                controller: _tabController, // 将 TabController 传递给 TabBar
                tabs: <Widget>[
                  Tab(text: '标签 1'),
                  Tab(text: '标签 2'),
                  Tab(text: '标签 3'),
                ],
              ),
            ),
          ];
        },
        body: TabBarView(
          controller: _tabController, // 将 TabController 传递给 TabBarView
          children: <Widget>[
            // 列表
            ListView.builder(
              itemBuilder: (BuildContext context, int index) {
                return ListTile(title: Text('第 $index 个列表项'));
              },
              itemCount: 50,
            ),
            // 网格
            GridView.builder(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                childAspectRatio: 1.0,
                mainAxisSpacing: 10.0,
                crossAxisSpacing: 10.0,
              ),
              itemBuilder: (BuildContext context, int index) {
                return Container(
                  color: Colors.grey[300],
                  child: Center(child: Text('第 $index 个网格项')),
                );
              },
              itemCount: 20,
            ),
            // 居中的文本
            Center(
              child: Text('第三个标签页'),
            ),
          ],
        ),
      ),
    );
  }
}

void main() {
  runApp(
    MaterialApp(
      home: MyNestedScrollView(),
    ),
  );
}

iShot_2023-07-11_16.35.39.gif

在这份代码中,我们使用NestedScrollView来创建一个带有可滚动标签栏的页面,页面由三个标签页组成,分别是一个列表、一个网格和一个居中的文本。

NestedScrollView中,我们使用headerSliverBuilder属性来构建顶部的固定导航栏,该导航栏包括一个可滚动的标签栏TabBarTabBar中有三个标签,分别对应着三个子组件。

TabBarView中,我们将三个子组件放置在一起,这些子组件分别是一个ListView、一个GridView和一个居中的文本。每个子组件都是可滚动的,且它们可以根据用户选择的标签进行切换。

最后,我们在initState中初始化了一个TabController,并在dispose中释放了它。同时,我们将TabController传递给了TabBarTabBarView,以便它们可以正确地管理标签栏。

三.2、可伸缩的背景图像

import 'package:flutter/material.dart';

class MyNestedScrollView extends StatefulWidget {
  @override
  _MyNestedScrollViewState createState() => _MyNestedScrollViewState();
}

class _MyNestedScrollViewState extends State<MyNestedScrollView> {
  // 创建一个ScrollController对象,用于监听滚动事件
  final _scrollController = ScrollController();

  @override
  void dispose() {
    // 在组件销毁时释放ScrollController对象
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 使用NestedScrollView作为body
      body: NestedScrollView(
        // 设置滚动监听
        controller: _scrollController,
        // 设置顶部固定导航栏
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return <Widget>[
            SliverAppBar(
              title: Text('可伸缩的背景图'),
              centerTitle: true,
              expandedHeight: 200.0,
              pinned: true,
              floating: true,
              snap: true,
              flexibleSpace: FlexibleSpaceBar(
                background: Image.network(
                  'https://images.pexels.com/photos/1001990/pexels-photo-1001990.jpeg?auto=compress&cs=tinysrgb&w=800',
                  fit: BoxFit.cover,
                ),
              ),
            ),
          ];
        },
        // 设置可滚动子组件
        body: ListView.builder(
          itemCount: 100,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              title: Text('列表项 $index'),
            );
          },
        ),
      ),
    );
  }
}

void main() {
  runApp(
    MaterialApp(
      home: MyNestedScrollView(),
    ),
  );
}

iShot_2023-07-11_17.04.06.gif

三.3、 NestedScrollView结合SliverList、SliverGrid的使用

SliverListSliverGrid是可以在很多地方使用的,只要是实现了Sliver接口的滚动容器都可以使用它们。

SliverListSliverGrid是Flutter中的滚动子组件,它们的作用是创建可滚动的列表或网格布局


NestedScrollView SliverListSliverGrid,以及ListViewGridView

NestedScrollView最常见的用例是使用SliverAppBar作为顶部的应用栏,并在其下方嵌套一个可滚动的SliverListSliverGrid作为内容视图。这是因为SliverListSliverGrid是实现了Sliver接口的滚动子组件,可以很好地与SliverAppBar配合使用,实现固定应用栏和可滚动内容的效果。在这种情况下,NestedScrollView一般都是使用SliverListSliverGrid

不过,NestedScrollView也可以使用其他的滚动子组件,包括ListViewGridView,只要使用SliverChildListDelegateSliverChildBuilderDelegate将它们包装为Sliver子组件即可。但是需要注意,与SliverListSliverGrid不同,ListViewGridView不是实现了Sliver接口的滚动子组件,它们不支持直接与SliverAppBar配合使用,因此在使用时需要特别注意。

(ps:ListViewGridView不是实现了Sliver接口的滚动子组件)


代码 NestedScrollView结合SliverList、GridView

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: MyPage(),
    ),
  );
}

class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
  late ScrollController _scrollController;

  @override
  void initState() {
    _scrollController = ScrollController();
    super.initState();
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        controller: _scrollController,
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return <Widget>[
            SliverAppBar(
              expandedHeight: 300, // 设置扩展高度
              flexibleSpace: FlexibleSpaceBar(
                background: Image.network(
                  'https://images.pexels.com/photos/1001990/pexels-photo-1001990.jpeg?auto=compress&cs=tinysrgb&w=800',
                  fit: BoxFit.cover,
                ),
              ),
            ),
          ];
        },
        body: CustomScrollView(
          controller: _scrollController, // 滚动控制器
          slivers: <Widget>[
            SliverList( // 一个垂直方向的列表
              delegate: SliverChildBuilderDelegate( // 子组件构造器代理
                    (BuildContext context, int index) {
                  return ListTile(
                    title: Text('Item $index'),
                  );
                },
                childCount: 10, // 子组件个数
              ),
            ),
            SliverGrid( // 一个网格布局
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( // 指定网格布局的样式
                crossAxisCount: 2, // 设置列数为2
                mainAxisSpacing: 10.0, // 设置主轴方向的间距为10.0
                crossAxisSpacing: 10.0, // 设置交叉轴方向的间距为10.0
                childAspectRatio: 1.0, // 设置每个子项的宽高比为1:1
              ),
              delegate: SliverChildBuilderDelegate( // 子组件构造器代理
                    (BuildContext context, int index) {
                  return Container(
                    color: Colors.blue, // 设置容器的背景颜色
                    child: Center(
                      child: Text('Grid Item $index'), // 显示每个子项的序号
                    ),
                  );
                },
                childCount: 6, // 子组件个数
              ),
            ),
          ],
        ),
      ),
    );
  }
}

在上述示例中,我们在headerSliverBuilder回调函数中创建了一个带有扩展高度的SliverAppBar。我们使用FlexibleSpaceBar作为SliverAppBarflexibleSpace属性,其中的background是一个Image.asset组件,用于设置背景图像。

body中,我们使用CustomScrollView作为嵌套滚动视图的容器,并在其中创建了SliverListSliverGridSliverList用于创建一个滚动的列表,而SliverGrid用于创建一个滚动的网格布局。你可以根据需求调整SliverGridDelegateWithFixedCrossAxisCount的参数来设置列数、间距和子项的宽高比。

通过这样的配置,你可以在NestedScrollView中同时使用SliverList和SliverGrid,创建出复杂的滚动布局。

四、NestedScrollView的自定义

  • 自定义SliverAppBarSliverAppBarNestedScrollView中最常用的应用栏,可以通过SliverAppBar的属性来进行自定义,如titleactionsflexibleSpace等。此外,还可以通过SliverAppBar的回调函数来监听应用栏的展开和折叠状态,并根据状态来进行其他的自定义操作。

  • 自定义Sliver子组件:NestedScrollView中的Sliver子组件包括SliverListSliverGrid等,这些子组件也可以进行自定义,如更改子项的样式、设置子项的交互效果等。

  • 自定义ScrollControllerScrollController可以用于控制NestedScrollView的滚动位置和状态,也可以用于监听滚动事件,根据事件来进行其他的自定义操作,例如显示或隐藏某些元素、改变元素的样式等。

  • 自定义TabBarNestedScrollView自带的TabBar可以用于在多个子页面之间切换,但是其样式和交互效果可能无法满足设计需求,此时可以通过自定义TabBar的方式来进行改进。

  • 自定义RefreshIndicatorRefreshIndicator可以用于下拉刷新,可以通过自定义RefreshIndicator的样式和交互效果来改进用户体验。

代码演示

SliverAppBarSliverListTabBarRefreshIndicator的自定义

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

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

// 自定义顶部标签栏
class CustomTabBar extends StatelessWidget implements PreferredSizeWidget {
  final TabController controller;

  CustomTabBar({Key? key, required this.controller}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TabBar(
      controller: controller,
      labelColor: Colors.white,
      // 选中标签的颜色
      unselectedLabelColor: Colors.grey,
      // 未选中标签的颜色
      indicatorSize: TabBarIndicatorSize.label,
      // 指示器的大小(和标签等宽)
      tabs: [
        Tab(text: '推荐'),
        Tab(text: '热门'),
        Tab(text: '关注'),
      ],
    );
  }

  @override
  Size get preferredSize => Size.fromHeight(kToolbarHeight); // 标签栏的高度
}

// 自定义列表项
class CustomListItem extends StatelessWidget {
  final String title;
  final String subtitle;

  CustomListItem({Key? key, required this.title, required this.subtitle})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        title: Text(title),
        subtitle: Text(subtitle),
        leading: Icon(Icons.ac_unit),
      ),
    );
  }
}

// 自定义下拉刷新指示器
class CustomRefreshIndicator extends StatelessWidget {
  final RefreshCallback onRefresh;
  final Widget child;

  CustomRefreshIndicator(
      {Key? key, required this.onRefresh, required this.child})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: onRefresh,
      child: child,
      color: Colors.blue,
      // 指示器的颜色
      strokeWidth: 2.0,
      // 指示器的宽度
      backgroundColor: Colors.white, // 背景颜色
    );
  }
}

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

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  late ScrollController _scrollController;
  bool _showFab = true; // 是否显示FAB按钮

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    _scrollController = ScrollController();

    // 监听滚动事件,根据滚动位置来控制FAB按钮的显示和隐藏
    _scrollController.addListener(() {
      if (_scrollController.position.userScrollDirection ==
          ScrollDirection.reverse) {
        if (_showFab) {
          setState(() {
            _showFab = false;
          });
        }
      } else {
        if (!_showFab) {
          setState(() {
            _showFab = true;
          });
        }
      }
    });
  }

  @override
  void dispose() {
    _tabController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  // 模拟异步获取数据
  Future<void> _onRefresh() async {
    await Future.delayed(Duration(seconds: 2));
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text('数据已更新'),
    ));
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        floatingActionButton: _showFab
            ? FloatingActionButton(
                onPressed: () {},
                child: Icon(Icons.add),
              )
            : null,
        body: NestedScrollView(
          controller: _scrollController,
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              // 自定义应用栏
              SliverAppBar(
                expandedHeight: 200.0, // 展开高度
                pinned: true, // 是否固定应用栏
                flexibleSpace: FlexibleSpaceBar(

                  background: Image.network(
                    'https://picsum.photos/400/200',
                    fit: BoxFit.cover,
                  ), // 背景图片
                ),
                bottom: PreferredSize(
                  preferredSize: Size.fromHeight(48.0), // 指定TabBar的高度
                  child: CustomTabBar(controller: _tabController), // 自定义标签栏
                ),
              ),
            ];
          },
          // 自定义列表
          body: CustomRefreshIndicator(
            onRefresh: _onRefresh,
            child: TabBarView(
              controller: _tabController,
              children: [
                ListView.builder(
                  itemCount: 20,
                  itemBuilder: (BuildContext context, int index) {
                    return CustomListItem(
                      title: '推荐 $index',
                      subtitle: '这是推荐 $index 的描述信息',
                    );
                  },
                ),
                ListView.builder(
                  itemCount: 20,
                  itemBuilder: (BuildContext context, int index) {
                    return CustomListItem(
                      title: '热门 $index',
                      subtitle: '这是热门 $index 的描述信息',
                    );
                  },
                ),
                ListView.builder(
                  itemCount: 20,
                  itemBuilder: (BuildContext context, int index) {
                    return CustomListItem(
                      title: '关注 $index',
                      subtitle: '这是关注 $index 的描述信息',
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

image.png

这份代码中,我们使用了SliverAppBarSliverListTabBarRefreshIndicator等进行了自定义,其中:

  • 自定义了顶部标签栏CustomTabBar,包括标签的颜色、大小、样式等;
  • 自定义了列表项CustomListItem,包括标题、副标题、图标等;
  • 自定义了下拉刷新指示器CustomRefreshIndicator,包括指示器的颜色、大小、样式等;
  • 监听了ScrollController的滚动事件,根据滚动位置来控制FAB按钮的显示和隐藏;
  • 使用Future.delayed模拟了异步获取数据的过程,实现了下拉刷新效果。

五. NestedScrollView的最佳实践

  1. 在使用NestedScrollView时,应当尽可能使用Sliver子组件,如SliverListSliverGrid,以获得更好的性能和体验。
  2. 如果需要在NestedScrollView中嵌套ListViewGridView等非Sliver子组件,可以使用SliverChildListDelegateSliverChildBuilderDelegate将它们包装为Sliver子组件,但需要注意与SliverAppBar的配合,以避免出现滚动冲突等问题。
  3. 在使用NestedScrollView时,应当尽可能使用AutomaticKeepAlive来保持Sliver子组件的状态,以避免在滚动时出现重绘等性能问题。
  4. 如果需要在NestedScrollView中使用TabBar,可以考虑使用NestedScrollView自带的TabBar属性,以避免手动同步TabBarScrollView的滚动状态。
  5. 在使用NestedScrollView时,应当尽可能减少不必要的嵌套和滚动嵌套,以获得更好的性能和体验。
  6. 如果需要在NestedScrollView中嵌套RefreshIndicator,应当将RefreshIndicator放在NestedScrollView的最外层,以避免出现滚动冲突等问题。
  7. 在使用NestedScrollView时,应当尽可能使用ScrollController来控制滚动位置和状态,以便在需要时进行手动控制和优化。