本节先介绍一些Flutter中用于处理手势的

  • GestureDetector
  • GestureRecognizer

然后再仔细讨论一下手势竞争与冲突问题。

GestureDetector

GestureDetector 是一个用于手势识别的功能性组件(手势检测器),我们通过它可以来识别各种手势。实际上是指针事件的语义化封装,接下来我们详细介绍一下各种手势识别。
参数属性

  1. (new) GestureDetector GestureDetector({Key key,
  2. Widget child,
  3. void Function(TapDownDetails) onTapDown,
  4. void Function(TapUpDetails) onTapUp,
  5. void Function() onTap,
  6. void Function() onTapCancel,
  7. void Function(TapDownDetails) onSecondaryTapDown,
  8. void Function(TapUpDetails) onSecondaryTapUp,
  9. void Function() onSecondaryTapCancel,
  10. void Function() onDoubleTap,
  11. void Function() onLongPress,
  12. void Function(LongPressStartDetails) onLongPressStart,
  13. void Function(LongPressMoveUpdateDetails) onLongPressMoveUpdate,
  14. void Function() onLongPressUp,
  15. void Function(LongPressEndDetails) onLongPressEnd,
  16. void Function(DragDownDetails) onVerticalDragDown,
  17. void Function(DragStartDetails) onVerticalDragStart,
  18. void Function(DragUpdateDetails) onVerticalDragUpdate,
  19. void Function(DragEndDetails) onVerticalDragEnd,
  20. void Function() onVerticalDragCancel,
  21. void Function(DragDownDetails) onHorizontalDragDown,
  22. void Function(DragStartDetails) onHorizontalDragStart,
  23. void Function(DragUpdateDetails) onHorizontalDragUpdate,
  24. void Function(DragEndDetails) onHorizontalDragEnd,
  25. void Function() onHorizontalDragCancel,
  26. void Function(ForcePressDetails) onForcePressStart,
  27. void Function(ForcePressDetails) onForcePressPeak,
  28. void Function(ForcePressDetails) onForcePressUpdate,
  29. void Function(ForcePressDetails) onForcePressEnd,
  30. void Function(DragDownDetails) onPanDown,
  31. void Function(DragStartDetails) onPanStart,
  32. void Function(DragUpdateDetails) onPanUpdate,
  33. void Function(DragEndDetails) onPanEnd,
  34. void Function() onPanCancel,
  35. void Function(ScaleStartDetails) onScaleStart,
  36. void Function(ScaleUpdateDetails) onScaleUpdate,
  37. void Function(ScaleEndDetails) onScaleEnd,
  38. HitTestBehavior behavior,
  39. bool excludeFromSemantics = false,
  40. DragStartBehavior dragStartBehavior = DragStartBehavior.start})
属性名 描述
onTapDown:(TapDownDetails e){} 一个可能导致点击主按钮的指针已在特定位置与屏幕联系

HitTestBehavior

  1. // 自己处理事件
  2. HitTestBehavior.opaque
  3. // child处理事件
  4. HitTestBehavior.deferToChild
  5. // 自己和child都可以接收事件
  6. HitTestBehavior.translucent

onVerticalDragUpdate

  1. onVerticalDragUpdate GestureDragUpdateCallback(DragUpdateDetails details)
  2. DragUpdateDetails 存储所有的drag信息
  3. primaryDelta 最新的drag 导致的位移

onVerticalDragEnd

  1. onVerticalDragEnd GestureDragEndCallback(DragEndDetails details)
  2. DragEndDetails 存储drag结束信息
  3. details.velocity.pixelsPerSecond.dy 获取滑动结束后垂直的位移 x轴同理

点击、双击、长按

