一. 監(jiān)聽滾動(dòng)事件
對(duì)于滾動(dòng)的視圖,我們經(jīng)常需要監(jiān)聽它的一些滾動(dòng)事件敌卓,在監(jiān)聽到的時(shí)候去做對(duì)應(yīng)的一些事情慎式。
比如視圖滾動(dòng)到底部時(shí),我們可能希望做上拉加載更多假哎;
比如滾動(dòng)到一定位置時(shí)顯示一個(gè)回到頂部的按鈕瞬捕,點(diǎn)擊回到頂部的按鈕,回到頂部舵抹;
比如監(jiān)聽滾動(dòng)什么時(shí)候開始肪虎,什么時(shí)候結(jié)束;
在Flutter中監(jiān)聽滾動(dòng)相關(guān)的內(nèi)容由兩部分組成:ScrollController和ScrollNotification惧蛹。
1.1. ScrollController
在Flutter中扇救,Widget并不是最終渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常這種監(jiān)聽事件以及相關(guān)的信息并不能直接從Widget中獲取香嗓,而是必須通過對(duì)應(yīng)的Widget的Controller來實(shí)現(xiàn)迅腔。
ListView、GridView的組件控制器是ScrollController靠娱,我們可以通過它來獲取視圖的滾動(dòng)信息沧烈,并且可以調(diào)用里面的方法來更新視圖的滾動(dòng)位置。
另外像云,通常情況下锌雀,我們會(huì)根據(jù)滾動(dòng)的位置來改變一些Widget的狀態(tài)信息,所以ScrollController通常會(huì)和StatefulWidget一起來使用迅诬,并且會(huì)在其中控制它的初始化腋逆、監(jiān)聽、銷毀等事件侈贷。
我們來做一個(gè)案例惩歉,當(dāng)滾動(dòng)到1000位置的時(shí)候,顯示一個(gè)回到頂部的按鈕:
-
jumpTo(double offset)
俏蛮、animateTo(double offset,...)
:這兩個(gè)方法用于跳轉(zhuǎn)到指定的位置撑蚌,它們不同之處在于,后者在跳轉(zhuǎn)時(shí)會(huì)執(zhí)行一個(gè)動(dòng)畫嫁蛇,而前者不會(huì)锨并。 - ScrollController間接繼承自Listenable,我們可以根據(jù)ScrollController來監(jiān)聽滾動(dòng)事件睬棚。
class MyHomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
ScrollController _controller;
bool _isShowTop = false;
@override
void initState() {
// 初始化ScrollController
_controller = ScrollController();
// 監(jiān)聽滾動(dòng)
_controller.addListener(() {
var tempSsShowTop = _controller.offset >= 1000;
if (tempSsShowTop != _isShowTop) {
setState(() {
_isShowTop = tempSsShowTop;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ListView展示"),
),
body: ListView.builder(
itemCount: 100,
itemExtent: 60,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
floatingActionButton: !_isShowTop ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
_controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
},
),
);
}
}
1.2. NotificationListener
如果我們希望監(jiān)聽什么時(shí)候開始滾動(dòng)第煮,什么時(shí)候結(jié)束滾動(dòng)解幼,這個(gè)時(shí)候我們可以通過NotificationListener
。
- NotificationListener是一個(gè)Widget包警,模板參數(shù)T是想監(jiān)聽的通知類型撵摆,如果省略,則所有類型通知都會(huì)被監(jiān)聽害晦,如果指定特定類型特铝,則只有該類型的通知會(huì)被監(jiān)聽。
- NotificationListener需要一個(gè)onNotification回調(diào)函數(shù)壹瘟,用于實(shí)現(xiàn)監(jiān)聽處理邏輯鲫剿。
- 該回調(diào)可以返回一個(gè)布爾值,代表是否阻止該事件繼續(xù)向上冒泡稻轨,如果為
true
時(shí)灵莲,則冒泡終止,事件停止向上傳播殴俱,如果不返回或者返回值為false
時(shí)政冻,則冒泡繼續(xù)。
案例: 列表滾動(dòng), 并且在中間顯示滾動(dòng)進(jìn)度
class MyHomeNotificationDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomeNotificationDemoState();
}
class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> {
int _progress = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification) {
// 1.判斷監(jiān)聽事件的類型
if (notification is ScrollStartNotification) {
print("開始滾動(dòng).....");
} else if (notification is ScrollUpdateNotification) {
// 當(dāng)前滾動(dòng)的位置和總長度
final currentPixel = notification.metrics.pixels;
final totalPixel = notification.metrics.maxScrollExtent;
double progress = currentPixel / totalPixel;
setState(() {
_progress = (progress * 100).toInt();
});
print("正在滾動(dòng):${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
} else if (notification is ScrollEndNotification) {
print("結(jié)束滾動(dòng)....");
}
return false;
},
child: Stack(
alignment: Alignment(.9, .9),
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 60,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
CircleAvatar(
radius: 30,
child: Text("$_progress%"),
backgroundColor: Colors.black54,
)
],
),
);
}
}