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

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

视频

youtu.be/_h7CkzXY3aM

www.bilibili.com/video/BV1Gh...

前言

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

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

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

原文 ducafecat.com/blog/flutte...

参考

api.flutter.dev/flutter/wid...

api.flutter.dev/flutter/wid...

api.flutter.dev/flutter/wid...

知识点 NestedScrollView

NestedScrollView 是 Flutter 中的一个 Widget,它可以嵌套多个滚动视图,例如 ListViewGridViewSliverAppBar 等。NestedScrollView 可以让多个滚动视图联动滚动,从而实现一些复杂的交互效果。

常见的业务场景:

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

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

步骤

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

第一步:实现 NestedScrollView 头部

lib/nested.dart

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

scala 复制代码
 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 函数

less 复制代码
   @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 头部实现函数

less 复制代码
   // 头部
   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

scala 复制代码
 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列表

less 复制代码
   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 中的滚动视图不会与其他视图重叠。

内容列表

less 复制代码
   // 内容列表
   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

scala 复制代码
 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

scala 复制代码
 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

less 复制代码
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,
          ),
        ),
      );
}

代码

github.com/ducafecat/f...

小结

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

感谢阅读本文

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


© 猫哥 ducafecat.com

end

相关推荐
江上清风山间明月8 小时前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能19 小时前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人20 小时前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen20 小时前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang1 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang1 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1231 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-1 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11192 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力2 天前
Flutter应用开发:对象存储管理图片
flutter