Flutter NestedScrollView 内嵌视图滚动行为一致
视频
https://www.bilibili.com/video/BV1Gh4y1571p/
前言
上一节讲了 CustomScrollView ,可以发现有的地方滚动并不是很连贯。
这时候就需要 NestedScrollView 来处理了。
今天会写一个如下图的例子来实现滚动一致。
原文 https://ducafecat.com/blog/flutter-sliver-nested-scroll-view
参考
https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html
https://api.flutter.dev/flutter/widgets/SliverOverlapAbsorber-class.html
https://api.flutter.dev/flutter/widgets/SliverOverlapInjector-class.html
知识点 NestedScrollView
NestedScrollView
是 Flutter 中的一个 Widget,它可以嵌套多个滚动视图,例如 ListView
、GridView
、SliverAppBar
等。NestedScrollView
可以让多个滚动视图联动滚动,从而实现一些复杂的交互效果。
常见的业务场景:
- 一个页面上有多个可滚动的区域,而且这些区域之间的滚动是相互独立的,但是它们的滚动行为需要协调一致,比如一个列表和一个悬浮的顶部栏。
- 实现类似于网易云音乐个人主页的效果,即在滚动过程中,一个悬浮的头部会被逐渐放大,同时顶部的导航栏会渐变消失,直到最后整个头部完全占据整个屏幕。
- 在列表中嵌套一个可滚动的子列表,例如在一个电商应用中,展示一个大分类下的多个小分类,每个小分类下面又有多个商品。
NestedScrollView
和 CustomScrollView
都是支持自定义滚动视图的 Widget。它们的区别在于,CustomScrollView
可以通过添加多个 Sliver
来实现复杂的滚动视图效果,而 NestedScrollView
则是将多个滚动视图嵌套在一起,并提供了一些方便的接口来协调它们之间的滚动。因此,NestedScrollView
的使用场景更加适合于多个可滚动区域之间需要协调滚动的情况。
步骤
NestedScrollView 分为头部和内容两个部分,我们分别来实现。
第一步:实现 NestedScrollView 头部
lib/nested.dart
编写头部组件函数,创建页面 NestedScrollPage
class NestedScrollPage extends StatefulWidget {
const NestedScrollPage({super.key});
@override
State<NestedScrollPage> createState() => _NestedScrollPageState();
}
class _NestedScrollPageState extends State<NestedScrollPage> {
final List<String> _tabs = const ['tab1', 'tab2', "tab3", "tab4"];
准备 _tabs 数据
build 函数
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: _tabs.length,
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
_buildHeader(context, innerBoxIsScrolled),
];
},
body: _buildTabBarView(),
),
),
);
}
headerSliverBuilder 头部实现函数
// 头部
Widget _buildHeader(BuildContext context, bool innerBoxIsScrolled) {
return // SliverOverlapAbsorber 的作用是处理重叠滚动效果,
// 防止 CustomScrollView 中的滚动视图与其他视图重叠。
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver:
// SliverAppBar 的作用是创建可折叠的顶部应用程序栏,
// 它可以随着滚动而滑动或固定在屏幕顶部,并且可以与其他 Sliver 小部件一起使用。
SliverAppBar(
title: const Text('滚动一致性'),
pinned: true,
elevation: 6, //影深
expandedHeight: 300.0,
forceElevated: innerBoxIsScrolled, //为true时展开有阴影
flexibleSpace: FlexibleSpaceBar(
background: Image.asset(
"assets/images/banner-bg.jpg",
fit: BoxFit.cover,
),
),
// 底部固定栏
bottom: MyCustomAppBar(
child: Column(
children: [
Container(
color: Colors.greenAccent,
child: const Center(child: Text('固定高度内容')),
),
TabBar(
tabs: _tabs
.map((String name) => Tab(
text: name,
))
.toList(),
),
],
),
),
),
);
}
SliverOverlapAbsorber 与 SliverOverlapInjector,作用是防止 CustomScrollView 中的滚动视图与其他视图重叠。
编写 MyCustomAppBar 悬停 Bar
lib/app_bar.dart
import 'package:flutter/material.dart';
class MyCustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final Widget child;
const MyCustomAppBar({super.key, required this.child});
@override
Widget build(BuildContext context) {
return child;
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 20.0);
}
第二步:实现 NestedScrollView 内容
lib/nested.dart
TabBarView 混入各种情况:横向滚动、固定高度、SliverList列表
Widget _buildTabBarView() {
return TabBarView(
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>(name),
slivers: <Widget>[
// SliverOverlapInjector 的作用是处理重叠滚动效果,
// 确保 CustomScrollView 中的滚动视图不会与其他视图重叠。
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
// 横向滚动
SliverToBoxAdapter(
child: SizedBox(
height: 100,
child: PageView(
children: [
Container(
color: Colors.yellow,
child: const Center(child: Text('横向滚动')),
),
Container(color: Colors.green),
Container(color: Colors.blue),
],
),
),
),
// 固定高度内容
SliverToBoxAdapter(
child: Container(
height: 100,
color: Colors.greenAccent,
child: const Center(child: Text('固定高度内容')),
),
),
// 列表
buildContent(name),
// 固定高度内容
SliverToBoxAdapter(
child: Container(
height: 100,
color: Colors.greenAccent,
child: const Center(child: Text('固定高度内容')),
),
),
// 列表 100 行
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
childCount: 100,
),
),
],
);
},
),
);
}).toList(),
);
}
SliverOverlapInjector 的作用是处理重叠滚动效果,
确保 CustomScrollView 中的滚动视图不会与其他视图重叠。
内容列表
// 内容列表
Widget buildContent(String name) => SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('$name - $index'),
);
},
childCount: 50,
),
),
);
启动
lib/main.dart
import 'package:flutter/material.dart';
import 'nested.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
// home: const MyPageView(),
home: const NestedScrollPage(),
);
}
}
直接设置 home 进入
NestedScrollPage
界面
最后完整代码:
lib/app_bar.dart
import 'package:flutter/material.dart';
class MyCustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final Widget child;
const MyCustomAppBar({super.key, required this.child});
@override
Widget build(BuildContext context) {
return child;
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 20.0);
}
lib/nested.dart
import 'package:flutter/material.dart';
import 'app_bar.dart';
class NestedScrollPage extends StatefulWidget {
const NestedScrollPage({super.key});
@override
State<NestedScrollPage> createState() => _NestedScrollPageState();
}
class _NestedScrollPageState extends State<NestedScrollPage> {
final List<String> _tabs = const ['tab1', 'tab2', "tab3", "tab4"];
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: _tabs.length,
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
_buildHeader(context, innerBoxIsScrolled),
];
},
body: _buildTabBarView(),
),
),
);
}
// 头部
Widget _buildHeader(BuildContext context, bool innerBoxIsScrolled) {
return // SliverOverlapAbsorber 的作用是处理重叠滚动效果,
// 防止 CustomScrollView 中的滚动视图与其他视图重叠。
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver:
// SliverAppBar 的作用是创建可折叠的顶部应用程序栏,
// 它可以随着滚动而滑动或固定在屏幕顶部,并且可以与其他 Sliver 小部件一起使用。
SliverAppBar(
title: const Text('滚动一致性'),
pinned: true,
elevation: 6, //影深
expandedHeight: 300.0,
forceElevated: innerBoxIsScrolled, //为true时展开有阴影
flexibleSpace: FlexibleSpaceBar(
background: Image.asset(
"assets/images/banner-bg.jpg",
fit: BoxFit.cover,
),
),
// 底部固定栏
bottom: MyCustomAppBar(
child: Column(
children: [
Container(
color: Colors.greenAccent,
child: const Center(child: Text('固定高度内容')),
),
TabBar(
tabs: _tabs
.map((String name) => Tab(
text: name,
))
.toList(),
),
],
),
),
),
);
}
Widget _buildTabBarView() {
return TabBarView(
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>(name),
slivers: <Widget>[
// SliverOverlapInjector 的作用是处理重叠滚动效果,
// 确保 CustomScrollView 中的滚动视图不会与其他视图重叠。
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
// 横向滚动
SliverToBoxAdapter(
child: SizedBox(
height: 100,
child: PageView(
children: [
Container(
color: Colors.yellow,
child: const Center(child: Text('横向滚动')),
),
Container(color: Colors.green),
Container(color: Colors.blue),
],
),
),
),
// 固定高度内容
SliverToBoxAdapter(
child: Container(
height: 100,
color: Colors.greenAccent,
child: const Center(child: Text('固定高度内容')),
),
),
// 列表
buildContent(name),
// 固定高度内容
SliverToBoxAdapter(
child: Container(
height: 100,
color: Colors.greenAccent,
child: const Center(child: Text('固定高度内容')),
),
),
// 列表 100 行
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
childCount: 100,
),
),
],
);
},
),
);
}).toList(),
);
}
Widget buildContent(String name) => SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('$name - $index'),
);
},
childCount: 50,
),
),
);
}
代码
小结
使用 NestedScrollView
是一个非常强大和灵活的 widget,可以实现许多常见的滚动视图布局,例如带有悬浮标题的列表视图,或者带有可展开/折叠部分的折叠面板。
感谢阅读本文
如果我有什么错?请在评论中让我知道。我很乐意改进。
© 猫哥 ducafecat.com
end