需求简介
订单列表如图所示,这是一个订单列表,红框中的消息,tab标题,工具栏三部分比较占空间,在上划的时候,隐藏比较好。
简单分析
看原来的代码,实现的思路是:
-
红框部分的消息和
Tab标题
,放在了AppBar
的bottom
部分 -
红框部分的工具栏和
TabBarView
放在了body
部分 -
TabBarView
中放置了一个ListView
这种写法算是主流做法,红框部分一直存在,不能隐藏。
现在要改成红框部分也能滑动,能隐藏,该怎么修改?
NestedScrollView
(1) 红框部分是需要滑动的
(2)内容部分在ListView
中,本身也是能滑动的
(3)并且还有TabBarView
,需要进行内容的切换
以上三条,用NestedScrollView
来实现非常适合。
-
红框部分放入图中的
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
在最上层,而FlexibleSpaceBar
的background
在下面一层,没有这个占位的话,那些消息,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
,很多文章的例子都把它放在SliverAppBar
的bottom
参数中,反而容易把人的思路带偏。在实践过程中,这个习惯性的坑绕了很久才绕出来 -
TabBar()
和TabBarView()
需要一个共同的Controller
把这两者联系起来,这个才是真正的限制条件。有些文章甚至说因为TabBarView()
,所以要用NestedScrollView
,纯粹瞎扯。这两者之间根本没有必然的逻辑关系。 -
SliverPersistentHeader
这个组件研究了很久,最后感觉挺失望的。一是写法非常特殊,需要一个单独的类做delegate
,很不方便。并且绕了这么多圈子,最后出来的效果,也并没有什么特别的,投入产出完全不成比例。实在要用相关的特性,不如直接用SliverAppBar
来替代它,几行代码就搞定,效果还更好。SliverAppBar
不一定要放在数组的第一个的,放后面的其他位置,效果也是一样的。 -
SliverList
这个组件咋一看挺唬人的,感觉很难用。这次试了一下,SliverList
与CustomScrollView
的组合,替换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(),
)
这样替换的成本很低,但是性能提升明显。
-
SliverOverlapAbsorber
和SliverOverlapInjector
一般成对出现,作用也是非常特定的,需要的时候抄作业就行,没有特殊需求的时候,不需要考虑。
编码的形式真是太恶心了
-
Sliver
家族号称性能有提升,实际尝试下来也确实如此。Flutter
既然号称性能接近原生,为什么不把Sliver
家族做的好用一点,或者把ListView
之类的性能提升上去。为什么折腾得这么复杂?一直不明白。
参考文章
不一样角度带你了解 Flutter 中的滑动列表实现
Flutter学习 可滚动Widget 下
Flutter 入门指北(Part 8)之 Sliver 组件、NestedScrollView
Flutter 之 嵌套可滚动组件 (二十五)
网友评论