在Flutter中,Widget并不是最終渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常這種監(jiān)聽事件以及相關(guān)的信息并不能直接從Widget中獲取,而是必須通過對應(yīng)的Widget的Controller來實(shí)現(xiàn)。
ListView、GridView的組件控制器是ScrollController,我們可以通過它來獲取視圖的滾動(dòng)信息,并且可以調(diào)用里面的方法來更新視圖的滾動(dòng)位置。
1. ScrollController
ScrollController構(gòu)造函數(shù)如下:
ScrollController({
double initialScrollOffset = 0.0, //初始滾動(dòng)位置
this.keepScrollOffset = true,//是否保存滾動(dòng)位置
...
})
ScrollController常用的屬性和方法:
- offset:可滾動(dòng)組件當(dāng)前的滾動(dòng)位置。
- jumpTo(double offset)、animateTo(double offset,...):這兩個(gè)方法用于跳轉(zhuǎn)到指定的位置,它們不同之處在于,后者在跳轉(zhuǎn)時(shí)會執(zhí)行一個(gè)動(dòng)畫,而前者不會。
ScrollController滾動(dòng)監(jiān)聽
ScrollController間接繼承自Listenable,我們可以根據(jù)ScrollController來監(jiān)聽滾動(dòng)事件,如:
controller.addListener(()=>print(controller.offset))
示例
我們創(chuàng)建一個(gè)ListView,當(dāng)滾動(dòng)位置發(fā)生變化時(shí),我們先打印出當(dāng)前滾動(dòng)位置,然后判斷當(dāng)前位置是否超過1000像素,如果超過則在屏幕右下角顯示一個(gè)“返回頂部”的按鈕,該按鈕點(diǎn)擊后可以使ListView恢復(fù)到初始位置;如果沒有超過1000像素,則隱藏“返回頂部”按鈕。
class MSHomePage extends StatefulWidget {
@override
State<MSHomePage> createState() => _MSHomePageState();
}
class _MSHomePageState extends State<MSHomePage> {
ScrollController scr = ScrollController();
bool _showTopBtn = false; //是否顯示“返回到頂部”按鈕
@override
void initState() {
super.initState();
// 監(jiān)聽滾動(dòng)位置,并打印
scr.addListener(() {
print(scr.offset);
bool _curShowBtnState = false;
if (scr.offset < 1000) {
_curShowBtnState = false;
} else {
_curShowBtnState = true;
}
// 只有showTopBtn的值發(fā)生變化時(shí),才刷新
if (_curShowBtnState != _showTopBtn) {
_showTopBtn = _curShowBtnState;
setState(() {});
}
});
}
@override
void dispose() {
//為了避免內(nèi)存泄露,需要調(diào)用src.dispose
scr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("滾動(dòng)組件"),
),
// Scrollbar 滾動(dòng)條
body: Scrollbar(
controller: scr,
child: ListView.builder(
controller: scr,
itemBuilder: (BuildContext ctx, int index) {
return ListTile(
leading: Text("$index"),
);
},
itemCount: 100,
itemExtent: 50, // 列表項(xiàng) 固定高度
),
),
floatingActionButton: _showTopBtn
? FloatingActionButton(
onPressed: () {
// 返回到頂部時(shí)執(zhí)行動(dòng)畫
scr.animateTo(0,
duration: Duration(microseconds: 200),
curve: Curves.easeIn);
},
child: Icon(Icons.arrow_upward),
)
: null,
);
}
}

