当前位置:   article > 正文

Flutter滑动冲突处理——GestureDetector嵌套ListView_flutter 安卓上滑动冲突

flutter 安卓上滑动冲突

哈喽,我是老刘

几年前分享了一篇GestureDetector嵌套ListView的文章

Flutter多控件滑动事件联动(滑动冲突处理) - 知乎 (zhihu.com)

由于文章中只给出了关键部位的代码,另外使用的技术也偏底层

所以很多同学私信我要完整的源码

这里把原先的方案整理一下,另外也给出完整的代码供大家参考

我们先一点一点来看这个问题

滑动关闭组件

首先大家一定在各种App中见过这个滑动关闭组件

就是手指向下滑动,组件跟随手指移动

手指抬起后组件滑出屏幕

我们先来实现这个组件,然后来讨论一下如果组件中的内容时一个ListView,要怎么处理

如果这个组件的内容时固定内容,不是ListView这样的可滚动组件

实现起来其实很简单

我这里直接放源码

  1. import 'package:flutter/material.dart';
  2. class CloseOnSwipeDownWidget extends StatefulWidget {
  3. final Widget child;
  4. const CloseOnSwipeDownWidget({
  5. Key? key,
  6. required this.child,
  7. }) : super(key: key);
  8. @override
  9. CloseOnSwipeDownWidgetState createState() => CloseOnSwipeDownWidgetState();
  10. }
  11. class CloseOnSwipeDownWidgetState extends State
  12. with TickerProviderStateMixin {
  13. double yOffset = 0.0;
  14. double initialPosition = 0.0;
  15. bool isAnimatingOut = false;
  16. int animTime = 0;
  17. @override
  18. void initState() {
  19. super.initState();
  20. }
  21. @override
  22. Widget build(BuildContext context) {
  23. return GestureDetector(
  24. onVerticalDragDown: (details) {
  25. initialPosition = details.globalPosition.dy;
  26. },
  27. onVerticalDragUpdate: (details) {
  28. double updatedPosition = details.globalPosition.dy;
  29. double deltaY = updatedPosition - initialPosition;
  30. animTime = 0;
  31. setState(() {
  32. yOffset = yOffset + deltaY;
  33. initialPosition = updatedPosition;
  34. });
  35. },
  36. onVerticalDragEnd: (details) {
  37. animTime = 300;
  38. if (yOffset > 200) {
  39. // 触发滑出动画
  40. _startSlideOutAnimation();
  41. } else {
  42. // 触发返回原始位置的动画
  43. _startReturnToOriginalPositionAnimation();
  44. }
  45. },
  46. child: Stack(
  47. children: [
  48. AnimatedPositioned( // 组件跟随手指位移,以及抬起手指后组件移动动画
  49. duration: Duration(milliseconds: animTime),
  50. curve: Curves.easeInOut,
  51. top: yOffset,
  52. left: 0,
  53. right: 0,
  54. child: widget.child,
  55. onEnd: () {
  56. if(isAnimatingOut) {
  57. Navigator.of(context).pop();
  58. }
  59. },
  60. ),
  61. ],
  62. ),
  63. );
  64. }
  65. // 开始滑出动画
  66. void _startSlideOutAnimation() {
  67. setState(() {
  68. isAnimatingOut = true;
  69. yOffset = MediaQuery.of(context).size.height;
  70. });
  71. }
  72. // 开始返回原始位置的动画
  73. void _startReturnToOriginalPositionAnimation() {
  74. setState(() {
  75. yOffset = 0.0;
  76. });
  77. }
  78. }

这里的原理很简单

就是通过GestureDetector检测用户的滑动行为

并且通过AnimatedPositioned将用户手指的每一段位移转换成整个组件的移动

并且在最终手指抬起时,通过AnimatedPositioned的动画效果让组件移出屏幕或者复位

