当前位置:网站首页>Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套
Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套
2022-07-04 08:57: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), ), ), ); }), ), ], ), ), )); }}
边栏推荐
- Démarrage des microservices: passerelle
- Horizon sunrise X3 PI (I) first boot details
- [Chongqing Guangdong education] National Open University spring 2019 455 logistics practice reference questions
- manjaro安装微信
- 广和通高性能4G/5G无线模组解决方案全面推动高效、低碳智能电网
- Awk from getting started to digging in (9) circular statement
- Bishi blog (13) -- oral arithmetic test app
- In depth investigation and Strategic Research Report on China's motion controller Market (2022 Edition)
- Codeforces Global Round 21(A-E)
- Target detection -- intensive reading of yolov3 paper
猜你喜欢
HMS core helps baby bus show high-quality children's digital content to global developers
Solve the problem of "Chinese garbled MySQL fields"
[C Advanced] file operation (2)
Industry depression has the advantages of industry depression
ctfshow web255 web 256 web257
Turn: excellent managers focus not on mistakes, but on advantages
Ehrlich sieve + Euler sieve + interval sieve
Codeforces Round #803 (Div. 2)(A-D)
转:优秀的管理者,关注的不是错误,而是优势
Call Baidu map to display the current position
随机推荐
manjaro安装微信
HMS core helps baby bus show high-quality children's digital content to global developers
Horizon sunrise X3 PI (I) first boot details
Go zero micro service practical series (IX. ultimate optimization of seckill performance)
In depth investigation and Strategic Research Report on China's motion controller Market (2022 Edition)
Codeforces Global Round 21(A-E)
到底什么才是DaaS数据即服务?别再被其他DaaS概念给误导了
Newh3c - routing protocol (RIP, OSPF)
Awk from entry to earth (18) GAW K line manual
What if the wireless network connection of the laptop is unavailable
awk从入门到入土(11)awk getline函数详解
4 small ways to make your Tiktok video clearer
std::is_ union,std::is_ class,std::integral_ constant
Awk from entry to earth (15) awk executes external commands
[Chongqing Guangdong education] National Open University spring 2019 455 logistics practice reference questions
Ehrlich sieve + Euler sieve + interval sieve
Four essential material websites for we media people to help you easily create popular models
【无标题】转发最小二乘法
C#实现一个万物皆可排序的队列
上周热点回顾(6.27-7.3)