我们通过GestureDetector对Container进行手势识别,触发相应事件后,在Container上显示事件名,为了增大点击区域,将Container设置为200×100,代码如下:

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(new MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. debugShowCheckedModeBanner: true,
  8. title: '原始指针事件处理',
  9. theme: ThemeData(
  10. primarySwatch: Colors.blue,
  11. ),
  12. home: Scaffold(
  13. appBar: AppBar(title: Text("原始指针事件处理")),
  14. body: GestureDetectorTestRoute(),
  15. ));
  16. }
  17. }
  18. class GestureDetectorTestRoute extends StatefulWidget {
  19. @override
  20. _GestureDetectorTestRouteState createState() =>
  21. new _GestureDetectorTestRouteState();
  22. }
  23. class _GestureDetectorTestRouteState extends State<GestureDetectorTestRoute> {
  24. String _operation = "No Gesture detected!"; //保存事件名
  25. @override
  26. Widget build(BuildContext context) {
  27. return Center(
  28. child: GestureDetector(
  29. child: Container(
  30. alignment: Alignment.center,
  31. color: Colors.blue,
  32. width: 200.0,
  33. height: 100.0,
  34. child: Text(_operation,
  35. style: TextStyle(color: Colors.white),
  36. ),
  37. ),
  38. onTap: () => updateText("Tap点击"), //点击
  39. onDoubleTap: () => updateText("DoubleTap双击"), //双击
  40. onLongPress: () => updateText("LongPress长按"), //长按
  41. ),
  42. );
  43. }
  44. void updateText(String text) {
  45. //更新显示的事件名
  46. setState(() {
  47. _operation = text;
  48. });
  49. }
  50. }

运行效果如图所示:
手势识别 GestureDetector - 图1

⚠️ 注意:当同时监听onTap和onDoubleTap事件时,当用户触发tap事件时,会有200毫秒左右的延时! 这是因为当用户点击完之后很可能会再次点击以触发双击事件,所以GestureDetector会等一段时间来确定是否为双击事件。如果用户只监听了onTap(没有监听onDoubleTap)事件时,则没有延时。

拖动、滑动

一次完整的手势过程是指用户手指按下到抬起的整个过程,期间,用户按下手指后可能会移动,也可能不会移动。GestureDetector对于拖动和滑动事件是没有区分的,他们本质上是一样的。GestureDetector会将要监听的组件的原点(左上角)作为本次手势的原点,当用户在监听的组件上按下手指时,手势识别就会开始。下面我们看一个拖动圆形字母A的示例:

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(new MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. debugShowCheckedModeBanner: true,
  8. title: 'GestureDetectorTestRoute',
  9. theme: ThemeData(
  10. primarySwatch: Colors.blue,
  11. ),
  12. home: Scaffold(
  13. appBar: AppBar(title: Text("GestureDetectorTestRoute")),
  14. body: Drag(),
  15. ));
  16. }
  17. }
  18. class Drag extends StatefulWidget {
  19. @override
  20. _DragState createState() => new _DragState();
  21. }
  22. class _DragState extends State<Drag> with SingleTickerProviderStateMixin {
  23. double _top = 20.0; //距顶部的偏移
  24. double _left = 20.0; //距左边的偏移
  25. String _text_output = "初始情况";
  26. @override
  27. Widget build(BuildContext context) {
  28. return Stack(
  29. alignment: Alignment.center, //指定未定位或部分定位widget的对齐方式
  30. children: <Widget>[
  31. Positioned(
  32. top: 18.0,
  33. child: Container(
  34. width: 300,
  35. height: 300,
  36. child: Text(_text_output,
  37. style: TextStyle(
  38. color: Colors.white,
  39. decorationStyle: TextDecorationStyle.solid)),
  40. color: Colors.red,
  41. padding: EdgeInsets.all(5),
  42. ),
  43. ),
  44. Positioned(
  45. top: _top,
  46. left: _left,
  47. child: GestureDetector(
  48. child: CircleAvatar(child: Text("A")),
  49. //手指按下时会触发此回调
  50. onPanDown: (DragDownDetails e) {
  51. //打印手指按下的位置(相对于屏幕)
  52. setState(() {
  53. _text_output = "DragDownDetails.globalPosition:${e.globalPosition}";
  54. });
  55. },
  56. //手指滑动时会触发此回调
  57. onPanUpdate: (DragUpdateDetails e) {
  58. //用户手指滑动时,更新偏移,重新构建
  59. setState(() {
  60. _left += e.delta.dx;
  61. _top += e.delta.dy;
  62. _text_output = "DragUpdateDetails.delta.[dx|dy]:【_left:$_left】【_top:$_top】";
  63. });
  64. },
  65. onPanEnd: (DragEndDetails e) {
  66. //打印滑动结束时在x、y轴上的速度
  67. setState(() {
  68. _text_output = "DragEndDetails.velocity:${e.velocity}";
  69. });
  70. },
  71. ),
  72. ),
  73. ],
  74. );
  75. }
  76. }