我们写一个页面来使用这个组件

  1. class TestPage extends StatelessWidget {
  2. const TestPage({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return Scaffold(
  6. backgroundColor: Colors.white,
  7. body: CloseOnSwipeDownWidget(
  8. child: Column(
  9. children: [
  10. SizedBox(
  11. height: MediaQuery.of(context).size.height - 500,
  12. ), // 让组件内容在页面底部
  13. Container(
  14. height: 500,
  15. color: Colors.blue,
  16. alignment: Alignment.bottomCenter,
  17. child: const Center(
  18. child: Text('我是内容'),
  19. ),
  20. ),
  21. ],
  22. ),
  23. ),
  24. );
  25. }
  26. }

看一下效果

那么现在如果把传入的内容换成一个ListView

你会发现ListView内部的内容可以正常滑动

但是外部的GestureDetector无法响应用户的手势了

我们下面就来解决这个问题

GestureDetector嵌套ListView

首先我们要知道为什么嵌入ListView后GestureDetector会失效

这是Flutter的竞技场机制导致的

用户的一个滑动行为其实在底层时通过down、move和up三种事件完成的

当一个down事件出现后,如果手指按下的坐标位置有多个组件可以响应滑动事件

就是我们目前例子中的GestureDetector嵌套ListView的场景

Flutter框架会将这些组件都加入竞技场

然后通过一定的逻辑选择一个组件胜出

通常同类组件嵌套时最内层的组件胜出

胜出的组件会处理接下来的move和up事件,其它组件则不会继续处理这些事件了

在GestureDetector嵌套ListView的场景中

ListView最终胜出,所以后续的事件都交由ListView处理

而GestureDetector收不到后续的事件,也就不会响应用户的手势了

因此,我们解决这个问题的第一步就是要让GestureDetector在这种场景下也能收到后续的事件

决胜竞技场

其实要做到这一步很简单

GestureDetector真正处理用户手势事件的是内部的Recognizer

比如处理上下滑动的是VerticalDragGestureRecognizer

而Recognizer在竞技场失败后也可以单方面宣布自己胜出

这样即使在竞技场失败了,GestureDetector也能收到后续的手势事件

因此我们现定义一个单方面宣布胜出的Recognizer

  1. class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
  2. @override
  3. void rejectGesture(int pointer) {
  4. // 单方面宣布自己胜出
  5. acceptGesture(pointer);
  6. }
  7. }

接下来,把这个Recognizer加入到GestureDetector中

这时就需要用到一个GestureDetector的底层组件RawGestureDetector

通过它我们可以自己指定需要的Recognizer

  1. RawGestureDetector(
  2. gestures: {
  3. _MyVerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
  4. _MyVerticalDragGestureRecognizer>(
  5. () => _MyVerticalDragGestureRecognizer(),
  6. (_MyVerticalDragGestureRecognizer recognizer) {
  7. recognizer
  8. ..onStart = (DragStartDetails details) {
  9. }
  10. ..onUpdate = (DragUpdateDetails details) {
  11. }
  12. ..onEnd = (DragEndDetails details) {
  13. };
  14. }),
  15. },
  16. child: ...
  17. );

这其中的onStart、onUpdate和onEnd 方法

就对应了GestureDetector中的onVerticalDragDown、onVerticalDragUpdate和onVerticalDragEnd方法

好的,到目前为止,我们已经解决了竞技场造成的只有ListView能收到手势事件的问题

但是这样的话就会造成用户滑动,内外两层组件都在移动的问题

因此,接下来我们就来解决如何两个滑动组件如何相互配合

监听ListView的滚动

ListView是ScrollView的子类

所有的ScrollView都会在滚动过程中沿着组件树向上发出各种滚动状态变化的通知

通过监听这些通知事件,就可以判断ScrollView的滚动状态

  1. NotificationListener( // 监听内部ListView的滑动变化
  2. onNotification: (ScrollNotification notification) {
  3. if (notification is OverscrollNotification && notification.overscroll < 0) {
  4. // 用户向下滑动,ListView已经滑动到顶部,处理GestureDetector的滑动事件
  5. } else if (notification is ScrollUpdateNotification) {
  6. // 用户在ListView中执行滑动动作,关闭外部GestureDetector的滑动处理
  7. } else {
  8. }
  9. return false;
  10. },
  11. child: //ListView
  12. ),

