美文网首页
Flutter-使用NestedScrollView实践 202

Flutter-使用NestedScrollView实践 202

作者: 勇往直前888 | 来源:发表于2023-06-18 14:50 被阅读0次

    需求简介

    订单列表

    如图所示,这是一个订单列表,红框中的消息,tab标题,工具栏三部分比较占空间,在上划的时候,隐藏比较好。

    简单分析

    看原来的代码,实现的思路是:

    • 红框部分的消息和Tab标题,放在了AppBarbottom部分

    • 红框部分的工具栏和TabBarView放在了body部分

    • TabBarView中放置了一个ListView

    这种写法算是主流做法,红框部分一直存在,不能隐藏。
    现在要改成红框部分也能滑动,能隐藏,该怎么修改?

    NestedScrollView

    (1) 红框部分是需要滑动的

    (2)内容部分在ListView中,本身也是能滑动的

    (3)并且还有TabBarView,需要进行内容的切换

    以上三条,用NestedScrollView来实现非常适合。

    企业微信截图_0f268e84-3451-46a0-a7cb-86f41a7de061.png
    • 红框部分放入图中的header

    • TabBarView部分放入图中的body

    • NestedScrollView的定义如下:

    class NestedScrollView extends StatefulWidget {
      const NestedScrollView({
        ...
        //通用属性已省略
        // header, sliver构造器
        required this.headerSliverBuilder,
        // 可以接受任意的滚动组件
        required this.body,
        this.dragStartBehavior = DragStartBehavior.start,
        this.floatHeaderSlivers = false,
      })
    

    header部分要求是Sliver家族的;但是body部分可以用普通的;这个有点奇怪;

    • 至于CustomScrollView,更简洁的理解是对SingleChildScrollView进行的性能优化。在实际使用中遇到过,SingleChildScrollView真的会遇到性能问题。所以,从那之后,宁可直接用ListView,也不用SingleChildScrollView。在大多数时候,直接用ListView真的可以替代SingleChildScrollView,这个组件基本上都快被遗忘了。

    • 当反馈卡顿问题的时候,第一反应就是用CustomScrollView来替代ListView或者SingleChildScrollView,在大多数的情况下,都是可以的。反正从反馈情况来看,用上了CustomScrollView之后,卡顿的问题基本上都消失了。从这个角度来说,CustomScrollView以及其对应的Sliver家族,还是有独特地位的。特别是SliverToBoxAdapter,在重构代码提升性能的时候,是真的好用。

    • 不过,这个案例,CustomScrollView就有点力不从心了。从需求看,是两部分内容的同向滑动,并不是单纯的性能问题。其实,这是个交互优化问题,并不是习惯性的性能优化问题,所以CustomScrollView就不行了,需要另外想办法。

    • NestedScrollView就是两部分的滑动,跟这个需求非常接近,并且UI掏出淘宝什么的演示,在某个搜索页面上确实实现了两个表格的滑动,感觉还挺好。希望他们也是用NestedScrollView来实现的,熟悉的CustomScrollView确实不顶用了。

    • 理论和需求搞清楚之后,就动手尝试吧。优化程序真的比写业务麻烦多了,只能不断地去尝试,别无二法。

    方案1:放入头部

    直接先上代码,由于业务代码较多,这里用了注释表达意图。

    @override
      Widget build(BuildContext context) {
        return GetBuilder<SliverOrderLogic>(
          assignId: true,
          builder: (logic) {
            return Scaffold(
              body: _buildBody(),
            );
          },
        );
      }
    
      /// 页面结构,放在一个CustomScrollView中
      Widget _buildBody() {
        return NestedScrollView(
          headerSliverBuilder: (context, innerBoxIsScrolled) {
            return [
              SliverOverlapAbsorber(
                handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                sliver: SliverAppBar(
                  backgroundColor: PandaColorConfig().colf9f9f9,
                  elevation: 0,
                  centerTitle: true,
                  iconTheme: IconThemeData(color: PandaColorConfig().col333333),
                  title: _buildTitle(),
                  actions: _buildActions(),
                  expandedHeight: logic.isShowMessage() ? 185.h : 165.h,
                  flexibleSpace: FlexibleSpaceBar(
                    background: Column(
                      children: [
                        /// 标题栏占位
                        SizedBox(
                          height: 85.5.h,
                        ),
    
                        /// 消息
                        _buildMessage(),
    
                        /// tab标题
                        _buildTabBar(),
    
                        /// 工具栏
                        _buildToolBar(),
                      ],
                    ),
                  ),
                  pinned: true,
                  floating: true,
                  snap: true,
                ),
              ),
            ];
          },
          body: _buildTabContent(),
        );
      }
    
    • Scaffold本身的头部不能使用,只用了body部分。整个body就是一个NestedScrollView,标题和内容两部分都靠NestedScrollView来完成。

    • SliverAppBar就是Sliver家族中的APP头部,这里用了其中的flexibleSpace属性。顾名思义,这个属性的作用是APP头部的展开布局。在大多数的应用中,这里是一张图片,不过这里用了这个特性,将消息,tab标题,工具栏这几部分放入一个Column中,在这里起到了下拉全部展示的效果。

    • 这里之所以要85的占位符,是应为视图层次问题。可以想象为SliverAppBar在最上层,而FlexibleSpaceBarbackground在下面一层,没有这个占位的话,那些消息,tab标题,工具栏等都会跑到SliverAppBar的下面去。

    /// 标题栏占位
        SizedBox(
              height: 85.5.h,
        ),
    
    • expandedHeight就是flexibleSpace展开时的高度。这里不能做到自适应,需要指定高度。这里只有消息有可能没有,所以提供高度还不是非常困难。其实,指定这个高度,在业务复杂的时候不好处理,这种实现方式非常勉强,不是很优雅;

    • pinned: true,这个属性就是让SliverAppBar在上滑的时候一直固定在头部

    • floating: true,snap: true,这两个属性就是让SliverAppBar在下滑的时候,展示消息、tab标题、工具栏等展开内容用的。

    • 交互上的问题:用这种方式,非常勉强地实现了上划的时候隐藏消息、tab标题、工具栏,下拉的时候又马上显示出来的特性。但是,显示的时候,会有一个动画,会有一种弹出展开列表的感觉,跟淘宝那个搜索列表的感觉是不一样的。

    • 说实话,绞尽脑汁实现的效果,就因为一点交互感觉不一样而达不到预期效果,真的很郁闷。不过没办法,只能继续想办法。

    实现方式二:header列表

      /// 页面结构,放在一个CustomScrollView中
      Widget _buildBody() {
        return NestedScrollView(
          floatHeaderSlivers: true, // 这个参数能够让头部优先滑动,类似淘宝上滑引出菜单
          headerSliverBuilder: (context, innerBoxIsScrolled) {
            return [
              _buildAppBar(),
              SliverToBoxAdapter(child: _buildMessage()),
              SliverToBoxAdapter(child: _buildTabBar()),
              SliverToBoxAdapter(child: _buildToolBar()),
            ];
          },
          body: _buildTabContent(),
        );
      }
    
      /// 头部
      Widget _buildAppBar() {
        return SliverAppBar(
          backgroundColor: PandaColorConfig().colf9f9f9,
          elevation: 0,
          centerTitle: true,
          iconTheme: IconThemeData(color: PandaColorConfig().col333333),
          title: _buildTitle(),
          actions: _buildActions(),
          pinned: true,
        );
      }
    
    • SliverAppBar只是实现一直固定的头部,不需要展开动画,只需要pinned: true,一个属性就可以了

    • 消息,tab标题,工具栏等和SliverAppBar处于同等地位,放入headerSliverBuilder的数组中

    • 由于headerSliverBuilder要求Sliver家族元素,所以用SliverToBoxAdapter包一下

    这样简单改造一下,就能得到头部和body部分的ListView同时滑动的效果。但是这里还有一点不满足要求:下滑的时候要求头部先出来,然后body部分继续滑动

    • 这里反复尝试了各种参数,最后发现floatHeaderSlivers: true,可以满足要求。
      默认是false,所以一直到不到预期效果,让人有吐血的感觉。

    • 最后出来的效果和淘宝app某个搜索页面差不多:上滑时,只有头部保留,其他的消息,tab标题,工具栏等隐藏;下滑时,消息,tab标题,工具栏等首先展示出来,然后内容列表继续下滑。

    后来想想,这种滑动效果,和floatHeaderSlivers的字面意思确实很像。只是一开始不知道,绕了很多圈子。

    注意事项

    • body: _buildTabContent(),这里其实有一个ListView,这个ListView不能指定Controller参数,不然的话,会导致整个header部分都不能滑动,只剩下body部分的这个ListView能滑动;千万不能这样用。
    CustomScrollView(
      //controller: logic.contentController, // 这里不能指定Controller,否则header将不能滑动
      physics: physics,
      slivers: [
        SliverList(
            delegate: SliverChildBuilderDelegate(
          (context, index) => _orderCell(logic.rows[index]),
          childCount: logic.rows.length,
        )),
      ],
    )
    
    • 滑动监听会影响性能,根据滑动距离修改透明度那种操作千万别做,会有明显的卡顿感觉。在调试的时候,加了个滑动监听,只是打印一下offset,都会有卡顿的感觉。
      /// 页面结构,放在一个CustomScrollView中
      Widget _buildBody() {
        return NestedScrollView(
          /// 这里可以加Controller,对于交互行为没有影响
          /// 但是如果这里加个滑动监听,会影响性能
          /// 这里只是加了个打印offset的操作,都会有卡顿的感觉
          //controller: logic.headerController,
          floatHeaderSlivers: true, // 这个参数能够让头部优先滑动,类似淘宝上滑引出菜单
          headerSliverBuilder: (context, innerBoxIsScrolled) {
            return [
              _buildAppBar(),
              SliverToBoxAdapter(child: _buildMessage()),
              SliverToBoxAdapter(child: _buildTabBar()),
              SliverToBoxAdapter(child: _buildToolBar()),
            ];
          },
          body: _buildTabContent(),
        );
      }
    
    • TabBar()只是一个普通的Widget,很多文章的例子都把它放在SliverAppBarbottom参数中,反而容易把人的思路带偏。在实践过程中,这个习惯性的坑绕了很久才绕出来

    • TabBar()TabBarView()需要一个共同的Controller把这两者联系起来,这个才是真正的限制条件。有些文章甚至说因为TabBarView(),所以要用NestedScrollView,纯粹瞎扯。这两者之间根本没有必然的逻辑关系。

    • SliverPersistentHeader这个组件研究了很久,最后感觉挺失望的。一是写法非常特殊,需要一个单独的类做delegate,很不方便。并且绕了这么多圈子,最后出来的效果,也并没有什么特别的,投入产出完全不成比例。实在要用相关的特性,不如直接用SliverAppBar来替代它,几行代码就搞定,效果还更好。SliverAppBar不一定要放在数组的第一个的,放后面的其他位置,效果也是一样的。

    • SliverList这个组件咋一看挺唬人的,感觉很难用。这次试了一下,SliverListCustomScrollView的组合,替换ListView还是挺简单的。
      替换后:

    CustomScrollView(
      physics: physics,
      slivers: [
        SliverList(
            delegate: SliverChildBuilderDelegate(
          (context, index) => _orderCell(logic.rows[index]),
          childCount: logic.rows.length,
        )),
      ],
    )
    

    替换前:

    ListView(
      physics: physics,
      children:
          get.rows.map((e) => _orderCell(get, e)).toList(),
    )
    

    这样替换的成本很低,但是性能提升明显。

    • SliverOverlapAbsorberSliverOverlapInjector一般成对出现,作用也是非常特定的,需要的时候抄作业就行,没有特殊需求的时候,不需要考虑。

    编码的形式真是太恶心了

    • Sliver家族号称性能有提升,实际尝试下来也确实如此。Flutter既然号称性能接近原生,为什么不把Sliver家族做的好用一点,或者把ListView之类的性能提升上去。为什么折腾得这么复杂?一直不明白。

    参考文章

    不一样角度带你了解 Flutter 中的滑动列表实现
    Flutter学习 可滚动Widget 下
    Flutter 入门指北(Part 8)之 Sliver 组件、NestedScrollView
    Flutter 之 嵌套可滚动组件 (二十五)

    相关文章

      网友评论

          本文标题:Flutter-使用NestedScrollView实践 202

          本文链接:https://www.haomeiwen.com/subject/wkrrydtx.html