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

常见的业务场景:

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

NestedScrollView 和 CustomScrollView 都是支持自定义滚动视图的 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

相关推荐
yuanlaile4 小时前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭14 小时前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白15 小时前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
陆断枫1 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
程序员老刘1 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185822 天前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙
恋猫de小郭2 天前
Flutter + EmbeddingGemma 2,谷歌发布完全端侧的 AI Edge Foresight
android·前端·flutter
君赏2 天前
基于 FlutterPatch 快速接入 Flutter 热更新
flutter
律宏阔2 天前
Dart FFI 内存管理:用 using + Arena 替代嵌套 try-finally
前端·flutter
律宏阔2 天前
Flutter 调用 Go:从 c-shared + ffigen 到 @Native + Native Assets 踩坑记录
前端·flutter