好的,把这些组合起来,完整的代码如下

  1. import 'package:flutter/gestures.dart';
  2. import 'package:flutter/material.dart';
  3. class CloseOnSwipeDownWidget2 extends StatefulWidget {
  4. final Widget child;
  5. const CloseOnSwipeDownWidget2({
  6. Key? key,
  7. required this.child,
  8. }) : super(key: key);
  9. @override
  10. CloseOnSwipeDownWidget2State createState() => CloseOnSwipeDownWidget2State();
  11. }
  12. class CloseOnSwipeDownWidget2State extends State
  13. with TickerProviderStateMixin {
  14. double yOffset = 0.0;
  15. double initialPosition = 0.0;
  16. bool isAnimatingOut = false;
  17. int animTime = 0;
  18. bool needDrag = true;
  19. @override
  20. void initState() {
  21. super.initState();
  22. }
  23. @override
  24. Widget build(BuildContext context) {
  25. return RawGestureDetector(
  26. gestures: {
  27. _MyVerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
  28. _MyVerticalDragGestureRecognizer>(
  29. () => _MyVerticalDragGestureRecognizer(),
  30. (_MyVerticalDragGestureRecognizer recognizer) {
  31. recognizer
  32. ..onStart = (DragStartDetails details) {
  33. initialPosition = details.globalPosition.dy;
  34. }
  35. ..onUpdate = (DragUpdateDetails details) {
  36. if (!needDrag) {
  37. return;
  38. }
  39. double updatedPosition = details.globalPosition.dy;
  40. double deltaY = updatedPosition - initialPosition;
  41. animTime = 0;
  42. setState(() {
  43. yOffset = yOffset + deltaY;
  44. initialPosition = updatedPosition;
  45. });
  46. }
  47. ..onEnd = (DragEndDetails details) {
  48. animTime = 300;
  49. if (yOffset > 200) {
  50. // 触发滑出动画
  51. _startSlideOutAnimation();
  52. } else {
  53. // 触发返回原始位置的动画
  54. _startReturnToOriginalPositionAnimation();
  55. }
  56. };
  57. }),
  58. },
  59. child: Stack(
  60. children: [
  61. AnimatedPositioned(
  62. duration: Duration(milliseconds: animTime),
  63. curve: Curves.easeInOut,
  64. top: yOffset,
  65. left: 0,
  66. right: 0,
  67. child: NotificationListener( // 监听内部ListView的滑动变化
  68. onNotification: (ScrollNotification notification) {
  69. if (notification is OverscrollNotification && notification.overscroll < 0) {
  70. // 用户向下滑动,ListView已经滑动到顶部,处理GestureDetector的滑动事件
  71. needDrag = true;
  72. } else if (notification is ScrollUpdateNotification) {
  73. // 用户在ListView中执行滑动动作,关闭外部GestureDetector的滑动处理
  74. needDrag = false;
  75. } else {
  76. }
  77. return false;
  78. },
  79. child: widget.child,
  80. ),
  81. onEnd: () {
  82. if (isAnimatingOut) {
  83. Navigator.of(context).pop();
  84. }
  85. },
  86. ),
  87. ],
  88. ),
  89. );
  90. }
  91. // 开始滑出动画
  92. void _startSlideOutAnimation() {
  93. setState(() {
  94. isAnimatingOut = true;
  95. yOffset = MediaQuery.of(context).size.height;
  96. });
  97. }
  98. // 开始返回原始位置的动画
  99. void _startReturnToOriginalPositionAnimation() {
  100. setState(() {
  101. yOffset = 0.0;
  102. });
  103. }
  104. }
  105. class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
  106. bool needDrag = true;
  107. @override
  108. void rejectGesture(int pointer) {
  109. // 单方面宣布自己胜出
  110. acceptGesture(pointer);
  111. }
  112. }

简单来说就是通过needDrag来判断外部GestureDetector是否跟随用户手势移动

needDrag的值基于监听ListView的状态

当ListView已经滑动到顶部,就开始响应用户的手势动作

下面是使用这个组件的代码

  1. class TestPage2 extends StatelessWidget {
  2. const TestPage2({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return Scaffold(
  6. backgroundColor: Colors.white,
  7. body: CloseOnSwipeDownWidget2(
  8. child: Column(
  9. children: [
  10. SizedBox(
  11. height: MediaQuery.of(context).size.height - 500,
  12. ), // 让组件内容在页面底部
  13. Container(
  14. height: 500,
  15. color: Colors.blue,
  16. child: ListView.builder(
  17. itemCount: 20,
  18. itemBuilder: (context, index) {
  19. return ListTile(
  20. title: Text('index $index'),
  21. );
  22. }),
  23. ),
  24. ],
  25. ),
  26. ),
  27. );
  28. }
  29. }

实现效果如下

好了,关于手势组件嵌套的问题就先聊到这里

如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。

点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。

可以作为Flutter学习的知识地图。

覆盖90%开发场景的《Flutter开发手册》 (qq.com)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/463121
推荐阅读
相关标签
  

闽ICP备14008679号