ScrollPosition
ScrollPosition是用來保存可滾動(dòng)組件的滾動(dòng)位置的。一個(gè)ScrollController對象可以同時(shí)被多個(gè)可滾動(dòng)組件使用,ScrollController會為每一個(gè)可滾動(dòng)組件創(chuàng)建一個(gè)ScrollPosition對象,這些ScrollPosition保存在ScrollController的positions屬性中(List<ScrollPosition>)。ScrollPosition是真正保存滑動(dòng)位置信息的對象,offset只是一個(gè)便捷屬性
double get offset => position.pixels;
一個(gè)ScrollController雖然可以對應(yīng)多個(gè)可滾動(dòng)組件,但是有一些操作,如讀取滾動(dòng)位置offset,則需要一對一!但是我們?nèi)匀豢梢栽谝粚Χ嗟那闆r下,通過其它方法讀取滾動(dòng)位置,舉個(gè)例子,假設(shè)一個(gè)ScrollController同時(shí)被兩個(gè)可滾動(dòng)組件使用,那么我們可以通過如下方式分別讀取他們的滾動(dòng)位置:
...
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
...
我們可以通過controller.positions.length來確定controller被幾個(gè)可滾動(dòng)組件使用。
ScrollPosition的方法
ScrollPosition有兩個(gè)常用方法:animateTo() 和 jumpTo(),它們是真正來控制跳轉(zhuǎn)滾動(dòng)位置的方法,ScrollController的這兩個(gè)同名方法,內(nèi)部最終都會調(diào)用ScrollPosition的。
ScrollController控制原理
我們來介紹一下ScrollController的另外三個(gè)方法:
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition);
void attach(ScrollPosition position) ;
void detach(ScrollPosition position) ;
當(dāng)ScrollController和可滾動(dòng)組件關(guān)聯(lián)時(shí),可滾動(dòng)組件首先會調(diào)用ScrollController的createScrollPosition()方法來創(chuàng)建一個(gè)ScrollPosition來存儲滾動(dòng)位置信息,接著,可滾動(dòng)組件會調(diào)用attach()方法,將創(chuàng)建的ScrollPosition添加到ScrollController的positions屬性中,這一步稱為“注冊位置”,只有注冊后animateTo() 和 jumpTo()才可以被調(diào)用。
當(dāng)可滾動(dòng)組件銷毀時(shí),會調(diào)用ScrollController的detach()方法,將其ScrollPosition對象從ScrollController的positions屬性中移除,這一步稱為“注銷位置”,注銷后animateTo() 和 jumpTo() 將不能再被調(diào)用。
需要注意的是,ScrollController的animateTo() 和 jumpTo()內(nèi)部會調(diào)用所有ScrollPosition的animateTo() 和 jumpTo(),以實(shí)現(xiàn)所有和該ScrollController關(guān)聯(lián)的可滾動(dòng)組件都滾動(dòng)到指定的位置。
2. NotificationListener
Flutter Widget樹中子Widget可以通過發(fā)送通知(Notification)與父(包括祖先)Widget通信。父級組件可以通過NotificationListener組件來監(jiān)聽自己關(guān)注的通知
可滾動(dòng)組件在滾動(dòng)時(shí)會發(fā)送ScrollNotification類型的通知,ScrollBar正是通過監(jiān)聽滾動(dòng)通知來實(shí)現(xiàn)的。通過NotificationListener監(jiān)聽滾動(dòng)事件和通過ScrollController有兩個(gè)主要的不同:
- 通過NotificationListener可以在從可滾動(dòng)組件到widget樹根之間任意位置都能監(jiān)聽。而
ScrollController只能和具體的可滾動(dòng)組件關(guān)聯(lián)后才可以。 - 收到滾動(dòng)事件后獲得的信息不同;
NotificationListener在收到滾動(dòng)事件時(shí),通知中會攜帶當(dāng)前滾動(dòng)位置和ViewPort的一些信息,而ScrollController只能獲取當(dāng)前滾動(dòng)位置。
示例
下面,我們監(jiān)聽ListView的滾動(dòng)通知,然后顯示當(dāng)前滾動(dòng)進(jìn)度百分比:
class MSHomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MSHomePageState();
}
}
class _MSHomePageState extends State<MSHomePage> {
String _progress = "0%";
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification noti) {
if (noti is ScrollStartNotification) {
print("開始滾動(dòng)");
} else if (noti is ScrollUpdateNotification) {
print("正在滾動(dòng)");
} else if (noti is ScrollEndNotification) {
print("結(jié)束滾動(dòng)");
} else {}
double pro = noti.metrics.pixels / noti.metrics.maxScrollExtent;
setState(() {
_progress = "${(pro * 100).toInt()}%";
});
return false;
},
child: Stack(
alignment: AlignmentDirectional.center,
children: [
ListView.builder(
itemCount: 100,
itemExtent: 56,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.people),
title: Text("聯(lián)系人 ${index + 1}"),
trailing: Icon(Icons.delete),
);
},
),
CircleAvatar(
radius: 30,
child: Text(_progress),
backgroundColor: Colors.black54,
),
],
),
);
}
}

在接收到滾動(dòng)事件時(shí),參數(shù)類型為ScrollNotification,它包括一個(gè)metrics屬性,它的類型是ScrollMetrics,該屬性包含當(dāng)前ViewPort及滾動(dòng)位置等信息:
- pixels:當(dāng)前滾動(dòng)位置。
- maxScrollExtent:最大可滾動(dòng)長度。
- extentBefore:滑出ViewPort頂部的長度;此示例中相當(dāng)于頂部滑出屏幕上方的列表長度。
- extentInside:ViewPort內(nèi)部長度;此示例中屏幕顯示的列表部分的長度。
- extentAfter:列表中未滑入ViewPort部分的長度;此示例中列表底部未顯示到屏幕范圍部分的長度。
- atEdge:是否滑到了可滾動(dòng)組件的邊界(此示例中相當(dāng)于列表頂或底部)
https://book.flutterchina.club/chapter6/scroll_controller.html