我写这个系列的目的,可能更倾向于自我总结,而不是教Flutter入门,所以可能有些东西是在我本人的理解下的一些用法与理解,不一定准确与适合所有开发者,如果有其他更好的想法,可以与我讨论,联系方式见关于

本项目基于macOS桌面程序,源码地址:https://github.com/NeverOvO/learningfoundation

-NeverOuO

GestureDetecotr属性

  GestureDetector({
    Key? key,
    this.child,
    this.onTapDown,
    this.onTapUp,
    this.onTap,
    this.onTapCancel,
    this.onSecondaryTap,
    this.onSecondaryTapDown,
    this.onSecondaryTapUp,
    this.onSecondaryTapCancel,
    this.onTertiaryTapDown,
    this.onTertiaryTapUp,
    this.onTertiaryTapCancel,
    this.onDoubleTapDown,
    this.onDoubleTap,
    this.onDoubleTapCancel,
    this.onLongPressDown,
    this.onLongPressCancel,
    this.onLongPress,
    this.onLongPressStart,
    this.onLongPressMoveUpdate,
    this.onLongPressUp,
    this.onLongPressEnd,
    this.onSecondaryLongPressDown,
    this.onSecondaryLongPressCancel,
    this.onSecondaryLongPress,
    this.onSecondaryLongPressStart,
    this.onSecondaryLongPressMoveUpdate,
    this.onSecondaryLongPressUp,
    this.onSecondaryLongPressEnd,
    this.onTertiaryLongPressDown,
    this.onTertiaryLongPressCancel,
    this.onTertiaryLongPress,
    this.onTertiaryLongPressStart,
    this.onTertiaryLongPressMoveUpdate,
    this.onTertiaryLongPressUp,
    this.onTertiaryLongPressEnd,
    this.onVerticalDragDown,
    this.onVerticalDragStart,
    this.onVerticalDragUpdate,
    this.onVerticalDragEnd,
    this.onVerticalDragCancel,
    this.onHorizontalDragDown,
    this.onHorizontalDragStart,
    this.onHorizontalDragUpdate,
    this.onHorizontalDragEnd,
    this.onHorizontalDragCancel,
    this.onForcePressStart,
    this.onForcePressPeak,
    this.onForcePressUpdate,
    this.onForcePressEnd,
    this.onPanDown,
    this.onPanStart,
    this.onPanUpdate,
    this.onPanEnd,
    this.onPanCancel,
    this.onScaleStart,
    this.onScaleUpdate,
    this.onScaleEnd,
    this.behavior,
    this.excludeFromSemantics = false,
    this.dragStartBehavior = DragStartBehavior.start,
  }) : assert(excludeFromSemantics != null),
       assert(dragStartBehavior != null),
       assert(() {
         final bool haveVerticalDrag = onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null;
         final bool haveHorizontalDrag = onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null;
         final bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null;
         final bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null;
         if (havePan || haveScale) {
           if (havePan && haveScale) {
             throw FlutterError.fromParts(<DiagnosticsNode>[
               ErrorSummary('Incorrect GestureDetector arguments.'),
               ErrorDescription(
                 'Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan.',
               ),
               ErrorHint('Just use the scale gesture recognizer.'),
             ]);
           }
           final String recognizer = havePan ? 'pan' : 'scale';
           if (haveVerticalDrag && haveHorizontalDrag) {
             throw FlutterError(
               'Incorrect GestureDetector arguments.\n'
               'Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer '
               'will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.',
             );
           }
         }
         return true;
       }()),
       super(key: key);

个人常用用法:隐藏键盘

包裹在存在TextEditingController页面的最外层,这样点击空白区域就能把键盘缩小回去:

behavior: HitTestBehavior.opaque,
onTap: (){
  FocusScope.of(context).requestFocus(FocusNode());
},

this.child

可以是任何Widget,此处不做展开。

this.behavior

/// How to behave during hit tests.
enum HitTestBehavior {
  /// Targets that defer to their children receive events within their bounds
  /// only if one of their children is hit by the hit test.
  deferToChild,

