likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 2

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

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

视频

youtu.be/_h7CkzXY3aM

www.bilibili.com/video/BV1Gh…

前言

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

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

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

Flutter 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 可以让多个滚动视图联动滚动,从而实现一些复杂的交互效果。

常见的业务场景:

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

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

NestedScrollViewCustomScrollView 都是支持自定义滚动视图的 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,
          ),
        ),
      );
}

代码

github.com/ducafecat/f…

小结

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

感谢阅读本文

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


© 猫哥 ducafecat.com

end