一,概述
RefreshIndicator是Flutter基於Material設計語言內置的控件,集合了下拉手勢、加載指示器和刷新操作一體,可玩性比FutureBuilder差了一大截,不過大家也用過Material設計語言的其他控件,視覺效果也不賴的。
要實現拉刷新列表的功能僅僅依靠RefreshIndicator還不行,我們還需要ScrollController對ListView的移動偏移量進行監控。
二,兩個重要的組件
- RefreshIndicator
- 構造函數
/** * 下拉刷新組件 *const RefreshIndicator ({ Key key, @required this.child, this.displacement: 40.0, //觸發下拉刷新的距離 @required this.onRefresh, //下拉回調方法,方法需要有async和await關鍵字,沒有await,刷新圖標立馬消失,沒有async,刷新圖標不會消失 this.color, //進度指示器前景色 默認為系統主題色 this.backgroundColor, //背景色 this.notificationPredicate: defaultScrollNotificationPredicate, }) */
- 構造函數
注意:
-
-
- RefreshIndicator的子元素必須是一個可以滾動的控件
- 如果你遇到不符合條件的控件,請將其用可以滾動的控件(如ListView、PageView等)包裝一下
onRefresh
的回調函數必須是Future<Null>
類型
-
- ScrollController
- 構造函數
ScrollController({ double initialScrollOffset = 0.0, //初始滾動位置 this.keepScrollOffset = true,//是否保存滾動位置 ... })
-
屬性和方法
- offset:可滾動Widget當前滾動的位置。
- jumpTo(double offset)、animateTo(double offset,...):這兩個方法用於跳轉到指定的位置,它們不同之處在於,后者在跳轉時會執行一個動畫,而前者不會。
-
滾動監聽(addListener(listener))
ScrollController間接繼承自Listenable,我們可以根據ScrollController來監聽滾動事件。如:
controller.addListener(()=>print(controller.offset))
-
滾動位置恢復(keepScrollOffset,initialScrollOffset)
PageStorage是一個用於保存頁面(路由)相關數據的Widget,它並不會影響子樹的UI外觀,其實,PageStorage是一個功能型Widget,它擁有一個存儲桶(bucket),子樹中的Widget可以通過指定不同的PageStorageKey來存儲各自的數據或狀態。
每次滾動結束,Scrollable Widget都會將滾動位置offset存儲到PageStorage中,當Scrollable Widget 重新創建時再恢復。如果ScrollController.keepScrollOffset為false,則滾動位置將不會被存儲,Scrollable Widget重新創建時會使用ScrollController.initialScrollOffset;ScrollController.keepScrollOffset為true時,Scrollable Widget在第一次創建時,會滾動到initialScrollOffset處,因為這時還沒有存儲過滾動位置。在接下來的滾動中就會存儲、恢復滾動位置,而initialScrollOffset會被忽略。
-
滾動監聽
Flutter Widget樹中子Widget可以通過發送通知(Notification)與父(包括祖先)Widget通信。父Widget可以通過NotificationListener Widget來監聽自己關注的通知,這種通信方式類似於Web開發中瀏覽器的事件冒泡,我們在Flutter中沿用“冒泡”這個術語。Scrollable Widget在滾動時會發送ScrollNotification類型的通知,ScrollBar正是通過監聽滾動通知來實現的。通過NotificationListener監聽滾動事件和通過ScrollController有兩個主要的不同:
- 通過NotificationListener可以在從Scrollable Widget到Widget樹根之間任意位置都能監聽。而ScrollController只能和具體的Scrollable Widget關聯后才可以。
- 收到滾動事件后獲得的信息不同;NotificationListener在收到滾動事件時,通知中會攜帶當前滾動位置和ViewPort的一些信息,而ScrollController只能獲取當前滾動位置。
NotificationListener
NotificationListener是一個Widget,模板參數T是想監聽的通知類型,如果省略,則所有類型通知都會被監聽,如果指定特定類型,則只有該類型的通知會被監聽。NotificationListener需要一個onNotification回調函數,用於實現監聽處理邏輯,該回調可以返回一個布爾值,代表是否阻止該事件繼續向上冒泡,如果為true時,則冒泡終止,事件停止向上傳播,如果不返回或者返回值為false 時,則冒泡繼續。
- 構造函數
-
-
ScrollController控制原理
我們來介紹一下ScrollController的另外三個方法:
ScrollPosition createScrollPosition( ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition ); void attach(ScrollPosition position) ; void detach(ScrollPosition position) ;
當ScrollController和Scrollable Widget關聯時,Scrollable Widget首先會調用ScrollController的createScrollPosition()方法來創建一個ScrollPosition來存儲滾動位置信息,接着,Scrollable Widget會調用attach()方法,將創建的ScrollPosition添加到ScrollController的positions屬性中,這一步稱為“注冊位置”,只有注冊后animateTo() 和 jumpTo()才可以被調用。當Scrollable Widget銷毀時,會調用ScrollController的detach()方法,將其ScrollPosition對象從ScrollController的positions屬性中移除,這一步稱為“注銷位置”,注銷后animateTo() 和 jumpTo() 將不能再被調用。
需要注意的是,ScrollController的animateTo() 和 jumpTo()內部會調用所有ScrollPosition的animateTo() 和 jumpTo(),以實現所有和該ScrollController關聯的Scrollable Widget都滾動到指定的位置。
-
三,下拉加載,上拉刷新實現
class Widget_RefreshIndicator_State extends State<Widget_RefreshIndicator_Page> { var list = []; int page = 0; bool isLoading = false;//是否正在請求新數據 bool showMore = false;//是否顯示底部加載中提示 bool offState = false;//是否顯示進入頁面時的圓形進度條 ScrollController scrollController = ScrollController(); @override void initState() { super.initState(); scrollController.addListener(() { if (scrollController.position.pixels == scrollController.position.maxScrollExtent) { print('滑動到了最底部${scrollController.position.pixels}'); setState(() { showMore = true; }); getMoreData(); } }); getListData(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text("RefreshIndicator"), ), body: Stack( children: <Widget>[ RefreshIndicator( child: ListView.builder( controller: scrollController, itemCount: list.length + 1,//列表長度+底部加載中提示 itemBuilder: choiceItemWidget, ), onRefresh: _onRefresh, ), Offstage( offstage: offState, child: Center( child: CircularProgressIndicator(), ), ), ], ) ), ); } @override void dispose() { super.dispose(); //手動停止滑動監聽 scrollController.dispose(); } /** * 加載哪個子組件 */ Widget choiceItemWidget(BuildContext context, int position) { if (position < list.length) { return HomeListItem(position, list[position], (position) { debugPrint("點擊了第$position條"); }); } else if (showMore) { return showMoreLoadingWidget(); }else{ return null; } } /** * 加載更多提示組件 */ Widget showMoreLoadingWidget() { return Container( height: 50.0, child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text('加載中...', style: TextStyle(fontSize: 16.0),), ], ), ); } /** * 模擬進入頁面獲取數據 */ void getListData() async { if (isLoading) { return; } setState(() { isLoading = true; }); await Future.delayed(Duration(seconds: 3), () { setState(() { isLoading = false; offState = true; list = List.generate(20, (i) { return ItemInfo("ListView的一行數據$i"); }); }); }); } /** * 模擬到底部加載更多數據 */ void getMoreData() async { if (isLoading) { return; } setState(() { isLoading = true; page++; }); print('上拉刷新開始,page = $page'); await Future.delayed(Duration(seconds: 3), () { setState(() { isLoading = false; showMore = false; list.addAll(List.generate(3, (i) { return ItemInfo("上拉添加ListView的一行數據$i"); })); print('上拉刷新結束,page = $page'); }); }); } /** * 模擬下拉刷新 */ Future < void > _onRefresh() async { if (isLoading) { return; } setState(() { isLoading = true; page = 0; }); print('下拉刷新開始,page = $page'); await Future.delayed(Duration(seconds: 3), () { setState(() { isLoading = false; List tempList = List.generate(3, (i) { return ItemInfo("下拉添加ListView的一行數據$i"); }); tempList.addAll(list); list = tempList; print('下拉刷新結束,page = $page'); }); }); } }