前言
可以用ScrollController來控制可滾動組件的滾動位置。
接口描述
ScrollController({
// 初始滾動位置
double initialScrollOffset = 0.0,
// 是否保持滾動位置
this.keepScrollOffset = true,
this.debugLabel,
})
代碼示例
// ScrollController
// 可以用ScrollController來控制可滾動組件的滾動位置。
import 'package:flutter/material.dart';
class ScrollControllerTest extends StatefulWidget{
@override
ScrollControllerTestState createState() => ScrollControllerTestState();
}
class ScrollControllerTestState extends State<ScrollControllerTest>{
ScrollController _controller = ScrollController();
// 是否顯示“返回到頂部”按鈕
bool showToTopBtn = false;
@override
void initState(){
super.initState();
// 監聽滾動事件,打印滾動位置
_controller.addListener((){
// 打印滾動位置
print(_controller.offset);
if(_controller.offset < 1000 && showToTopBtn){
setState(() {
showToTopBtn = false;
});
} else if(_controller.offset >= 1000 && showToTopBtn == false){
setState(() {
showToTopBtn = true;
});
}
});
}
@override
void dispose(){
// 為了避免內存泄漏,需要調用_controller.dispose
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text('滾動控制1'),
),
body: Scrollbar(
child: ListView.builder(
itemCount: 100,
itemExtent: 50.0,
controller: _controller,
itemBuilder: (context, index){
return ListTile(title: Text("$index"),);
},
),
),
floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: (){
// 返回到頂部時執行動畫
_controller.animateTo(
.0,
// 返回頂部的過程中執行一個滾動動畫,動畫時間是200毫秒,動畫曲線是Curves.ease
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
},
),
);
}
}
// 滾動監聽
class ScrollNotificationTest extends StatefulWidget{
@override
_ScrollNotificationTestState createState() => _ScrollNotificationTestState();
}
class _ScrollNotificationTestState extends State<ScrollNotificationTest>{
//保持進度百分比
String _progress = '0%';
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text('滾動控制2'),
),
// 進度條
body: Scrollbar(
// 監聽滾動通知
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification){
// pixels:當前滾動位置;maxScrollExtent:最大可滾動長度
double progress = notification.metrics.pixels /
notification.metrics.maxScrollExtent;
// 重新構建
setState(() {
_progress = '${(progress * 100).toInt()}%';
});
// extentBefore:滑出ViewPort頂部的長度;此示例中相當於頂部滑出屏幕上方的列表長度。
// extentInside:ViewPort內部長度;此示例中屏幕顯示的列表部分的長度。
// extentAfter:列表中未滑入ViewPort部分的長度;此示例中列表底部未顯示到屏幕范圍部分的長度。
// atEdge:是否滑到了可滾動組件的邊界(此示例中相當於列表頂或底部)。
print('BottomEdge: ${notification.metrics.extentAfter == 0}');
//
return true;
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (context, index){
return ListTile(title: Text('$index'),);
},
),
// 顯示百分比
CircleAvatar(
radius: 30.0,
child: Text(_progress),
backgroundColor: Colors.black54,
)
],
),
),
),
);
}
}
總結
滾動位置恢復
PageStorage是一個用於保存頁面(路由)相關數據的組件,它並不會影響子樹的UI外觀,其實,PageStorage是一個功能型組件,它擁有一個存儲桶(bucket),子樹中的Widget可以通過指定不同的PageStorageKey來存儲各自的數據或狀態。
每次滾動結束,可滾動組件都會將滾動位置offset存儲到PageStorage中,當可滾動組件重新創建時再恢復。如果ScrollController.keepScrollOffset為false,則滾動位置將不會被存儲,可滾動組件重新創建時會使用ScrollController.initialScrollOffset;ScrollController.keepScrollOffset為true時,可滾動組件在第一次創建時,會滾動到initialScrollOffset處,因為這時還沒有存儲過滾動位置。在接下來的滾動中就會存儲、恢復滾動位置,而initialScrollOffset會被忽略。
當一個路由中包含多個可滾動組件時,如果你發現在進行一些跳轉或切換操作后,滾動位置不能正確恢復,這時你可以通過顯式指定PageStorageKey來分別跟蹤不同的可滾動組件的位置。
ScrollPosition
ScrollPosition是用來保存可滾動組件的滾動位置的。一個ScrollController對象可以同時被多個可滾動組件使用,ScrollController會為每一個可滾動組件創建一個ScrollPosition對象,這些ScrollPosition保存在ScrollController的positions屬性中(List
一個ScrollController雖然可以對應多個可滾動組件,但是有一些操作,如讀取滾動位置offset,則需要一對一!但是我們仍然可以在一對多的情況下,通過其它方法讀取滾動位置。ScrollPosition有兩個常用方法:animateTo() 和 jumpTo(),它們是真正來控制跳轉滾動位置的方法,ScrollController的這兩個同名方法,內部最終都會調用ScrollPosition的。
ScrollController控制原理
ScrollController的另外三個方法:
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition);
void attach(ScrollPosition position) ;
void detach(ScrollPosition position) ;
當ScrollController和可滾動組件關聯時,可滾動組件首先會調用ScrollController的createScrollPosition()方法來創建一個ScrollPosition來存儲滾動位置信息,接着,可滾動組件會調用attach()方法,將創建的ScrollPosition添加到ScrollController的positions屬性中,這一步稱為“注冊位置”,只有注冊后animateTo() 和 jumpTo()才可以被調用。
當可滾動組件銷毀時,會調用ScrollController的detach()方法,將其ScrollPosition對象從ScrollController的positions屬性中移除,這一步稱為“注銷位置”,注銷后animateTo() 和 jumpTo() 將不能再被調用。
需要注意的是,ScrollController的animateTo() 和 jumpTo()內部會調用所有ScrollPosition的animateTo() 和 jumpTo(),以實現所有和該ScrollController關聯的可滾動組件都滾動到指定的位置。
滾動監聽
Flutter Widget樹中子Widget可以通過發送通知(Notification)與父(包括祖先)Widget通信。父級組件可以通過NotificationListener組件來監聽自己關注的通知,這種通信方式類似於Web開發中瀏覽器的事件冒泡。
可滾動組件在滾動時會發送ScrollNotification類型的通知,ScrollBar正是通過監聽滾動通知來實現的。通過NotificationListener監聽滾動事件和通過ScrollController有兩個主要的不同:
- 通過NotificationListener可以在從可滾動組件到widget樹根之間任意位置都能監聽。而ScrollController只能和具體的可滾動組件關聯后才可以。
- 收到滾動事件后獲得的信息不同;NotificationListener在收到滾動事件時,通知中會攜帶當前滾動位置和ViewPort的一些信息,而ScrollController只能獲取當前滾動位置。
