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-clas...

https://api.flutter.dev/flutter/widgets/SliverOverlapAbsorber...

https://api.flutter.dev/flutter/widgets/SliverOverlapInjector...

知识点 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});  @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,          ),        ),      );}

代码

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

小结

应用 NestedScrollView 是一个十分弱小和灵便的 widget,能够实现许多常见的滚动视图布局,例如带有悬浮题目的列表视图,或者带有可开展/折叠局部的折叠面板。

感激浏览本文

如果我有什么错?请在评论中让我晓得。我很乐意改良。


© 猫哥
ducafecat.com

end