当前位置:网站首页>Flutter 小技巧之 ListView 和 PageView 的各種花式嵌套
Flutter 小技巧之 ListView 和 PageView 的各種花式嵌套
2022-07-04 09:09:00 【GSYTech】
這次的 Flutter 小技巧是 ListView
和 PageView
的花式嵌套,不同 Scrollable
的嵌套沖突問題相信大家不會陌生,今天就通過 ListView
和 PageView
的三種嵌套模式帶大家收獲一些不一樣的小技巧。
正常嵌套
最常見的嵌套應該就是橫向 PageView
加縱向 ListView
的組合,一般情况下這個組合不會有什麼問題,除非你硬是要斜著滑。
最近剛好遇到好幾個人同時在問:“斜滑 ListView
容易切換到 PageView
滑動” 的問題,如下 GIF 所示,當用戶在滑動 ListView
時,滑動角度帶上傾斜之後,可能就會導致滑動的是 PageView
而不是 ListView
。
雖然從我個人體驗上並不覺得這是個問題,但是如果產品硬是要你修改,難道要自己重寫 PageView
的手勢響應嗎?
我們簡單看一下,不管是 PageView
還是 ListView
它們的滑動效果都來自於 Scrollable
,而 Scrollable
內部針對不同方向的響應,是通過 RawGestureDetector
完成:
VerticalDragGestureRecognizer
處理垂直方向的手勢HorizontalDragGestureRecognizer
處理水平方向的手勢
所以簡單看它們響應的判斷邏輯,可以看到一個很有趣的方法 computeHitSlop
: 根據 pointer 的類型確定當然命中需要的最小像素,觸摸默認是 kTouchSlop (18.0)。
看到這你有沒有靈光一閃:如果我們把 PageView
的 touchSlop 修改了,是不是就可以調整它響應的靈敏度? 恰好在 computeHitSlop
方法裏,它可以通過 DeviceGestureSettings
來配置,而 DeviceGestureSettings
來自於 MediaQuery
,所以如下代碼所示:
body: MediaQuery( ///調高 touchSlop 到 50 ,這樣 pageview 滑動可能有點點影響, ///但是大概率處理了斜著滑動觸發的問題 data: MediaQuery.of(context).copyWith( gestureSettings: DeviceGestureSettings( touchSlop: 50, )), child: PageView( scrollDirection: Axis.horizontal, pageSnapping: true, children: [ HandlerListView(), HandlerListView(), ], ),),
小技巧一:通過嵌套一個 MediaQuery
,然後調整 gestureSettings
的 touchSlop
從而修改 PageView
的靈明度 ,另外不要忘記,還需要把 ListView
的 touchSlop
切換會默認 的 kTouchSlop
:
class HandlerListView extends StatefulWidget { @override _MyListViewState createState() => _MyListViewState();}class _MyListViewState extends State<HandlerListView> { @override Widget build(BuildContext context) { return MediaQuery( ///這裏 touchSlop 需要調回默認 data: MediaQuery.of(context).copyWith( gestureSettings: DeviceGestureSettings( touchSlop: kTouchSlop, )), child: ListView.separated( itemCount: 15, itemBuilder: (context, index) { return ListTile( title: Text('Item $index'), ); }, separatorBuilder: (context, index) { return const Divider( thickness: 3, ); }, ), ); }}
最後我們看一下效果,如下 GIF 所示,現在就算你斜著滑動,也很觸發 PageView
的水平滑動,只有橫向移動時才會觸發 PageView
的手勢,當然, 如果要說這個粗暴的寫法有什麼問題的話,大概就是降低了 PageView
響應的靈敏度。
同方向 PageView 嵌套 ListView
介紹完常規使用,接著來點不一樣的,在垂直切換的 PageView
裏嵌套垂直滾動的 ListView
, 你第一感覺是不是覺得不靠譜,為什麼會有這樣的場景?
對於產品來說,他們不會考慮你如何實現的問題,他們只會拍著腦袋說淘寶可以,為什麼你不行,所以如果是你,你會怎麼做?
而關於這個需求,社區目前討論的結果是:把 PageView
和 ListView
的滑動禁用,然後通過 RawGestureDetector
自己管理。
如果對實現邏輯分析沒興趣,可以直接看本小節末尾的 源碼鏈接 。
看到自己管理先不要慌,雖然要自己實現 PageView
和 ListView
的手勢分發,但是其實並不需要重寫 PageView
和 ListView
,我們可以複用它們的 Darg
響應邏輯,如下代碼所示:
- 通過
NeverScrollableScrollPhysics
禁止了PageView
和ListView
的滾動效果 - 通過頂部
RawGestureDetector
的VerticalDragGestureRecognizer
自己管理手勢事件 - 配置
PageController
和ScrollController
用於獲取狀態
body: RawGestureDetector( gestures: <Type, GestureRecognizerFactory>{ VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< VerticalDragGestureRecognizer>( () => VerticalDragGestureRecognizer(), (VerticalDragGestureRecognizer instance) { instance ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd ..onCancel = _handleDragCancel; }) }, behavior: HitTestBehavior.opaque, child: PageView( controller: _pageController, scrollDirection: Axis.vertical, ///屏蔽默認的滑動響應 physics: const NeverScrollableScrollPhysics(), children: [ ListView.builder( controller: _listScrollController, ///屏蔽默認的滑動響應 physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return ListTile(title: Text('List Item $index')); }, itemCount: 30, ), Container( color: Colors.green, child: Center( child: Text( 'Page View', style: TextStyle(fontSize: 50), ), ), ) ], ),),
接著我們看 _handleDragStart
實現,如下代碼所示,在產生手勢 details
時,我們主要判斷:
- 通過
ScrollController
判斷ListView
是否可見 - 判斷觸摸比特置是否在
ListIView
範圍內 - 根據狀態判斷通過哪個
Controller
去生產Drag
對象,用於響應後續的滑動事件
void _handleDragStart(DragStartDetails details) { ///先判斷 Listview 是否可見或者可以調用 ///一般不可見時 hasClients false ,因為 PageView 也沒有 keepAlive if (_listScrollController?.hasClients == true && _listScrollController?.position.context.storageContext != null) { ///獲取 ListView 的 renderBox final RenderBox? renderBox = _listScrollController ?.position.context.storageContext .findRenderObject() as RenderBox; ///判斷觸摸的比特置是否在 ListView 內 ///不在範圍內一般是因為 ListView 已經滑動上去了,坐標比特置和觸摸比特置不一致 if (renderBox?.paintBounds .shift(renderBox.localToGlobal(Offset.zero)) .contains(details.globalPosition) == true) { _activeScrollController = _listScrollController; _drag = _activeScrollController?.position.drag(details, _disposeDrag); return; } } ///這時候就可以認為是 PageView 需要滑動 _activeScrollController = _pageController; _drag = _pageController?.position.drag(details, _disposeDrag); }
前面我們主要在觸摸開始時,判斷需要響應的對象時ListView
還是 PageView
,然後通過 _activeScrollController
保存當然響應對象,並且通過 Controller 生成用於響應手勢信息的 Drag
對象。
簡單說:滑動事件發生時,默認會建立一個
Drag
用於處理後續的滑動事件,Drag
會對原始事件進行加工之後再給到ScrollPosition
去觸發後續滑動效果。
接著在 _handleDragUpdate
方法裏,主要是判斷響應是不是需要切換到 PageView
:
- 如果不需要就繼續用前面得到的
_drag?.update(details)
響應ListView
滾動 - 如果需要就通過
_pageController
切換新的_drag
對象用於響應
void _handleDragUpdate(DragUpdateDetails details) { if (_activeScrollController == _listScrollController && ///手指向上移動,也就是快要顯示出底部 PageView details.primaryDelta! < 0 && ///到了底部,切換到 PageView _activeScrollController?.position.pixels == _activeScrollController?.position.maxScrollExtent) { ///切換相應的控制器 _activeScrollController = _pageController; _drag?.cancel(); ///參考 Scrollable 裏 ///因為是切換控制器,也就是要更新 Drag ///拖拽流程要切換到 PageView 裏,所以需要 DragStartDetails ///所以需要把 DragUpdateDetails 變成 DragStartDetails ///提取出 PageView 裏的 Drag 相應 details _drag = _pageController?.position.drag( DragStartDetails( globalPosition: details.globalPosition, localPosition: details.localPosition), _disposeDrag); } _drag?.update(details);}
這裏有個小知識點:如上代碼所示,我們可以簡單通過
details.primaryDelta
判斷滑動方向和移動的是否是主軸
最後如下 GIF 所示,可以看到 PageView
嵌套 ListView
同方向滑動可以正常運行了,但是目前還有個兩個小問題,從圖示可以看到:
- 在切換之後
ListView
的比特置沒有保存下來 - 產品要求去除
ListView
的邊緣溢出效果
所以我們需要對 ListView
做一個 KeepAlive ,然後用簡單的方法去除 Android 邊緣滑動的 Material 效果:
- 通過
with AutomaticKeepAliveClientMixin
讓ListView
在切換之後也保持滑動比特置 - 通過
ScrollConfiguration.of(context).copyWith(overscroll: false)
快速去除 Scrollable 的邊緣 Material 效果
child: PageView( controller: _pageController, scrollDirection: Axis.vertical, ///去掉 Android 上默認的邊緣拖拽效果 scrollBehavior: ScrollConfiguration.of(context).copyWith(overscroll: false),///對 PageView 裏的 ListView 做 KeepAlive 記住比特置class KeepAliveListView extends StatefulWidget { final ScrollController? listScrollController; final int itemCount; KeepAliveListView({ required this.listScrollController, required this.itemCount, }); @override KeepAliveListViewState createState() => KeepAliveListViewState();}class KeepAliveListViewState extends State<KeepAliveListView> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); return ListView.builder( controller: widget.listScrollController, ///屏蔽默認的滑動響應 physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return ListTile(title: Text('List Item $index')); }, itemCount: widget.itemCount, ); } @override bool get wantKeepAlive => true;}
所以這裏我們有解鎖了另外一個小技巧:通過 ScrollConfiguration.of(context).copyWith(overscroll: false)
快速去除 Android 滑動到邊緣的 Material 2效果,為什麼說 Material2, 因為 Material3 上變了,具體可見: Flutter 3 下的 ThemeExtensions 和 Material3 。
同方向 ListView 嵌套 PageView
那還有沒有更非常規的?答案是肯定的,畢竟產品的小腦袋,怎麼會想不到在垂直滑動的 ListView
裏嵌套垂直切換的 PageView
這種需求。
有了前面的思路,其實實現這個邏輯也是异曲同工:把 PageView
和 ListView
的滑動禁用,然後通過 RawGestureDetector
自己管理,不同的就是手勢方法分發的差异。
RawGestureDetector( gestures: <Type, GestureRecognizerFactory>{ VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< VerticalDragGestureRecognizer>( () => VerticalDragGestureRecognizer(), (VerticalDragGestureRecognizer instance) { instance ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd ..onCancel = _handleDragCancel; }) }, behavior: HitTestBehavior.opaque, child: ListView.builder( ///屏蔽默認的滑動響應 physics: NeverScrollableScrollPhysics(), controller: _listScrollController, itemCount: 5, itemBuilder: (context, index) { if (index == 0) { return Container( height: 300, child: KeepAlivePageView( pageController: _pageController, itemCount: itemCount, ), ); } return Container( height: 300, color: Colors.greenAccent, child: Center( child: Text( "Item $index", style: TextStyle(fontSize: 40, color: Colors.blue), ), )); }), )
同樣是在 _handleDragStart
方法裏,這裏首先需要判斷:
ListView
如果已經滑動過,就不響應頂部PageView
的事件- 如果此時
ListView
處於頂部未滑動,判斷手勢比特置是否在PageView
裏,如果是響應PageView
的事件
void _handleDragStart(DragStartDetails details) { ///只要不是頂部,就不響應 PageView 的滑動 ///所以這個判斷只支持垂直 PageView 在 ListView 的頂部 if (_listScrollController.offset > 0) { _activeScrollController = _listScrollController; _drag = _listScrollController.position.drag(details, _disposeDrag); return; } ///此時處於 ListView 的頂部 if (_pageController.hasClients) { ///獲取 PageView final RenderBox renderBox = _pageController.position.context.storageContext.findRenderObject() as RenderBox; ///判斷觸摸範圍是不是在 PageView final isDragPageView = renderBox.paintBounds .shift(renderBox.localToGlobal(Offset.zero)) .contains(details.globalPosition); ///如果在 PageView 裏就切換到 PageView if (isDragPageView) { _activeScrollController = _pageController; _drag = _activeScrollController.position.drag(details, _disposeDrag); return; } } ///不在 PageView 裏就繼續響應 ListView _activeScrollController = _listScrollController; _drag = _listScrollController.position.drag(details, _disposeDrag); }
接著在 _handleDragUpdate
方法裏,判斷如果 PageView
已經滑動到最後一頁,也將滑動事件切換到 ListView
void _handleDragUpdate(DragUpdateDetails details) { var scrollDirection = _activeScrollController.position.userScrollDirection; ///判斷此時響應的如果還是 _pageController,是不是到了最後一頁 if (_activeScrollController == _pageController && scrollDirection == ScrollDirection.reverse && ///是不是到最後一頁了,到最後一頁就切換回 pageController (_pageController.page != null && _pageController.page! >= (itemCount - 1))) { ///切換回 ListView _activeScrollController = _listScrollController; _drag?.cancel(); _drag = _listScrollController.position.drag( DragStartDetails( globalPosition: details.globalPosition, localPosition: details.localPosition), _disposeDrag); } _drag?.update(details);}
當然,同樣還有 KeepAlive 和去除列錶 Material 邊緣效果,最後運行效果如下 GIF 所示。
最後再補充一個小技巧:如果你需要 Flutter 打印手勢競技的過程,可以配置 debugPrintGestureArenaDiagnostics = true;
來讓 Flutter 輸出手勢競技的處理過程。
import 'package:flutter/gestures.dart';void main() { debugPrintGestureArenaDiagnostics = true; runApp(MyApp());}
最後
最後總結一下,本篇介紹了如何通過 Darg
解决各種因為嵌套而導致的手勢沖突,相信大家也知道了如何利用 Controller
和 Darg
來快速自定義一些滑動需求,例如 ListView
聯動 ListView
的差量滑動效果:
///listView 聯動 listViewclass ListViewLinkListView extends StatefulWidget { @override _ListViewLinkListViewState createState() => _ListViewLinkListViewState();}class _ListViewLinkListViewState extends State<ListViewLinkListView> { ScrollController _primaryScrollController = ScrollController(); ScrollController _subScrollController = ScrollController(); Drag? _primaryDrag; Drag? _subDrag; @override void initState() { super.initState(); } @override void dispose() { _primaryScrollController.dispose(); _subScrollController.dispose(); super.dispose(); } void _handleDragStart(DragStartDetails details) { _primaryDrag = _primaryScrollController.position.drag(details, _disposePrimaryDrag); _subDrag = _subScrollController.position.drag(details, _disposeSubDrag); } void _handleDragUpdate(DragUpdateDetails details) { _primaryDrag?.update(details); ///除以10實現差量效果 _subDrag?.update(DragUpdateDetails( sourceTimeStamp: details.sourceTimeStamp, delta: details.delta / 30, primaryDelta: (details.primaryDelta ?? 0) / 30, globalPosition: details.globalPosition, localPosition: details.localPosition)); } void _handleDragEnd(DragEndDetails details) { _primaryDrag?.end(details); _subDrag?.end(details); } void _handleDragCancel() { _primaryDrag?.cancel(); _subDrag?.cancel(); } void _disposePrimaryDrag() { _primaryDrag = null; } void _disposeSubDrag() { _subDrag = null; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("ListViewLinkListView"), ), body: RawGestureDetector( gestures: <Type, GestureRecognizerFactory>{ VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< VerticalDragGestureRecognizer>( () => VerticalDragGestureRecognizer(), (VerticalDragGestureRecognizer instance) { instance ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd ..onCancel = _handleDragCancel; }) }, behavior: HitTestBehavior.opaque, child: ScrollConfiguration( ///去掉 Android 上默認的邊緣拖拽效果 behavior: ScrollConfiguration.of(context).copyWith(overscroll: false), child: Row( children: [ new Expanded( child: ListView.builder( ///屏蔽默認的滑動響應 physics: NeverScrollableScrollPhysics(), controller: _primaryScrollController, itemCount: 55, itemBuilder: (context, index) { return Container( height: 300, color: Colors.greenAccent, child: Center( child: Text( "Item $index", style: TextStyle( fontSize: 40, color: Colors.blue), ), )); })), new SizedBox( width: 5, ), new Expanded( child: ListView.builder( ///屏蔽默認的滑動響應 physics: NeverScrollableScrollPhysics(), controller: _subScrollController, itemCount: 55, itemBuilder: (context, index) { return Container( height: 300, color: Colors.deepOrange, child: Center( child: Text( "Item $index", style: TextStyle(fontSize: 40, color: Colors.white), ), ), ); }), ), ], ), ), )); }}
边栏推荐
- Live in a dream, only do things you don't say
- If you can quickly generate a dictionary from two lists
- C語言-入門-基礎-語法-[運算符,類型轉換](六)
- Jianzhi offer 09 realizes queue with two stacks
- Awk from getting started to digging in (9) circular statement
- Global and Chinese trisodium bicarbonate operation mode and future development forecast report Ⓢ 2022 ~ 2027
- C语言-入门-基础-语法-数据类型(四)
- 地平线 旭日X3 PI (一)首次开机细节
- Talk about single case mode
- C language - Introduction - Foundation - syntax - [identifier, keyword, semicolon, space, comment, input and output] (III)
猜你喜欢
AMLOGIC gsensor debugging
地平线 旭日X3 PI (一)首次开机细节
HMS core helps baby bus show high-quality children's digital content to global developers
High order phase difference such as smear caused by myopic surgery
Mantis creates users without password options
After unplugging the network cable, does the original TCP connection still exist?
CLion-控制台输出中文乱码
微服務入門:Gateway網關
ArcGIS应用(二十二)Arcmap加载激光雷达las格式数据
Codeforces Round #750 (Div. 2)(A,B,C,D,F1)
随机推荐
AI Winter Olympics | is the future coming? Enter the entrance of the meta universe - virtual digital human
Investment analysis and prospect prediction report of global and Chinese high purity tin oxide Market Ⓞ 2022 ~ 2027
"How to connect the Internet" reading notes - FTTH
Basic discipline formula and unit conversion
Codeforces Round #803 (Div. 2)(A-D)
随机事件的关系与运算
A subclass must use the super keyword to call the methods of its parent class
Service call feign of "micro service"
165 webmaster online toolbox website source code / hare online tool system v2.2.7 Chinese version
保姆级JDEC增删改查练习
[error record] no matching function for call to 'cacheflush' cacheflush();)
Reading notes on how to connect the network - tcp/ip connection (II)
awk从入门到入土(14)awk输出重定向
swatch
Awk from digging into the ground to getting started (10) awk built-in functions
1211 or chicken and rabbit in the same cage
Educational Codeforces Round 115 (Rated for Div. 2)
High order phase difference such as smear caused by myopic surgery
《网络是怎么样连接的》读书笔记 - 认识网络基础概念(一)
微服務入門:Gateway網關