Flutter 组件集录 | FlexibleSpaceBar 组件是怎么炼成的

5,134 阅读5分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」


1. 前言: 问题引入

FlexibleSpaceBar 是一个和 SliverAppBar 共生的组件,一般不单独使用。如下所示,在滑动的过程中 FlexibleSpaceBartitle 区域会有缩放的效果。仔细观察会发现,这个缩放是对整个 title 组件生效的,比如 全部笔记1590 项笔记 文字都有缩放效果。

上滑下滑
41.gif42.gif

那问题来了,如何只让 全部笔记 的标题有缩放效果,下面的副标题在滑动过程中一直保持不变呢?或者我们如何监听滑动的分率,实现一些自定义的变换效果呢?这就要从 FlexibleSpaceBar 组件的源码中寻找答案,看它是否支持这种效果,如果不支持,我们该如何自己实现。


2. 探索: FlexibleSpaceBar 组件的标题如何实现缩放?

在滑动中,title 组件的内容有缩放效果是实事,这说明在组件的 构建逻辑 中必然存在缩放的变换。所以摆在我们面前的第一个问题是:

FlexibleSpaceBar 组件的标题如何 实现缩放 的?

FlexibleSpaceBar 是一个 StatefulWidget,它依赖于 _FlexibleSpaceBarState 状态类来构建组件。所以缩放变换的逻辑也应该在 _FlexibleSpaceBarState#build 方法中:

image.png

class FlexibleSpaceBar extends StatefulWidget {
    //略...

    @override
    State<FlexibleSpaceBar> createState() => _FlexibleSpaceBarState();
}

如下所示,在 _FlexibleSpaceBarState#build 中通过 Transform 实现对标题的缩放,缩放使用的矩阵对象是 scaleTransform :

image.png


3. 探索: FlexibleSpaceBar 组件是如何感知滑动数据的?

从上面效果中可以看出,SliverAppBar 滑动距离和剩余空间的比值,会作为缩放数值的依据。那么摆在我们面前的第一个问题是:

FlexibleSpaceBar 组件是如何感知 滑动数据 的?

如下,是 scaleTransform 矩阵的生成过程,其中的缩放值是一个 补间值,起始值是 widget.expandedTitleScale,也就是初始的缩放值,默认为 1.5。也就是说默认情况下,title 组件会被放大 1.5 倍,然后根据 t 值的大小,向 1 进行补间计算。

比如 t = 0.5 时, scaleValue 的值就在 1.51 的正中间,即 1 + (1.5-1)*0.5 = 1.25,以此类推。现在的关键就是这个 t 是如何计算的,滑动的数据信息是谁,通过什么渠道 "贩卖的"

---->[_FlexibleSpaceBarState#build]----
final double scaleValue = Tween<double>(begin: widget.expandedTitleScale, end: 1.0).transform(t);
final Matrix4 scaleTransform = Matrix4.identity()
  ..scale(scaleValue, scaleValue, 1.0);

如下是 t 值的计算过程,可以看出滑动的数据是由 FlexibleSpaceBarSettings 组件记录的,毫无疑问,它是一个 InheritedWidget, 子树可以通过它访问存储的信息,本质上和 Theme 一族是一样的。当 t0 时,表示完全展开的状态;t1 时,表示 SliverAppBar 剩余空间完全收起:

image.png


4. 探索: FlexibleSpaceBarSettings 组件在何时入树的?

现在进入最后一个问题,FlexibleSpaceBarSettings 是何时进入组件树的,其中记录的信息又是如何放入其中的。在 FlexibleSpaceBar 中有一个静态方法,如下所示,会在入参 child 组件的上层包裹住 FlexibleSpaceBarSettings ,其中持有的信息都是通过入参传递的。如下其注释中也有提及:

image.png

也就是说,觉得存入的滑动信息是 FlexibleSpaceBar#createSettings 方法的调用者。到这就好办了,想要知道方法何时被调用的,调试来帮忙。

image.png

如下可以看出,是在 _SliverAppBarDelegate 中被调用的。它最终会作为 SliverPersistentHeader 组件的代理类,用于 _SliverAppBarState 的构建逻辑中。

image.png

到这里就万事俱备了,从上滑的分析可以知道,滑动的信息从 FlexibleSpaceBarSettings 中可以获取到。另外,FlexibleSpaceBar 状态类的构建逻辑在处理变换时是比较死的,没有暴露给使用者可操作性的空间。所以需要实现一些自定义的变换效果,可以 copy 一下 FlexibleSpaceBar 的源码,来魔改一下。


5. 支持固定副标题

现在来实现一下固定副标题,效果如下,在滑动过程中 1590 项笔记 副标题一直保持不变。

上滑下滑

现在将源码拷贝一份,命名为 DiyFlexibleSpaceBar,在其中定义 fixedSubtitle 构造入参,作为副标题的插槽。

image.png

final Widget? fixedSubtitle;

然后在源码组件状态类的 构建逻辑 中,通过 Column 提供一个插槽即可,如下所示:

image.png

然后使用 DiyFlexibleSpaceBar 组件,提供 fixedSubtitle 组件即可:

image.png


6. 将滑动分度值暴露出去

有时候我们期望监听到滑动的进度,从而可以在 DiyFlexibleSpaceBar 外部,处理自定义的滑动效果。比如下图中,标题的右侧有一个小风车,可以随着滑动的进度旋转:

上滑下滑
46.gif48.gif

首先,定义一个 FractionalBuilder 的函数类型,用于回调 t 数值来返回组件 Widget; 然后在 DiyFlexibleSpaceBar 中定义 titleIconBuilder 成员:

typedef FractionalBuilder = Widget Function(double t);

class DiyFlexibleSpaceBar extends StatefulWidget {

  const DiyFlexibleSpaceBar({
    /// 略...
    this.titleIconBuilder,

    final FractionalBuilder? titleIconBuilder;

在构建逻辑在触发 titleIconBuilder 构造组件,参数 t 就可以从这里暴露出去:

image.png

然后外界可以感知到 t 的存在,以此控制风车旋转角度即可。关于风车的绘制,参考 《Flutter 绘制集录 | 第四画 - 风车》

image.png


Flutter 提供的组件,只能满足大多数的使用场景,对于特别的需求,可能是不支持的。这时就是考验一位编程者能力的时机了,能看懂源码,并能以此进行改进,来完成需求就显得极为重要。希望大家可以在日常开发中 多做推敲 ,而不是遇事就 伸手去要。 那本文就到这里,谢谢观看 ~