LoadingMore 和 Sliver 结合
如果你是个 Flutter 新手,正在寻求解决方案,那么这篇文章不适合你。如果是个老🐦,那点个赞或者评论区随便讲两句吧,让我知道有大佬看了我的文章👀。
进入正题
缘起于 BouncingScrollPhysics
和 NotificationListener
,因为 BouncingScrollPhysics
为了实现回弹的效果, apply 的值为0,导致 xxx 不会发出 OverScrolledNotification
,为了获取 overscrolled 的信息,要么对ScrollUpdateNotification
嗯造,要么改写BouncingScrollPhysics
。
大多数LoadingMore都是这么做的,再套个Stack
和NotificationListener
实现一下刷新的组件。不过这里,并不使用上述两种方式,而是 类似 SliverPersistentHeaderDelegate
的那种通过给开发者 build
方法,并提供类似像 shrinkOffset
, overlapsContent
的参数,自己去构建对应的 Sliver
。
SliverPersistentHeader
的 秘密
吸顶效果的爹,内容不多讲,因为没用过这个 Widget
的后面的也看不懂。
话不多说,直接干到 _SliverPersistentHeaderElement
看源码,我们可以得知,这个 element
和 renderobj
是互相持有的。
先看看 element
更新 widget
的方法:
// element 触发 delegate.build 的方法
void _build(double shrinkOffset, bool overlapsContent) {
owner!.buildScope(this, () {
child = updateChild(
child,
floating
? _FloatingHeader(child: widget.delegate.build(
this,
shrinkOffset,
overlapsContent
))
: widget.delegate.build(this, shrinkOffset, overlapsContent),
null,
);
});
}
再看看对应的 renderobj
内部, 该 element
创建的 renderobj
的一个重要父类是 RenderSliverPersistentHeader
, 然后层层继承下来,根据 Widget
的 pinned
和 floating
属性生成对应的私有 _RenderSliverXXXX
(我们并不关心),并带有_RenderSliverPersistentHeaderForWidgetsMixin
(这个才是重点)。
这个 mixin 就是让 renderobj
持有 element
的原因,并且通过调用 updateChild
( renderobj
的方法) 触发 delegate
提供的 build
。
mixin _RenderSliverPersistentHeaderForWidgetsMixin on RenderSliverPersistentHeader {
_SliverPersistentHeaderElement? _element;
@override
double get minExtent => _element!.widget.delegate.minExtent;
@override
double get maxExtent => _element!.widget.delegate.maxExtent;
@override
void updateChild(double shrinkOffset, bool overlapsContent) {
assert(_element != null);
_element!._build(shrinkOffset, overlapsContent);
}
@protected
void triggerRebuild() {
markNeedsLayout();
}
}
那么整个触发 build
的流程是什么样的呢?这里帮你梳理一遍 Flutter 滑动部分的知识
推荐文章:
我就隐藏魔法了,过程大概是 Scrollable
-> Viewport flush
-> Sliver performLayout
。
比如现在轮到吸顶 RenderSliverPersistentHeader
进行 performLayout
,看源码得知执行了layoutChild
(触发吸顶) 和 updateGeometry
(应该是吸顶效果,因为更新了主轴坐标)。
layoutChild
(拨云见雾)
invokeLayoutCallback
中执行了 updateChild
,也就是我们上述 _RenderSliverPersistentHeaderForWidgetsMixin
的一个方法,触发了 delegate
的 build
,最后进行 layout
。
void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent = false }) {
final double shrinkOffset = math.min(scrollOffset, maxExtent);
if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
// 调用了 renderObj的 updateChild 再触发 element的 _build
updateChild(shrinkOffset, overlapsContent);
});
_lastShrinkOffset = shrinkOffset;
_lastOverlapsContent = overlapsContent;
_needsUpdateChild = false;
}
double stretchOffset = 0.0;
if (stretchConfiguration != null && constraints.scrollOffset == 0.0) {
stretchOffset += constraints.overlap.abs();
}
child?.layout(
constraints.asBoxConstraints(
maxExtent: math.max(minExtent, maxExtent - shrinkOffset) + stretchOffset,
),
parentUsesSize: true,
);
......
}
有人就问了,诶,你这个吸顶和 LoadingMore有什么关系捏?
确实关系并不大,但是没有不行,因为我们想要实现那种类似 delegate
和 build
的方式实现,这里不得不了解内部实现,不过下面就正式进入 加载更多
的内容了
SliverConstraints
和 SliverGeometry
的 魔法
ViewPort
很聪明,它总是对 看得见
和 缓冲区
内的 所有Sliver进行 performLayout
, SliverConstraints
输入给 Sliver
布局, SliverGeometry
再输出给 ViewPort
。
我们就可以无脑利用这些属性,来判断是否溜到底部了捏,从而实现无 ScrollNotification
进行 LoadingMore
!!!!!
SliverConstraints
属性介绍
-
remainingPaintExtent
: 可以简单的理解为还有多少 pixel 可以画(不一定对...但是保证,当你的sliver
从底部滑出去了,这个值就为0)。 -
precedingScrollExtent
: 之前的Sliver
一共消耗了多少滑动大小(对应下面的scrollExtent
),可以用来判断Sliver
是否充满Viewport
。 -
viewportMainAxisExtent
: 如其名。 -
overlap
: 前一个Sliver
的paintExtent
-layoutExtent
,常见于吸顶之类的效果,官方注释写得详细,这里几乎没使用。
SliverGeometry
属性介绍
-
scrollExtent
: 滑动体的长度,比如说的最后一个sliver
,高度 100,但是这个值为 0,那么只有在overscroll
的时候看得到,松手回弹后就溜到viewport
底部外面了。 -
paintExtent
: 如其名。 -
layoutExtent
&maxPaintExtent
: 如其名,打开 devtools 的 那个绘制界面布局信息,对应的就是的箭头➡️。
属性都给你了,怎么判断我就不用多讲了吧?上一份自己写的 performLayout
代码
double overscrolled = 0;
double get maxScrollExtent => _element!.widget.delegate.maxScrollExtent;
@override
void performLayout() {
SliverConstraints constraints = this.constraints;
// We never call build unless user overscrolled.
if (constraints.remainingPaintExtent < 1) {
geometry = SliverGeometry.zero;
return;
}
// calculate remain space on viewport.
// if slivers before this one not fill the viewportExtent, this value could
// be < 0, which means this sliver is always visiable now, in this case, we
// never performm any load more behavior.
double extent =
constraints.precedingScrollExtent - constraints.viewportMainAxisExtent;
// the total overscrolled area in viewport.
double maxExtent = constraints.remainingPaintExtent - min(constraints.overlap, 0.0);
if (extent <= 0) {
// we offer overscrolled 0 to builder, but the constraint to passed to
// child still the remainingPaintExtent. you can use this constraint
//to custom what you want.
overscrolled = 0;
invokeLayoutCallback((constraints) {
updateChild();
});
child?.layout(constraints.asBoxConstraints(maxExtent: maxExtent));
geometry = SliverGeometry(
scrollExtent: 0,
paintExtent: maxExtent,
maxPaintExtent: maxExtent,
);
return;
}
// here, remainingPaintExtent is overscrolled.
overscrolled = maxExtent;
invokeLayoutCallback((constraints) {
updateChild();
});
child?.layout(constraints.asBoxConstraints(maxExtent: maxExtent),
parentUsesSize: true);
geometry = SliverGeometry(
scrollExtent: min(maxExtent, maxScrollExtent),
paintExtent: maxExtent,
maxPaintExtent: maxExtent);
}
看看效果图
缺个法师来触发更新了
当我思考不出怎么去写一份优雅的 build
和 加载相结合的代码时,不小心看了 CupertinoSliverRefreshControl
的源码。
法师就是 CupertinoSliverRefreshControl
的 状态机
很可惜,官方写的状态机代码是看不懂的,因为这东西是要画图自己搓的,我就自己搓了一张,在这里贴一下。
接着,在自己创造的 delegate
设置一些必要的属性,把状态机的代码写进来,比如触发更新的阈值之类的东西,记住在执行异步 setState
的时候,应推迟或提前到 帧 build
之前,这里使用推迟到帧后的方法。
ValueNotifier<RefreshState> loadingStateNotifier;
bool isTriggered = false;
bool canTiggerNext = false;
@override
final double triggerDistance;
@override
final double maxScrollExtent;
final double ignoreRefreshDistance;
RefreshCallback? onRefresh;
@override
Widget builder(BuildContext context, double overscrolled) {
handleNextState(loadingStateNotifier, overscrolled);
return ValueListenableBuilder<RefreshState>(
valueListenable: loadingStateNotifier,
builder: (context, state, child) {
return handleStateBuild(state, overscrolled);
});
}
// 状态机代码
void handleNextState(ValueNotifier<RefreshState> currentState, double overscrolled) {
switch (currentState.value) {
case RefreshState.inactive:
if (overscrolled < triggerDistance) {
currentState.value = RefreshState.inactive;
break;
}
isTriggered = true;
currentState.value = RefreshState.refreshing;
SchedulerBinding.instance!.addPostFrameCallback((timeStamp) {
onRefresh!().whenComplete(
() {
isTriggered = false;
currentState.value = RefreshState.done;
},
);
});
break;
case RefreshState.refreshing:
if (isTriggered) {
currentState.value = RefreshState.refreshing;
break;
}
currentState.value = RefreshState.done;
break;
case RefreshState.done:
if (overscrolled < ignoreRefreshDistance) {
// when done, wating overscroll to 0 or user make it to 0,
// the state could be inactive, otherwise, keep
currentState.value = RefreshState.inactive;
break;
}
currentState.value = RefreshState.done;
}
}
Widget handleStateBuild(RefreshState currentState, double overscrolled) {
switch (currentState) {
case RefreshState.inactive:
// inactive 需要构建的 widget
case RefreshState.refreshing:
// refreshing 需要构建的 widget
case RefreshState.done:
// done 需要构建的 widget
}
}
实现效果图
使用方法就十分优雅了,类似 Pinterest 的更新效果
return CustomScrollView(
slivers: [
// 假设这里是法法的 SliverWaterFallFlow ....
LoadingMoreSliver(
onRefresh: () async {
await getDataFromServers();
setState((){
// 数据更新
);
},
)
],
);