猫哥课堂 ducafecat.com

Flutter NestedScrollView 内嵌视图滚动行为一致

视频

https://youtu.be/_h7CkzXY3aM

前言

上一节讲了 CustomScrollView ,可以发现有的地方滚动并不是很连贯。

这时候就需要 NestedScrollView 来处理了。

今天会写一个如下图的例子来实现滚动一致。

image-20230725225019859

原文 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,它可以嵌套多个滚动视图,例如 ListViewGridViewSliverAppBar 等。NestedScrollView 可以让多个滚动视图联动滚动,从而实现一些复杂的交互效果。

常见的业务场景:

  • 一个页面上有多个可滚动的区域,而且这些区域之间的滚动是相互独立的,但是它们的滚动行为需要协调一致,比如一个列表和一个悬浮的顶部栏。
  • 实现类似于网易云音乐个人主页的效果,即在滚动过程中,一个悬浮的头部会被逐渐放大,同时顶部的导航栏会渐变消失,直到最后整个头部完全占据整个屏幕。
  • 在列表中嵌套一个可滚动的子列表,例如在一个电商应用中,展示一个大分类下的多个小分类,每个小分类下面又有多个商品。

NestedScrollViewCustomScrollView 都是支持自定义滚动视图的 Widget。它们的区别在于,CustomScrollView 可以通过添加多个 Sliver 来实现复杂的滚动视图效果,而 NestedScrollView 则是将多个滚动视图嵌套在一起,并提供了一些方便的接口来协调它们之间的滚动。因此,NestedScrollView 的使用场景更加适合于多个可滚动区域之间需要协调滚动的情况。

步骤

NestedScrollView 分为头部和内容两个部分,我们分别来实现。

第一步:实现 NestedScrollView 头部

lib/nested.dart

编写头部组件函数,创建页面 NestedScrollPage

class NestedScrollPage extends StatefulWidget { const NestedScrollPage({super.key}); State<NestedScrollPage> createState() => _NestedScrollPageState(); } class _NestedScrollPageState extends State<NestedScrollPage> { final List<String> _tabs = const ['tab1', 'tab2', "tab3", "tab4"];

准备 _tabs 数据

build 函数

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}); Widget build(BuildContext context) { return child; } 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. 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}); Widget build(BuildContext context) { return child; } 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}); State<NestedScrollPage> createState() => _NestedScrollPageState(); } class _NestedScrollPageState extends State<NestedScrollPage> { final List<String> _tabs = const ['tab1', 'tab2', "tab3", "tab4"]; 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, ), ), ); }

代码

https://github.com/ducafecat/flutter_develop_tips/blob/main/flutter_application_sliver_scroll/lib/nested.dart

小结

使用 NestedScrollView 是一个非常强大和灵活的 widget,可以实现许多常见的滚动视图布局,例如带有悬浮标题的列表视图,或者带有可展开/折叠部分的折叠面板。

感谢阅读本文

如果我有什么错?请在评论中让我知道。我很乐意改进。


© 猫哥 ducafecat.com

end


Copyright 2023 ducafecat. All rights reserved.
微信: ducafecat, line: ducafecat,京ICP备2021009050号-3