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

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

视频

https://youtu.be/_h7CkzXY3aM

https://www.bilibili.com/video/BV1Gh4y1571p/

前言

上一节讲了 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});

  @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

  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要让 `ListView` 滚动到指定的位置,可以使用 `ScrollController`,将其传递给 `ListView` 的 `controller` 参数。然后,调用 `animateTo` 方法,传递要滚动到的位置。以下是一个简单的示例代码: ```dart class MyPage extends StatefulWidget { @override _MyPageState createState() => _MyPageState(); } class _MyPageState extends State<MyPage> { final List<String> items = List.generate(100, (index) => "Item $index"); final ScrollController _scrollController = ScrollController(); void _scrollToIndex(int index) { _scrollController.animateTo( index * 50.0, // 50.0 为每个 item 的高度 duration: Duration(milliseconds: 500), curve: Curves.easeInOut, ); } @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, _) { return [ SliverAppBar( title: Text("My Page"), pinned: true, expandedHeight: 200.0, flexibleSpace: FlexibleSpaceBar( background: Image.network( "https://picsum.photos/200/300", fit: BoxFit.cover, ), ), bottom: TabBar( tabs: [ Tab(text: "Tab 1"), Tab(text: "Tab 2"), Tab(text: "Tab 3"), ], ), ), ]; }, body: TabBarView( children: [ ListView.builder( controller: _scrollController, itemCount: items.length, itemBuilder: (context, index) { return ListTile( title: Text(items[index]), ); }, ), Container(color: Colors.green), Container(color: Colors.blue), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => _scrollToIndex(20), // 滚动到第 20 个 item child: Icon(Icons.arrow_downward), ), ); } } ``` 在这个示例中,我们创建了一个 `MyPage` widget,使用了 `NestedScrollView` 和 `TabBarView`,其中包含了一个 `ListView`。我们创建了一个 `_scrollController`,并将其传递给了 `ListView` 的 `controller` 参数。然后,我们定义了一个 `_scrollToIndex` 方法,用于将 `ListView` 滚动到指定位置,这里以第 20 个 item 为例。最后,我们在页面中添加了一个悬浮按钮,点击它会调用 `_scrollToIndex` 方法,将 `ListView` 滚动到指定位置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值