一. 什么是 NestedScrollView?
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中,可以通过配置
title
、background
、flexibleSpace
等属性来定义Header的内容和样式。可以根据需要进行调整,例如添加标题、背景图像、导航按钮等。 -
在
body
属性中,可以配置Sliver列表的内容。- 可以根据需要使用不同的Sliver组件,如
SliverList
、SliverGrid
等,来实现所需的布局。
- 可以根据需要使用不同的Sliver组件,如
这些是使用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(),
),
);
}
在这份代码中,我们使用NestedScrollView
来创建一个带有可滚动标签栏的页面,页面由三个标签页组成,分别是一个列表、一个网格和一个居中的文本。
在NestedScrollView
中,我们使用headerSliverBuilder
属性来构建顶部的固定导航栏,该导航栏包括一个可滚动的标签栏TabBar
。TabBar
中有三个标签,分别对应着三个子组件。
在TabBarView
中,我们将三个子组件放置在一起,这些子组件分别是一个ListView
、一个GridView
和一个居中的文本。每个子组件都是可滚动的,且它们可以根据用户选择的标签进行切换。
最后,我们在initState
中初始化了一个TabController
,并在dispose
中释放了它。同时,我们将TabController
传递给了TabBar
和TabBarView
,以便它们可以正确地管理标签栏。
三.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(),
),
);
}
三.3、 NestedScrollView结合SliverList、SliverGrid的使用
SliverList
和SliverGrid
是可以在很多地方使用的,只要是实现了Sliver
接口的滚动容器都可以使用它们。
SliverList
和SliverGrid
是Flutter中的滚动子组件,它们的作用是创建可滚动的列表或网格布局
NestedScrollView SliverList
和SliverGrid
,以及ListView
和GridView
NestedScrollView
最常见的用例是使用SliverAppBar
作为顶部的应用栏,并在其下方嵌套一个可滚动的SliverList
或SliverGrid
作为内容视图。这是因为SliverList
和SliverGrid
是实现了Sliver
接口的滚动子组件,可以很好地与SliverAppBar
配合使用,实现固定应用栏和可滚动内容的效果。在这种情况下,NestedScrollView
一般都是使用SliverList
和SliverGrid
。
不过,NestedScrollView
也可以使用其他的滚动子组件,包括ListView
和GridView
,只要使用SliverChildListDelegate
或SliverChildBuilderDelegate
将它们包装为Sliver
子组件即可。但是需要注意,与SliverList
和SliverGrid
不同,ListView
和GridView
不是实现了Sliver
接口的滚动子组件,它们不支持直接与SliverAppBar
配合使用,因此在使用时需要特别注意。
(ps:ListView
和GridView
不是实现了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
作为SliverAppBar
的flexibleSpace
属性,其中的background
是一个Image.asset
组件,用于设置背景图像。
在body
中,我们使用CustomScrollView
作为嵌套滚动视图的容器,并在其中创建了SliverList
和SliverGrid
。SliverList
用于创建一个滚动的列表,而SliverGrid
用于创建一个滚动的网格布局。你可以根据需求调整SliverGridDelegateWithFixedCrossAxisCount
的参数来设置列数、间距和子项的宽高比。
通过这样的配置,你可以在NestedScrollView中同时使用SliverList和SliverGrid,创建出复杂的滚动布局。
四、NestedScrollView的自定义
-
自定义
SliverAppBar
:SliverAppBar
是NestedScrollView
中最常用的应用栏,可以通过SliverAppBar
的属性来进行自定义,如title
、actions
、flexibleSpace
等。此外,还可以通过SliverAppBar
的回调函数来监听应用栏的展开和折叠状态,并根据状态来进行其他的自定义操作。 -
自定义
Sliver
子组件:NestedScrollView
中的Sliver
子组件包括SliverList
、SliverGrid
等,这些子组件也可以进行自定义,如更改子项的样式、设置子项的交互效果等。 -
自定义
ScrollController
:ScrollController
可以用于控制NestedScrollView
的滚动位置和状态,也可以用于监听滚动事件,根据事件来进行其他的自定义操作,例如显示或隐藏某些元素、改变元素的样式等。 -
自定义
TabBar
:NestedScrollView
自带的TabBar
可以用于在多个子页面之间切换,但是其样式和交互效果可能无法满足设计需求,此时可以通过自定义TabBar
的方式来进行改进。 -
自定义
RefreshIndicator
:RefreshIndicator
可以用于下拉刷新,可以通过自定义RefreshIndicator
的样式和交互效果来改进用户体验。
代码演示
SliverAppBar
、SliverList
、TabBar
和RefreshIndicator
的自定义
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 的描述信息',
);
},
),
],
),
),
),
),
);
}
}
这份代码中,我们使用了SliverAppBar
、SliverList
、TabBar
和RefreshIndicator
等进行了自定义,其中:
- 自定义了顶部标签栏
CustomTabBar
,包括标签的颜色、大小、样式等; - 自定义了列表项
CustomListItem
,包括标题、副标题、图标等; - 自定义了下拉刷新指示器
CustomRefreshIndicator
,包括指示器的颜色、大小、样式等; - 监听了
ScrollController
的滚动事件,根据滚动位置来控制FAB按钮的显示和隐藏; - 使用
Future.delayed
模拟了异步获取数据的过程,实现了下拉刷新效果。
五. NestedScrollView的最佳实践
- 在使用
NestedScrollView
时,应当尽可能使用Sliver
子组件,如SliverList
和SliverGrid
,以获得更好的性能和体验。 - 如果需要在
NestedScrollView
中嵌套ListView
或GridView
等非Sliver
子组件,可以使用SliverChildListDelegate
或SliverChildBuilderDelegate
将它们包装为Sliver
子组件,但需要注意与SliverAppBar
的配合,以避免出现滚动冲突等问题。 - 在使用
NestedScrollView
时,应当尽可能使用AutomaticKeepAlive
来保持Sliver
子组件的状态,以避免在滚动时出现重绘等性能问题。 - 如果需要在
NestedScrollView
中使用TabBar
,可以考虑使用NestedScrollView
自带的TabBar
属性,以避免手动同步TabBar
和ScrollView
的滚动状态。 - 在使用
NestedScrollView
时,应当尽可能减少不必要的嵌套和滚动嵌套,以获得更好的性能和体验。 - 如果需要在
NestedScrollView
中嵌套RefreshIndicator
,应当将RefreshIndicator
放在NestedScrollView
的最外层,以避免出现滚动冲突等问题。 - 在使用
NestedScrollView
时,应当尽可能使用ScrollController
来控制滚动位置和状态,以便在需要时进行手动控制和优化。