运行后,就可以在任意方向拖动了,运行效果如图所示:
手势识别 GestureDetector - 图2
日志:

  1. I/flutter ( 8513): 用户手指按下:Offset(26.3, 101.8)
  2. I/flutter ( 8513): Velocity(235.5, 125.8)

代码解释:

  • DragDownDetails.globalPosition:当用户按下时,此属性为用户按下的位置相对于屏幕(而非父组件)原点(左上角)的偏移。
  • DragUpdateDetails.delta:当用户在屏幕上滑动时,会触发多次Update事件,delta指一次Update事件的滑动的偏移量。
  • DragEndDetails.velocity:该属性代表用户抬起手指时的滑动速度(包含x、y两个轴的),示例中并没有处理手指抬起时的速度,常见的效果是根据用户抬起手指时的速度做一个减速动画。

    单一方向拖动

    在本示例中,是可以朝任意方向拖动的,但是在很多场景,我们只需要沿一个方向来拖动,如一个垂直方向的列表,GestureDetector 可以只识别特定方向的手势事件,我们将上面的例子改为只能沿垂直方向拖动: ```dart import ‘package:flutter/material.dart’;

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: true, title: ‘GestureDetectorTestRoute’, theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar(title: Text(“GestureDetectorTestRoute”)), body: DragVertical(), )); } }

class DragVertical extends StatefulWidget { @override _DragVerticalState createState() => new _DragVerticalState(); } class _DragVerticalState extends State { double _top = 0.0; @override Widget build(BuildContext context) { return Stack( children: [ Positioned( top: _top, child: GestureDetector( child: CircleAvatar(child: Text(“A”)), //垂直方向拖动事件 onVerticalDragUpdate: (DragUpdateDetails details) { setState(() { // _left += e.delta.dx; _top += details.delta.dy; }); } ), ) ], ); } }

  1. 这样就只能在垂直方向拖动了,如果只想在水平方向滑动同理。
  2. <a name="9qbTT"></a>
  3. ### 缩放
  4. GestureDetector 可以监听缩放事件 onScaleUpdate,下面示例演示了一个简单的图片缩放效果:
  5. ```dart
  6. import 'package:flutter/material.dart';
  7. void main() => runApp(new MyApp());
  8. class MyApp extends StatelessWidget {
  9. @override
  10. Widget build(BuildContext context) {
  11. return MaterialApp(
  12. debugShowCheckedModeBanner: true,
  13. title: '缩放',
  14. theme: ThemeData(
  15. primarySwatch: Colors.blue,
  16. ),
  17. home: Scaffold(
  18. appBar: AppBar(title: Text("缩放")),
  19. body: _ScaleTestRoute(),
  20. ));
  21. }
  22. }
  23. class _ScaleTestRoute extends StatefulWidget {
  24. _ScaleTestRoute({Key key}) : super(key: key);
  25. @override
  26. __ScaleTestRouteState createState() => __ScaleTestRouteState();
  27. }
  28. class __ScaleTestRouteState extends State<_ScaleTestRoute> {
  29. double _width = 200.0; //通过修改图片宽度来达到缩放效果
  30. @override
  31. Widget build(BuildContext context) {
  32. return Center(
  33. child: GestureDetector(
  34. //指定宽度,高度自适应
  35. child: Image.asset("images/avatar.jpg", width: _width),
  36. onScaleUpdate: (ScaleUpdateDetails details) {
  37. setState(() {
  38. //缩放倍数在0.8到10倍之间
  39. _width = 200.0 * details.scale.clamp(.8, 10.0);
  40. });
  41. },
  42. ),
  43. );
  44. }
  45. }

