在 Flutter 3.24 中增加了两位新和头部相关的 Sliver 组件成员,本文来看一下 PinnedHeaderSliver 为我们带来的便利。下面是本文基于 PinnedHeaderSliver 实现的三个案例。已经收录到了 FlutterUnit 开源项目 中,文中详细的源码可查阅对应组件:
官方案例 | 标题吸顶效果 | 标题+搜索吸顶 |
---|---|---|
1. PinnedHeaderSliver 的基本使用
如果官方有一个组件的案例,都会在该组件的源码注释中给出对应的位置。可以在 SDK 目录下按照路径找到它。我们就先从 PinnedHeaderSliver 的官方案例开始认识它的使用:
官方案例的效果如下所示,头部浅紫色的区域是 滑动视口 中的一部分,在滑动过程中会始终保持在顶部。点击右下角按钮时头部的文字会在轮训切换为一行和两行。
滑动 | 两行 |
---|---|
我们知道 Sliver 系列的组件只能使用在滑动视口之下, 案例中使用了 CustomScrollView
组件构建了可滑动的区域, PinnedHeaderSliver
作为第一个滑片, 构造入参传入 header 组件:
---->[node_01.dart]----
CustomScrollView(
controller: scrollController,
slivers: <Widget>[
PinnedHeaderSliver(child: header),
const _ItemList(),
],
),
),
这里的 header 是一个普通的组件,会根据计数器是否是奇数数展示 1 行还是 2 行文字:
final Widget header = Container(
color: colorScheme.surface,
padding: const EdgeInsets.all(4),
child: Material(
color: colorScheme.primaryContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
width: 7,
color: colorScheme.outline,
),
),
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 48),
child: Text(
count.isOdd ? 'Alternative Title\nWith Two Lines' : 'PinnedHeaderSliver',
style: theme.textTheme.headlineMedium!.copyWith(
color: colorScheme.onPrimaryContainer,
),
),
),
),
);
总的来看,PinnedHeaderSliver 还是非常简单的,它的构造入参中只有 child 和 key。也就是说该组件的作用就是:
将一个 普通组件 以滑片的身份入住滑动视口,并且滑动到顶部时,会停留而不滑出视口。
它是一个简化版的 SliverPersistentHeader,不要自定义 SliverPersistentHeaderDelegate 就可以轻松完成吸顶效果。可能看这个案例体会不到这个组件的实用价值,下面继续看一下我设计的小案例。
2. PinnedHeaderSliver 实现标题吸顶
如下所示,顶部的标题在下滑消失过程中,会以透明度变换逐渐展示在顶部的中间。上滑出现时,顶部的标题也会逐渐消失。这里的顶部标题就是使用到了 PinnedHeaderSliver
:
下滑 | 上滑 |
---|---|
由于滑动过程中顶部栏始终不动,所以它是 PinnedHeaderSliver
,下面的 Settings 文字是 SliverToBoxAdapter
滑片,所以滑动过程中 Settings 文字会移出视口。
既然上面官方代码中可以在构建头部时动态设置一行还是两行,那我们也可以动态设置标题文字的透明度。现在最关键的是如何的到滑动过程中 滑出的分度值,来确定标题文字的透明度。
如下所示,基于 SliverLayoutBuilder
组件可以感知滑片的 滑动约束 。这和 LayoutBuilder 组件的作用是类似的,其中可以根据滑动的偏移量 scrollOffset
和头部栏的高度计算出分度值:
---->[node_02.dart]----
Widget _buildSliverBar() {
const Icon icon = Icon(CupertinoIcons.settings, color: Colors.blue);
const TextStyle style = TextStyle(fontSize: 16, fontWeight: FontWeight.bold);
const Text text = Text('Settings', style: style);
Widget action = IconButton(onPressed: () {}, icon: icon );
return SliverLayoutBuilder(builder: (_, scs) {
double factor = (scs.scrollOffset / kToolbarHeight).clamp(0, 1);
factor = factor < 0.2 ? 0 : factor;
AppBar header = AppBar(
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
actions: [action],
centerTitle: true,
title: Opacity(opacity: factor, child: text),
);
return PinnedHeaderSliver(child: header);
});
}
Widget _buildTitleText() {
const TextStyle style = TextStyle(fontSize: 20, fontWeight: FontWeight.bold);
const Text text = Text('Settings', style: style);
return const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.only(left: 12.0, bottom: 8),
child: text,
),
);
}
3. 多个 PinnedHeaderSliver 的基本使用
细心的朋友可能发现了,上面效果中分隔线在滑动到顶部时也吸住了,不会滑出视口。PinnedHeaderSliver 组件可以同时存在多个。在其上方的普通 Sliver 滑出视口后,下方的 PinnedHeaderSliver 会停留在顶部。后续的普通 Sliver 可继续上滑:
所以代码中只需要添加如下代码即可:
---->[node_02.dart]----
CustomScrollView(
controller: scrollController,
slivers: <Widget>[
_buildSliverBar(),
_buildTitleText(),
const PinnedHeaderSliver(child: Divider()), ///++ add divier
const _ItemList(),
],
),
那么最后一个效果的实现方案就呼之欲出了: 我们可以将输入框放下 PinnedHeaderSliver 中,即可完成输入框在下滑过程中吸顶。
下滑 | 上滑 |
---|---|
---->[node_03.dart]----
Widget _buildSliverSearch() {
BoxDecoration decoration = BoxDecoration(
color: const Color(0xffefeff1), borderRadius: BorderRadius.circular(6));
Widget prefix = const Padding(
padding: EdgeInsets.only(left: 8.0),
child: Icon(CupertinoIcons.search, size: 20, color: Color(0xff808082))
);
return PinnedHeaderSliver(
child: ColoredBox(
color: Colors.white,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0, right: 12, left: 12),
child: CupertinoTextField(
readOnly: true,
placeholder: '搜索',
onTap: _toSearchPage,
decoration: decoration,
prefix:prefix,
style: const TextStyle(fontSize: 14),
placeholderStyle: const TextStyle(color: Color(0xff808082)),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
),
),
const Divider(),
],
),
));
}
尾声
总的来看,PinnedHeaderSliver 是一个非常简单,但也非常强大的组件。它可以在滑动视口中放置多个,让你随心所欲地可以看中哪些部位可以吸顶。那本文就到这里,如果 FlutterUnit 系列对你有所帮助,还请在 Github 上多多 Start 。我们下篇再见~
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。