本文主要介紹了 Flutter 監聽滾動控制元件的滾動事件和一些滾動控制器的用法,最後實現 appBar 滾動漸變,如有不當之處敬請指正。
閱讀本文大約需要 5 分鐘
介紹
在 Flutter 中滾動監聽一般可以採用兩種方式來實現,分別是 ScrollController
和 NotificationListener
這兩種方式。
ScrollController介紹
ScrollController
介紹一下ScrollController
常用的屬性和方法:
offset
:可滾動元件當前的滾動位置。jumpTo(double offset)
跳轉到指定位置,offset
為滾動偏移量。animateTo(double offset,@required Duration duration,@required Curve curve)
同jumpTo(double offset)
一樣,不同的是animateTo
跳轉時會執行一個動畫,需要傳入執行動畫需要的時間和動畫曲線。
ScrollPosition
ScrollPosition是用來儲存可滾動元件的滾動位置的。一個 ScrollController 物件可能會被多個可滾動的元件使用,
ScrollController 會為每一個滾動元件建立一個 ScrollPosition 物件來儲存位置資訊。ScrollPosition 中儲存的是在 ScrollController 的 positions 屬性裡面,他是一個 List<ScrollPosition>
陣列,在 ScrollController 中真正儲存位置資訊的就是 ScrollPosition,而 offset 只是一個便捷使用的屬性。檢視原始碼中可以發現 offset 獲取就是從 ScrollPosition 中獲取的。
/// Returns the attached [ScrollPosition], from which the actual scroll offset
/// of the [ScrollView] can be obtained.
/// Calling this is only valid when only a single position is attached.
ScrollPosition get position {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
return _positions.single;
}
/// The current scroll offset of the scrollable widget.
/// Requires the controller to be controlling exactly one scrollable widget.
double get offset => position.pixels;
複製程式碼
一個 ScrollController
雖然可以對應多個可滾動元件,但是讀取滾動位置 offset
,則需要一對一讀取。在一對多的情況下,我們可以使用其他方法來實現讀取滾動位置。假設現在一個 ScrollController
對應了兩個可以滾動的元件,那麼可以通過 position.elementAt(index)
來獲取 ScrollPosition
,從而獲得 offset
:
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
複製程式碼
ScrollPosition的方法
ScrollPosition
有兩個常用方法:分別是 animateTo()
和 jumpTo()
,他們才是真正控制跳轉到滾動位置的方法,在 ScrollController 中這兩個同名方法,內部最終都會呼叫 ScrollPosition 這兩個方法。
Future<void> animateTo(
double offset, {
@required Duration duration,
@required Curve curve,
}) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
final List<Future<void>> animations = List<Future<void>>(_positions.length);
for (int i = 0; i < _positions.length; i += 1)
// 呼叫 ScrollPosition 中的 animateTo 方法
animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve);
return Future.wait<void>(animations).then<void>((List<void> _) => null);
}
複製程式碼
ScrollController控制原理
ScrollController
還有其他比較重要的三個方法:
createScrollPosition
:當ScrollController
和可滾動元件關聯時,可滾動元件首先會調ScrollController
的createScrollPosition
方法來建立一個ScrollPosition
來儲存滾動位置資訊。
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition);
複製程式碼
- 在滾動元件呼叫
createScrollPosition
方法之後,接著會呼叫attach
方法來將建立號的ScrollPosition
資訊新增到positions
屬性中,這一步稱為“註冊位置”,只有註冊後animateTo()
和jumpTo()
才可以被呼叫。
void attach(ScrollPosition position);
複製程式碼
- 最後當可滾動元件被銷燬時,會呼叫
detach()
方法,將其ScrollPosition
物件從ScrollController
的positions
屬性中移除,這一步稱為“登出位置”,登出後animateTo()
和jumpTo()
將不能再被呼叫。
void detach(ScrollPosition position);
複製程式碼
NotificationListener介紹
通知冒泡
Flutter Widget 樹中子 Widge t可以通過傳送通知(Notification)與父(包括祖先) Widget 進行通訊,父級元件可以通過 NotificationListener
元件來監聽自己關注的通知,這種通訊方式類似於 Web 開發中瀏覽器的事件冒泡,在 Flutter 中就沿用了“冒泡”這個術語,稱為通知冒泡
通知冒泡和使用者觸控事件冒泡是相似的,但有一點不同:通知冒泡可以中止,但使用者觸控事件不行。
滾動通知
Flutter 中很多地方使用了通知,如可滾動元件(Scrollable Widget)滑動時就會分發滾動通知(ScrollNotification),而 Scrollbar
正是通過監聽 ScrollNotification
來確定滾動條位置的。
switch (notification.runtimeType){
case ScrollStartNotification: print("開始滾動"); break;
case ScrollUpdateNotification: print("正在滾動"); break;
case ScrollEndNotification: print("滾動停止"); break;
case OverscrollNotification: print("滾動到邊界"); break;
}
複製程式碼
其中 ScrollStartNotification
和 ScrollUpdateNotification
等都是繼承 ScrollNotification
類的,不同型別的通知子類會包含不同的資訊,ScrollUpdateNotification
有一個 scrollDelta
屬性,它記錄了移動的位移。
NotificationListener
時繼承 StatelessWidget
類的額,左右我們可以直接在放置在Widget 數中,通過裡面的 onNotification
可以指定一個模板引數,該模板引數型別必須是繼承自Notification
,可以顯式指定模板引數時,比如通知的型別為滾動結束通知:
NotificationListener<ScrollEndNotification>
複製程式碼
這個時候 NotificationListener
便只會接收該引數型別的通知。
onNotification
回撥為通知處理回撥,他的返回值時布林型別(bool),當返回值為 true
時,阻止冒泡,其父級 Widget 將再也收不到該通知;當返回值為 false
時繼續向上冒泡通知。
兩者區別
首先這兩種方式都可以實現對滾動的監聽,但是他們還是有一些區別:
ScrollController
可以控制滾動控制元件的滾動,而NotificationListener
是不可以的。- 通過
NotificationListener
可以在從可滾動元件到widget樹根之間任意位置都能監聽,而ScrollController
只能和具體的可滾動元件關聯後才可以。 - 收到滾動事件後獲得的資訊不同;
NotificationListener
在收到滾動事件時,通知中會攜帶當前滾動位置和ViewPort的一些資訊,而ScrollController
只能獲取當前滾動位置。
ScrollController例項
效果圖
程式碼實現步驟
-
建立滾動所需的介面,一個
Scaffold
元件body
裡面方式一個Stack
的層疊小部件,裡面放置一個listview
,和自定義的appBar
;floatingActionButton
放置一個返回頂部的懸浮按鈕。Scaffold( body: Stack( children: <Widget>[ MediaQuery.removePadding( removeTop: true, context: context, child: ListView.builder( // ScrollController 關聯滾動元件 controller: _controller, itemCount: 100, itemBuilder: (context, index) { if (index == 0) { return Container( height: 200, child: Swiper( itemBuilder: (BuildContext context, int index) { return new Image.network( "http://via.placeholder.com/350x150", fit: BoxFit.fill, ); }, itemCount: 3, autoplay: true, pagination: new SwiperPagination(), ), ); } return ListTile( title: Text("ListTile:$index"), ); }, ), ), Opacity( opacity: toolbarOpacity, child: Container( height: 98, color: Colors.blue, child: Padding( padding: const EdgeInsets.only(top: 30.0), child: Center( child: Text( "ScrollerDemo", style: TextStyle(color: Colors.white, fontSize: 20.0), ), ), ), ), ) ], ), floatingActionButton: !showToTopBtn ? null : FloatingActionButton( child: Icon(Icons.keyboard_arrow_up), onPressed: () { _controller.animateTo(0.0, duration: Duration(milliseconds: 500), curve: Curves.ease); }, ), ) 複製程式碼
-
建立
ScrollController
物件,在初始化中新增對滾動的監聽,並和ListView
這個可滾動小部件進行關聯:
ScrollController _controller = new ScrollController();
@override
void initState() {
_controller.addListener(() {
print(_controller.offset); //列印滾動位置
})
}
複製程式碼
- 在 _controller.addListener 中新增相關業務程式碼,根據滾動的偏移量計算出透明度,實現appBar滾動漸變:
double t = _controller.offset / DEFAULT_SCROLLER;
if (t < 0.0) {
t = 0.0;
} else if (t > 1.0) {
t = 1.0;
}
setState(() {
toolbarOpacity = t;
});
複製程式碼
-
更具滾動的高度和當前
floatingActionButton
的現實狀態,判斷floatingActionButton
是否需要展示:if(_controller.offset < DEFAULT_SHOW_TOP_BTN && showToTopBtn){ setState(() { showToTopBtn = false; }); }else if(_controller.offset >= DEFAULT_SHOW_TOP_BTN && !showToTopBtn){ setState(() { showToTopBtn = true; }); } 複製程式碼
-
點選
floatingActionButton
返回到頂部:
_controller.animateTo(0.0, duration: Duration(milliseconds: 500), curve: Curves.ease);
複製程式碼
完整程式碼請參考下方GitHub專案中
/demo/scroller_demo.dart
檔案。
NotificationListener例項
效果圖
程式碼實現步驟
在 NotificationListener 例項中佈局基本上和 ScrollController 一致,不同的地方在於 ListView 需要包裹在 NotificationListener 中作為 child,然後 NotificationListener 在 onNotification 中判斷滾動偏移量:
if (notification is ScrollUpdateNotification && notification.depth == 0) {
double t = notification.metrics.pixels / DEFAULT_SCROLLER;
if (t < 0.0) {
t = 0.0;
} else if (t > 1.0) {
t = 1.0;
}
setState(() {
toolbarOpacity = t;
});
print(notification.metrics.pixels); //列印滾動位置
}
複製程式碼
完整程式碼請參考下方GitHub專案中
/demo/notification_listener_demo.dart
檔案
結尾
完整程式碼奉上GitHub地址:fluter_demo ,歡迎star和fork。
到此,本文就結束了,如有不當之處敬請指正,一起學習探討,謝謝?。