运行效果如图所示:
手势识别 GestureDetector - 图3
现在在图片上双指张开、收缩就可以放大、缩小图片。本示例比较简单,实际中我们通常还需要一些其它功能,如双击放大或缩小一定倍数、双指张开离开屏幕时执行一个减速放大动画等,读者可以在学习完后面“动画”一章中的内容后自己来尝试实现一下。

GestureRecognizer

  • GestureDetector 内部是使用一个或多个 GestureRecognizer 来识别各种手势的
  • 而 GestureRecognizer 的作用就是通过 Listener 来将原始指针事件转换为语义手势,GestureDetector 直接可以接收一个子widget。
  • GestureRecognizer 是一个抽象类,一种手势的识别器对应一个 GestureRecognizer 的子类,Flutter实现了丰富的手势识别器,我们可以直接使用。

示例
假设我们要给一段富文本(RichText)的不同部分分别添加点击事件处理器,但是 TextSpan 并不是一个widget,这时我们不能用 GestureDetector,但TextSpan有一个 recognizer 属性,它可以接收一个GestureRecognizer。
假设我们需要在点击时给文本变色:

  1. import 'package:flutter/gestures.dart';
  2. import 'package:flutter/material.dart';
  3. void main() => runApp(new MyApp());
  4. class MyApp extends StatelessWidget {
  5. @override
  6. Widget build(BuildContext context) {
  7. return MaterialApp(
  8. debugShowCheckedModeBanner: true,
  9. title: 'GestureRecognizer',
  10. theme: ThemeData(
  11. primarySwatch: Colors.blue,
  12. ),
  13. home: Scaffold(
  14. appBar: AppBar(title: Text("GestureRecognizer")),
  15. body: _GestureRecognizerTestRoute(),
  16. ));
  17. }
  18. }
  19. class _GestureRecognizerTestRoute extends StatefulWidget {
  20. _GestureRecognizerTestRoute({Key key}) : super(key: key);
  21. @override
  22. __GestureRecognizerTestRouteState createState() =>
  23. __GestureRecognizerTestRouteState();
  24. }
  25. class __GestureRecognizerTestRouteState
  26. extends State<_GestureRecognizerTestRoute> {
  27. TapGestureRecognizer _tapGestureRecognizer = new TapGestureRecognizer();
  28. bool _toggle = false; //变色开关
  29. @override
  30. void dispose() {
  31. //用到GestureRecognizer的话一定要调用其dispose方法释放资源
  32. _tapGestureRecognizer.dispose();
  33. super.dispose();
  34. }
  35. @override
  36. Widget build(BuildContext context) {
  37. return Center(
  38. child: Text.rich(TextSpan(children: [
  39. TextSpan(text: "你好世界"),
  40. TextSpan(
  41. text: "点我变色",
  42. style: TextStyle(
  43. fontSize: 30.0, color: _toggle ? Colors.blue : Colors.red),
  44. recognizer: _tapGestureRecognizer
  45. ..onTap = () {
  46. setState(() {
  47. _toggle = !_toggle;
  48. });
  49. },
  50. ),
  51. TextSpan(text: "你好世界"),
  52. ])),
  53. );
  54. }
  55. }

运行效果:
手势识别 GestureDetector - 图4

注意:使用GestureRecognizer后一定要调用其dispose()方法来释放资源(主要是取消内部的计时器)。

手势竞争与冲突

手势竞争

