本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
问题
先说下问题与解决思路,以及解决方案。
当我们想要监听一个widget的滑动状态时,可以使用:NotificationListener
。
在我目前空余时间写的一个flutter项目中,有一个十分复杂的组件,需要用到这东西。
要实现下面这个功能。
这个UI由哪些功能点
- 当listview的第一个条目显示出来的时候,此时继续下拉,整个listview下移
- 当listview处于最底部时,向上拖拽时,整个listview上移
- 当手离开屏幕时,如果listview的最高高度处于屏幕高度二分之一以上,整个listview自动滚动到最顶部
- 当手离开屏幕时,如果listview的最高高度处于屏幕高度二分之一以下,整个listview自动滚动到最底部
这篇博客呢,讲的就是关于功能点一的。当listview的第一个条目显示出来的时候,此时继续下拉。我要处理这个情况的UI。
由这个问题,引发的解决问题的思路,以及关于学习新姿势的一些思考与感悟。
PS: 为了达到完美的效果,这个需求,我搞了一周~~
NotificationListener的使用
final GlobalKey _key = GlobalKey();
@override
Widget build(BuildContext context) {
final Widget child = NotificationListener<ScrollStartNotification>(
key: _key,
child: NotificationListener<ScrollUpdateNotification>(
child: NotificationListener<OverscrollNotification>(
child: NotificationListener<ScrollEndNotification>(
child: widget.child,
onNotification: (ScrollEndNotification notification) {
return false;
},
),
onNotification: (OverscrollNotification notification) {
return false;
},
),
onNotification: (ScrollUpdateNotification notification) {
return false;
},
),
onNotification: (ScrollStartNotification scrollUpdateNotification) {
return false;
},
);
return child;
}
其中,
- ScrollStartNotification 组件开始滑动
- ScrollUpdateNotification 组件位置发生改变
- OverscrollNotification 表示窗口小组件未更改它的滚动位置,因为更改会导致滚动位置超出其滚动范围
- ScrollEndNotification 组件已经停止滚动
Demo
body: SafeArea(
child: NotificationListener<ScrollStartNotification>(
child: NotificationListener<OverscrollNotification>(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Text('data=$index');
},
itemCount: 100),
onNotification: (OverscrollNotification notification) {
print('OverscrollNotification');
},
),
onNotification: (ScrollStartNotification notification) {
print('ScrollStartNotification');
},
))
在Android中效果
可以看到刚开始下拉的时候,回调的是
ScrollStartNotification
的onNotification
方法,之后都是OverscrollNotification。
在ios中效果
可以看到
OverscrollNotification
不会被调用,调用的是ScrollStartNotification
在我的一些复杂UI效果中,需要在OverscrollNotification
回调中做一些事情。
当ScrollView滚动到顶部时,继续下拉时。在Android平台中,OverscrollNotification
会被调用;在iOS平台的真机中,OverscrollNotification
不会被调用,调用的是ScrollStartNotification
。这就造成了平台的不一致性。我也尝试了Google一下,但是…我看到这个问题的时候,问题还没解决。后来我就解决了,然后给了他回答。这个后面再说。
问题:OverscrollNotification
在Android中正常调用;在iOS的真机中,无法调用。
定位原因
分析NotificationListener的onNotification调用栈。
- Step 1 翻源码
ListView
是继承自ScrollView
的。我们跟着ScrollView
的build方法,一步步向上级查询,可以看到scroll_activity.dart
的下面几个跟OverscrollNotification
相关的方法:
这就明了多了。
- Step 2 翻源码
继续上溯,进入到scroll_position.dart
,看到OverscrollNotification
被实际调用的方法:
- Step 3
OverscrollNotification
能否被调用的判断位置
- Step 4 分析
applyBoundaryConditions
方法
@protected
double applyBoundaryConditions(double value) {
final double result = physics.applyBoundaryConditions(this, value);//这里physics来控制返回值
assert(() {
final double delta = value - pixels;
if (result.abs() > delta.abs()) {
throw FlutterError(
'${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n'
'The method was called to consider a change from $pixels to $value, which is a '
'delta of ${delta.toStringAsFixed(1)} units. However, it returned an overscroll of '
'${result.toStringAsFixed(1)} units, which has a greater magnitude than the delta. '
'The applyBoundaryConditions method is only supposed to reduce the possible range '
'of movement, not increase it.\n'
'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the '
'viewport dimension is $viewportDimension.'
);
}
return true;
}());
return result;
}
而physics
是ScrollPhysics
的实例。
在进入到physics.applyBoundaryConditions(this, value);
的applyBoundaryConditions
方法中
///
/// [BouncingScrollPhysics] returns zero. In other words, it allows scrolling
/// past the boundary unhindered.
///
/// [ClampingScrollPhysics] returns the amount by which the value is beyond
/// the position or the boundary, whichever is furthest from the content. In
/// other words, it disallows scrolling past the boundary, but allows
/// scrolling back from being overscrolled, if for some reason the position
/// ends up overscrolled.
double applyBoundaryConditions(ScrollMetrics position, double value) {
if (parent == null)
return 0.0;
return parent.applyBoundaryConditions(position, value);
}
注释中,写着BouncingScrollPhysics
的滑动不受阻碍,可以一直滑动。也就是在iOS平台的ScrollView
中,可以一直下拉。也就是,我上面的demo效果。对于ClampingScrollPhysics
无法继续下拉。
step 5
parent
的具体实现
继续debug源码。physics
在Android中的实现
physics.applyBoundaryConditions
在Android中由 ClampingScrollPhysics 完成
ClampingScrollPhysics.applyBoundaryConditions
@override
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw FlutterError(
'$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n'
);
}
return true;
}());
if (value < position.pixels && position.pixels <= position.minScrollExtent) // underscroll
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll
return value - position.pixels;
if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
return 0.0;
}
-
physics
在iOS中的具体实现
physics.applyBoundaryConditions
在iOS中由 BouncingScrollPhysics 完成
BouncingScrollPhysics.applyBoundaryConditions
@override
double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
在Android平台中,会对applyBoundaryConditions
的返回值做处理,不为零的时候(看下step3),是会调用OverscrollNotification.onNotification
;但是对于iOS平台,由于默认一直返回0.0,故不会调用。
原来如此
由于我这里需要的是Android的效果,所以需要将physics
的具体实现更改为ClampingScrollPhysics
即可,正好,
我们将physics
的实现变更为ClampingScrollPhysics
,完美解决。
拓展思维
如果,我们将physics
的实现变更为BouncingScrollPhysics
,会发生什么?
完美的在Android上实现了,同iOS一样的可以一直下拉的listview效果。
彩蛋
思考为什么两个平台physics
的具体实现不同
这个原因,也就是相当于physics
什么时候被初始化的。我就不娓娓道来了,我这边翻阅并且debug源码找到了出处。在scroll_configuration.dart
文件中,有下面一段代码:
/// The scroll physics to use for the platform given by [getPlatform].
///
/// Defaults to [BouncingScrollPhysics] on iOS and [ClampingScrollPhysics] on
/// Android.
ScrollPhysics getScrollPhysics(BuildContext context) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return const BouncingScrollPhysics();
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return const ClampingScrollPhysics();
}
return null;
}
可以看到,不同的平台,返回的值是不用的。返回的结果,也验证了我们刚才debug的结果。小惊喜:,看TargetPlatform.fuchsia
,看来fuchsia系统即将到来。
Flutter要统一天下啊~
共勉
学习一门新系统知识,一定要知其然并知其所以然。如果,我直接设置physics
的值,不会学习到实质性的知识。明白了原理才能掌控全局。之前看一些Android大神的博客,很多东西,都是翻阅源码debug而来的。况且当下Flutter的相关有深度有见地的资料不多的情况下,我也是被逼的,没办法。只有翻阅源码了。翻过了源码,却获得了意外之喜,收获了更多知识。
最后一句话,与君共勉:勤而学之,柳暗花明又一村。
PS:最终实现的开头效果的源码与思路,里面涉及到手势识别、类似Android的事件分发、动画、滑动监听以及解刨源码等等。估计要写很多字~~有时间再来一篇博客。
flutter issues已经提交了相关建议。
Flutter 豆瓣客户端,诚心开源
Flutter Container
Flutter SafeArea
Flutter Row Column MainAxisAlignment Expanded
Flutter Image全解析
Flutter 常用按钮总结
Flutter ListView豆瓣电影排行榜
Flutter Card
Flutter Navigator&Router(导航与路由)
OverscrollNotification不起效果引起的Flutter感悟分享
Flutter 上拉抽屉实现
Flutter 豆瓣客户端,诚心开源
Flutter 更改状态栏颜色