  /// Opaque targets can be hit by hit tests, causing them to both receive
  /// events within their bounds and prevent targets visually behind them from
  /// also receiving events.
  opaque,

  /// Translucent targets both receive events within their bounds and permit
  /// targets visually behind them to also receive events.
  translucent,
}

用来控制GestureDetector内子组件与空白空间是否接受触控的逻辑

一般让GestureDetector的所有范围都可以被点击,采用opaque较多。

onTap相关

onTap: (){
  print("onTap");
},

onTapUp: (e){
  print("onTapUp");
  print(e.globalPosition);
},

onTapDown: (e){
  print("onTapDown");
  print(e.globalPosition);
},

onTapCancel: (){
  print("onTapCancel");
},

onTap

这个是GestureDetector中最常用的方法,指用户点击一次且没有滑动取消点击后的触发方法。

onTapDown

单击按下后即触发

onTapDown: (e){
  print("onTapDown");
  print(e.globalPosition);
},

这里的e为:TapDownDetails details

  TapDownDetails({
    this.globalPosition = Offset.zero,
    Offset? localPosition,
    this.kind,
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;

this.globalPosition : 指针与屏幕接触的全局位置。

this.kind : 引发事件的设备类型。

localPosition : 指针与屏幕接触的本地位置。

这里可以用来记录用户常用点击位置与点击起始位置。

onTapUp

单击且没有移动取消,放开手指后触发。

onTapUp: (e){
  print("onTapUp");
  print(e.globalPosition);
},

e同onTapDown。

这里可以用来记录用户点击结束位置。

onTapCancel

单击取消,一般在点击后不放开手指拖动到其他位置可以触发

onTapCancel: (){
  print("onTapCancel");
},

这里可以收集用户取消点击的频率与场景,优化UI引导点击。

onSecondaryTap

这个其实就是右键,具体逻辑与onTap系列相同,不作详细展开。

onTertiaryTap

这里为鼠标第三键即鼠标中键。但是我在测试中感觉触发不是太稳定,由于几乎不会使用,之后再补充吧。

onDoubleTap

双击触发

onDoubleTap: (){
  print("onDoubleTap");
},
onDoubleTapDown: (e){
  print("onDoubleTapDown");
  print(e.globalPosition);
},
onDoubleTapCancel: (){
  print("onDoubleTapCancel");
},

onDoubleTapDown

双击只有Down触发,没有Up,这里需要注意。

另外在某些较为卡顿或极端条件下,可能会一起触发单击方法,所以需要注意。

onLongPress

长按触发

长按方法的触发逻辑链为:onLongPressDown -> onLongPressStart -> onLongPress -> onLongPressMoveUpdate -> onLongPressEnd -> onLongPressUp

onLongPress: (){
  print("onLongPress");
},
onLongPressDown: (e){
  print("onLongPressDown");
},
onLongPressUp: (){
  print("onLongPressUp");
},
onLongPressStart: (e){
  print("onLongPressStart");
},
onLongPressMoveUpdate: (e){
  print("onLongPressMoveUpdate");
},
onLongPressEnd: (e){
  print("onLongPressEnd");
},
onLongPressCancel: (){
  print("onLongPressCancel");
},

onLongPress

最为常用的长按触发方法

onLongPress: (){
  print("onLongPress");
},

onLongPressDown

长按时,首先触发的方法,用来获取长按的起点

onLongPressDown: (e){
  print("onLongPressDown");
},

这里的e为:LongPressDownDetails details

const LongPressDownDetails({
    this.globalPosition = Offset.zero,
    Offset? localPosition,
    this.kind,
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;

用法基本与onTap的TapDownDetails details相似

onLongPressStart

长按后,触发的方法,在onLongPressStart后就会对onLongPress方法进行触发。

onLongPressMoveUpdate

长按中,用户拖动手指时会进行触发,来判定获取用户手指、鼠标的长按位置变更。

onLongPressCancel

长按中,当用户手指或鼠标移动到屏幕或指定位置外时会触发取消动作。

onSecondaryLongPress/onTertiaryLongPress

与长按类似,这里为右键长按与中键长按的方法。

onVerticalDrag类

垂直拖拽方法,触发顺序为:onVerticalDragDown -> onVerticalDragStart -> onVerticalDragUpdate -> onVerticalDragEnd

onVerticalDragDown: (e){
  print(e.localPosition);
  print("onVerticalDragDown");
},
onVerticalDragStart: (e){
  print("onVerticalDragStart");
},
onVerticalDragUpdate: (e){
  print("onVerticalDragUpdate");
},
onVerticalDragEnd: (e){
  print("onVerticalDragEnd");
},
onVerticalDragCancel: (){
  print("onVerticalDragCancel");
},

onVerticalDragDown

垂直拖拽时首先触发的方法,用来获取垂直拖拽的起点

onVerticalDragDown: (e){
  print(e.localPosition);
  print("onVerticalDragDown");
},

这里的e为:DragDownDetails details

DragDownDetails({
    this.globalPosition = Offset.zero,
    Offset? localPosition,
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;

相比点击类方法,少了kind属性。

onVerticalDragCancel

垂直拖拽取消时触发方法,一般为用户拖动鼠标或手指离开屏幕或Widget范围外时。

onHorizontalDrag类

水平拖动时触发的方法,用法与onVerticalDrag类相似

onHorizontalDragDown: (e){
  print("onHorizontalDragDown");
},
onHorizontalDragStart: (e){
  print("onHorizontalDragStart");
},
onHorizontalDragUpdate: (e){
  print("onHorizontalDragStart");
},
onHorizontalDragEnd: (e){
  print("onHorizontalDragEnd");
},
onHorizontalDragCancel: (){
  
},

onPan类

onPan类基本就是onHorizontalDrag类与onVerticalDrag类之和。

onPanDown: (e){
  print("onPanDown");
},
onPanStart: (e){
  print("onPanStart");
},
onPanUpdate: (e){
  print("onPanUpdate");
},
onPanEnd: (e){
  print("onPanEnd");
},
onPanCancel: (){
  
},

onForcePress类

用力按压时能触发的方法,需要一定的设备支持

onForcePressPeak: (e){
  print("onForcePressPeak");
},
onForcePressStart: (e){
  print("onForcePressStart");
},
onForcePressUpdate: (e){
  print("onForcePressUpdate");
},
onForcePressEnd: (e){
  print("onForcePressEnd");
},

这里的e为:ForcePressDetails details

ForcePressDetails({
    required this.globalPosition,
    Offset? localPosition,
    required this.pressure,
  }) : assert(globalPosition != null),
       assert(pressure != null),
       localPosition = localPosition ?? globalPosition;

this.pressure : 按在屏幕上的压力

由于场景和设备的限制,不做展开后续有相应设备后补充。

右键点击的个人用法:右键菜单

//右键
onSecondaryTap: (){
  showMenu(context: context, position: RelativeRect.fromLTRB(_x!, _y!, _x!, _y!), items: [
    PopupMenuItem(child: Text('option1'), value: 'btn1',),
    PopupMenuItem(child: Text('option2'), value: 'btn2',),
    PopupMenuItem(child: Text('option3'), value: 'btn3',),
  ]).then((value){
    print(value);
  });
},
onSecondaryTapDown: (details){
  setState(() {
    _x = details.globalPosition.dx;
    _y = details.globalPosition.dy;
  });
},


这串代码可以在组件范围内点击右键触发一个菜单。

部分桌面端,对于鼠标的拖拽失效处理

class MyCustomScrollBehavior extends MaterialScrollBehavior {
  // Override behavior methods and getters like dragDevices
  @override
  Set<PointerDeviceKind> get dragDevices => {
    PointerDeviceKind.touch,
    PointerDeviceKind.mouse,
    // etc.
  };
}

ScrollConfiguration(
  behavior: MyCustomScrollBehavior(),