如果在上例中我们同时监听水平和垂直方向的拖动事件,那么我们斜着拖动时哪个方向会生效?

  • 实际上取决于第一次移动时两个轴上的位移分量,哪个轴的大,哪个轴在本次滑动事件竞争中就胜出。
  • 实际上Flutter中的手势识别引入了一个Arena的概念,Arena直译为“竞技场”的意思,每一个手势识别器(GestureRecognizer)都是一个“竞争者”(GestureArenaMember),当发生滑动事件时,他们都要在“竞技场”去竞争本次事件的处理权,而最终只有一个“竞争者”会胜出(win)。
    • 例如,假设有一个ListView,它的第一个子组件也是ListView,如果现在滑动这个子ListView,父ListView会动吗?答案是否定的,这时只有子ListView会动,因为这时子ListView会胜出而获得滑动事件的处理权。

示例:我们以拖动手势为例,同时识别水平和垂直方向的拖动手势,当用户按下手指时就会触发竞争(水平方向和垂直方向),一旦某个方向“获胜”,则直到当次拖动手势结束都会沿着该方向移动。代码如下:

  1. import 'package:flutter/gestures.dart';
  2. import 'package:flutter/material.dart';
  3. void main() => runApp(new MyApp());
  4. class MyApp extends StatelessWidget {
  5. @override
  6. Widget build(BuildContext context) {
  7. return MaterialApp(
  8. debugShowCheckedModeBanner: true,
  9. title: '手势竞争',
  10. theme: ThemeData(
  11. primarySwatch: Colors.blue,
  12. ),
  13. home: Scaffold(
  14. appBar: AppBar(title: Text("手势竞争")),
  15. body: BothDirectionTestRoute(),
  16. ));
  17. }
  18. }
  19. class BothDirectionTestRoute extends StatefulWidget {
  20. @override
  21. BothDirectionTestRouteState createState() =>
  22. new BothDirectionTestRouteState();
  23. }
  24. class BothDirectionTestRouteState extends State<BothDirectionTestRoute> {
  25. double _top = 0.0;
  26. double _left = 0.0;
  27. @override
  28. Widget build(BuildContext context) {
  29. return Stack(
  30. children: <Widget>[
  31. Positioned(
  32. top: _top,
  33. left: _left,
  34. child: GestureDetector(
  35. child: CircleAvatar(child: Text("A")),
  36. //垂直方向拖动事件
  37. onVerticalDragUpdate: (DragUpdateDetails details) {
  38. setState(() {
  39. _top += details.delta.dy;
  40. });
  41. },
  42. //水平方向拖动事件
  43. onHorizontalDragUpdate: (DragUpdateDetails details) {
  44. setState(() {
  45. _left += details.delta.dx;
  46. });
  47. },
  48. ),
  49. )
  50. ],
  51. );
  52. }
  53. }

此示例运行后,每次拖动只会沿一个方向移动(水平或垂直),而竞争发生在手指按下后首次移动(move)时,此例中具体的“获胜”条件是:首次移动时的位移在水平和垂直方向上的分量大的一个获胜。

手势冲突

由于手势竞争最终只有一个胜出者,所以,当有多个手势识别器时,可能会产生冲突。假设有一个widget,它可以左右拖动,现在我们也想检测在它上面手指按下和抬起的事件,代码如下:

  1. import 'package:flutter/gestures.dart';
  2. import 'package:flutter/material.dart';
  3. void main() => runApp(new MyApp());
  4. class MyApp extends StatelessWidget {
  5. @override
  6. Widget build(BuildContext context) {
  7. return MaterialApp(
  8. debugShowCheckedModeBanner: true,
  9. title: '手势冲突',
  10. theme: ThemeData(
  11. primarySwatch: Colors.blue,
  12. ),
  13. home: Scaffold(
  14. appBar: AppBar(title: Text("手势冲突")),
  15. body: GestureConflictTestRoute(),
  16. ));
  17. }
  18. }
  19. class GestureConflictTestRoute extends StatefulWidget {
  20. @override
  21. GestureConflictTestRouteState createState() =>
  22. new GestureConflictTestRouteState();
  23. }
  24. class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
  25. double _left = 0.0;
  26. @override
  27. Widget build(BuildContext context) {
  28. return Stack(
  29. children: <Widget>[
  30. Positioned(
  31. left: _left,
  32. child: GestureDetector(
  33. child: CircleAvatar(child: Text("A")), //要拖动和点击的widget
  34. onTapDown: (details) {
  35. //一个可能导致点击主按钮的指针已在特定位置与屏幕联系。
  36. print("down");
  37. },
  38. onHorizontalDragUpdate: (DragUpdateDetails details) { //用主按钮与屏幕接触并水平移动的指针已在水平方向上移动。
  39. setState(() {
  40. _left += details.delta.dx;
  41. });
  42. },
  43. onHorizontalDragEnd: (details) {
  44. //以前用主按钮接触屏幕并水平移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动
  45. print("onHorizontalDragEnd");
  46. },
  47. onTapUp: (details) {
  48. //用主按钮触发点击的指针已停止在特定位置与屏幕接触
  49. print("up");
  50. },
  51. ),
  52. )
  53. ],
  54. );
  55. }
  56. }

