本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布
問題
先說下問題與解決思路,以及解決方案。
當(dāng)我們想要監(jiān)聽一個(gè)widget的滑動(dòng)狀態(tài)時(shí),可以使用:NotificationListener。
在我目前空余時(shí)間寫的一個(gè)flutter項(xiàng)目中,有一個(gè)十分復(fù)雜的組件,需要用到這東西。
要實(shí)現(xiàn)下面這個(gè)功能。

這個(gè)UI由哪些功能點(diǎn)
- 當(dāng)listview的第一個(gè)條目顯示出來的時(shí)候,此時(shí)繼續(xù)下拉,整個(gè)listview下移
- 當(dāng)listview處于最底部時(shí),向上拖拽時(shí),整個(gè)listview上移
- 當(dāng)手離開屏幕時(shí),如果listview的最高高度處于屏幕高度二分之一以上,整個(gè)listview自動(dòng)滾動(dòng)到最頂部
- 當(dāng)手離開屏幕時(shí),如果listview的最高高度處于屏幕高度二分之一以下,整個(gè)listview自動(dòng)滾動(dòng)到最底部
這篇博客呢,講的就是關(guān)于功能點(diǎn)一的。當(dāng)listview的第一個(gè)條目顯示出來的時(shí)候,此時(shí)繼續(xù)下拉。我要處理這個(gè)情況的UI。
由這個(gè)問題,引發(fā)的解決問題的思路,以及關(guān)于學(xué)習(xí)新姿勢(shì)的一些思考與感悟。
PS: 為了達(dá)到完美的效果,這個(gè)需求,我搞了一周~~
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 組件開始滑動(dòng)
- ScrollUpdateNotification 組件位置發(fā)生改變
- OverscrollNotification 表示窗口小組件未更改它的滾動(dòng)位置,因?yàn)楦臅?huì)導(dǎo)致滾動(dòng)位置超出其滾動(dòng)范圍
- ScrollEndNotification 組件已經(jīng)停止?jié)L動(dòng)
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中效果

可以看到剛開始下拉的時(shí)候,回調(diào)的是
ScrollStartNotification的onNotification方法,之后都是OverscrollNotification。
在ios中效果

可以看到
OverscrollNotification不會(huì)被調(diào)用,調(diào)用的是ScrollStartNotification
在我的一些復(fù)雜UI效果中,需要在OverscrollNotification回調(diào)中做一些事情。
當(dāng)ScrollView滾動(dòng)到頂部時(shí),繼續(xù)下拉時(shí)。在Android平臺(tái)中,OverscrollNotification會(huì)被調(diào)用;在iOS平臺(tái)的真機(jī)中,OverscrollNotification不會(huì)被調(diào)用,調(diào)用的是ScrollStartNotification。這就造成了平臺(tái)的不一致性。我也嘗試了Google一下,但是…我看到這個(gè)問題的時(shí)候,問題還沒解決。后來我就解決了,然后給了他回答。這個(gè)后面再說。

問題:OverscrollNotification在Android中正常調(diào)用;在iOS的真機(jī)中,無法調(diào)用。
定位原因
分析NotificationListener的onNotification調(diào)用棧。
- Step 1 翻源碼
ListView是繼承自ScrollView的。我們跟著ScrollView的build方法,一步步向上級(jí)查詢,可以看到scroll_activity.dart的下面幾個(gè)跟OverscrollNotification相關(guān)的方法:

這就明了多了。
- Step 2 翻源碼
繼續(xù)上溯,進(jìn)入到scroll_position.dart,看到OverscrollNotification被實(shí)際調(diào)用的方法:

- Step 3
OverscrollNotification能否被調(diào)用的判斷位置

- 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的實(shí)例。
在進(jìn)入到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的滑動(dòng)不受阻礙,可以一直滑動(dòng)。也就是在iOS平臺(tái)的ScrollView中,可以一直下拉。也就是,我上面的demo效果。對(duì)于ClampingScrollPhysics無法繼續(xù)下拉。
step 5
parent的具體實(shí)現(xiàn)
繼續(xù)debug源碼。physics在Android中的實(shí)現(xiàn)

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中的具體實(shí)現(xiàn)

physics.applyBoundaryConditions在iOS中由 BouncingScrollPhysics 完成
BouncingScrollPhysics.applyBoundaryConditions
@override
double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
在Android平臺(tái)中,會(huì)對(duì)applyBoundaryConditions的返回值做處理,不為零的時(shí)候(看下step3),是會(huì)調(diào)用OverscrollNotification.onNotification;但是對(duì)于iOS平臺(tái),由于默認(rèn)一直返回0.0,故不會(huì)調(diào)用。
原來如此
由于我這里需要的是Android的效果,所以需要將physics的具體實(shí)現(xiàn)更改為ClampingScrollPhysics即可,正好,

我們將physics的實(shí)現(xiàn)變更為ClampingScrollPhysics,完美解決。

拓展思維
如果,我們將physics的實(shí)現(xiàn)變更為BouncingScrollPhysics,會(huì)發(fā)生什么?

完美的在Android上實(shí)現(xiàn)了,同iOS一樣的可以一直下拉的listview效果。
彩蛋
思考為什么兩個(gè)平臺(tái)physics的具體實(shí)現(xiàn)不同
這個(gè)原因,也就是相當(dāng)于physics什么時(shí)候被初始化的。我就不娓娓道來了,我這邊翻閱并且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;
}
可以看到,不同的平臺(tái),返回的值是不用的。返回的結(jié)果,也驗(yàn)證了我們剛才debug的結(jié)果。小驚喜:,看TargetPlatform.fuchsia,看來fuchsia系統(tǒng)即將到來。
Flutter要統(tǒng)一天下啊~

共勉
學(xué)習(xí)一門新系統(tǒng)知識(shí),一定要知其然并知其所以然。如果,我直接設(shè)置physics的值,不會(huì)學(xué)習(xí)到實(shí)質(zhì)性的知識(shí)。明白了原理才能掌控全局。之前看一些Android大神的博客,很多東西,都是翻閱源碼debug而來的。況且當(dāng)下Flutter的相關(guān)有深度有見地的資料不多的情況下,我也是被逼的,沒辦法。只有翻閱源碼了。翻過了源碼,卻獲得了意外之喜,收獲了更多知識(shí)。
最后一句話,與君共勉:勤而學(xué)之,柳暗花明又一村。
PS:最終實(shí)現(xiàn)的開頭效果的源碼與思路,里面涉及到手勢(shì)識(shí)別、類似Android的事件分發(fā)、動(dòng)畫、滑動(dòng)監(jiān)聽以及解刨源碼等等。估計(jì)要寫很多字~~有時(shí)間再來一篇博客。
flutter issues已經(jīng)提交了相關(guān)建議。

Flutter 豆瓣客戶端,誠(chéng)心開源
Flutter Container
Flutter SafeArea
Flutter Row Column MainAxisAlignment Expanded
Flutter Image全解析
Flutter 常用按鈕總結(jié)
Flutter ListView豆瓣電影排行榜
Flutter Card
Flutter Navigator&Router(導(dǎo)航與路由)
OverscrollNotification不起效果引起的Flutter感悟分享
Flutter 上拉抽屜實(shí)現(xiàn)
Flutter 豆瓣客戶端,誠(chéng)心開源
Flutter 更改狀態(tài)欄顏色