现在我们按住圆形“A”拖动然后抬起手指,控制台日志如下:

  1. I/flutter (17539): down
  2. I/flutter (17539): onHorizontalDragEnd

我们发现没有打印”up”,这是为什么呢?

  • 这是因为在拖动时,刚开始按下手指时在没有移动时,拖动手势还没有完整的语义,此时onTapDown手势胜出(win),此时打印”down”
  • 而拖动时,拖动手势会胜出,当手指抬起时,onHorizontalDragEnd和 onTapUp发生了冲突,但是因为是在拖动的语义中,所以onHorizontalDragEnd胜出,所以就会打印 “onHorizontalDragEnd”。

处理手势冲突

如果我们的代码逻辑中,对于手指按下和抬起是强依赖的,比如在一个轮播图组件中,我们希望

  • 手指按下时,暂停轮播,
  • 而抬起时恢复轮播

但是由于轮播图组件中本身可能已经处理了拖动手势(支持手动滑动切换),甚至可能也支持了缩放手势,这时我们如果在外部再用onTapDown、onTapUp来监听的话是不行的。这时我们应该怎么做?其实很简单,通过Listener监听原始指针事件就行:

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(new MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. debugShowCheckedModeBanner: true,
  8. title: '处理手势冲突',
  9. theme: ThemeData(
  10. primarySwatch: Colors.blue,
  11. ),
  12. home: Scaffold(
  13. appBar: AppBar(title: Text("处理手势冲突")),
  14. body: GestureConflictTestRoute(),
  15. ));
  16. }
  17. }
  18. class GestureConflictTestRoute extends StatefulWidget {
  19. @override
  20. GestureConflictTestRouteState createState() =>
  21. new GestureConflictTestRouteState();
  22. }
  23. class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
  24. double _leftB = 0.0;
  25. @override
  26. Widget build(BuildContext context) {
  27. return Stack(
  28. children: <Widget>[
  29. Positioned(
  30. top: 80.0,
  31. left: _leftB,
  32. child: Listener(
  33. onPointerDown: (details) {
  34. print("down");
  35. },
  36. onPointerUp: (details) {
  37. //会触发
  38. print("up");
  39. },
  40. child: GestureDetector(
  41. child: CircleAvatar(child: Text("B")),
  42. onHorizontalDragStart: (DragStartDetails e){
  43. print("onHorizontalDragStart");
  44. },
  45. onHorizontalDragUpdate: (DragUpdateDetails details) {
  46. setState(() {
  47. _leftB += details.delta.dx;
  48. });
  49. },
  50. onHorizontalDragEnd: (details) {
  51. print("onHorizontalDragEnd");
  52. },
  53. ),
  54. ),
  55. )
  56. ],
  57. );
  58. }
  59. }

手势冲突只是手势级别的,而手势是对原始指针的语义化的识别,所以在遇到复杂的冲突场景时,都可以通过Listener直接识别原始指针事件来解决冲突。