End

Flutter 陈航 19-手势识别 PointerEvent GestureDetector GestureRecognizer

本文地址


目录

19 | 用户交互事件该如何响应?

手势操作在 Flutter 中分为两类:

  • 原始的指针事件(Pointer Event):即原生开发中常见的触摸事件,表示屏幕上触摸行为触发的位移行为
  • 手势识别(Gesture Detector):表示多个原始指针事件的组合操作,如点击、双击、长按等,是指针事件的语义化封装

Listener

指针事件表示用户交互的原始触摸数据,如手指接触屏幕 PointerDownEvent、手指在屏幕上移动 PointerMoveEvent、手指抬起 PointerUpEvent、触摸取消 PointerCancelEvent,这与原生系统的底层触摸事件抽象是一致的。

在手指接触屏幕,触摸事件发起时,Flutter 会确定手指与屏幕发生接触的位置上究竟有哪些组件,并将触摸事件交给最内层的组件去响应。与浏览器中的事件冒泡机制类似,事件会从这个最内层的组件开始,沿着组件树向根节点向上冒泡分发。

不过 Flutter 无法像浏览器冒泡那样取消或者停止事件进一步分发,只能通过 hitTestBehavior 去调整组件在命中测试期内应该如何表现,比如把触摸事件交给子组件,或者交给其视图层级之下的组件去响应。

Flutter 提供了 Listener Widget,可以监听其子 Widget 的原始指针事件。

Listener(
  child: Container(color: Colors.blue, width: 200, height: 200),
  onPointerDown: (event) => flog("按下 $event"),
  onPointerMove: (event) => flog("移动 $event"),
  onPointerUp: (event) => flog("抬起 $event"),
)

void flog(String text) {
  log(text);
  debugPrint(text);
}

GestureDetector

GestureDetector 是一个处理各种高级用户触摸行为的 Widget,与 Listener 一样,也是一个功能性组件。

GestureDetector(
   child: Container(color: Colors.red, width: 200, height: 200),
   onTap: () => flog("点击"),
   onDoubleTap: () => flog("双击"),
   onLongPress: () => flog("长按"),
   onPanUpdate: (e) => flog("拖拽 ${e.delta.toString()}"),
)

拖拽 onPanUpdate 和 缩放 onScaleUpdate 不可以同时使用

onPanUpdate: (e) => setState(() {
  flog("拖拽 ${e.delta.info(1)}");
  _left += e.delta.dx;
  _top += e.delta.dy;
}),
onScaleUpdate: (details) => setState(() {
  flog("缩放 scale= ${details.scale} ${details.horizontalScale} ${details.verticalScale}");
  _width = 200 * details.horizontalScale;
  _height = 200 * details.verticalScale;
})
extension on Offset {
  String info([int? fractionDigits]) {
    var dxValue = dx.toStringAsExponential(fractionDigits);
    var dyValue = dy.toStringAsExponential(fractionDigits);
    return "dx=$dxValue dy=$dyValue";
  }
}

手势竞技场 Arena

尽管可以对一个 Widget 同时监听多个手势事件,但最终只会有一个手势能够得到本次事件的处理权。对于多个手势的识别,Flutter 引入了手势竞技场 Arena,用来识别究竟哪个手势可以响应用户事件。

手势竞技场会考虑用户触摸屏幕的时长、位移、拖动方向,来确定最终手势。

实际上,GestureDetector 内部对每一个手势都建立了一个工厂类,而工厂类的内部会使用手势识别 GestureRecognizer 来确定当前处理的手势。

而所有手势的工厂类都会被交给 RawGestureDetector 类,以完成监测手势的大量工作:使用 Listener 监听原始指针事件,并在状态改变时把信息同步给所有的手势识别器,然后这些手势会在竞技场决定最后由谁来响应用户事件。

改变竞技场行为

如果给多个存在父子关系的 Widget 注册同类型的手势监听器,手势竞技场通常最终会确认由 子 Widget 来响应事件。

GestureDetector(
  onTap: () => flog('父视图的点击回调'),
  child: Container(
    color: Colors.green,
    child: Center(
      child: GestureDetector(
        onTap: () => flog('子视图的点击回调'),
        child: Container(color: Colors.blue, width: 200, height: 200),
      ),
    ),
  ),
)

为了让父容器也能接收到手势,需要使用 RawGestureDetector 和 GestureFactory 改变竞技场行为。

GestureRecognizer

GestureDetector 内部对每一个手势都建立了一个工厂类,工厂类的内部会使用手势识别类 GestureRecognizer 来确定当前要处理的手势。

TapGestureRecognizer 继承自 GestureRecognizer

// 自定义【点击手势识别器】,让其在竞技场被 PK 失败时,把自己重新添加回来,以便接下来能继续响应手势事件
class MyTapGestureRecognizer extends TapGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
    flog("rejectGesture(拒绝响应手势) pointer=$pointer");
    acceptGesture(pointer);
  }
}

GestureRecognizerFactory

GestureRecognizerFactory 用于工厂类的初始化,其提供了手势识别对象创建(_constructor)、以及对应的初始化入口(_initializer)。

Factory for creating gesture recognizers that delegates to callbacks.

GestureRecognizerFactoryWithHandlers 继承自 GestureRecognizerFactory

/// 定义手势识别工厂的初始化逻辑
var _gestureRecognizerFactory = GestureRecognizerFactoryWithHandlers<MyTapGestureRecognizer>(
  _constructor,
  _initializer,
);

/// 提供手势识别对象创建的方法
MyTapGestureRecognizer Function() _constructor = () {
  flog("_constructor");
  return MyTapGestureRecognizer();
};

/// 提供初始化方法
_initializer(MyTapGestureRecognizer instance) {
  flog('_initializer');
  instance.onTap = () => flog('instance.onTap');
}

建立映射关系

建立 手势识别器 GestureRecognizer 与 手势识别工厂 GestureRecognizerFactory 的映射关系:

/// 用于建立 手势识别器 GestureRecognizer 与 手势识别工厂 GestureRecognizerFactory 的映射关系
Map<Type, GestureRecognizerFactory<MyTapGestureRecognizer>> _gestures = {
  MyTapGestureRecognizer: _gestureRecognizerFactory,
};

RawGestureDetector

将手势识别器和其工厂类 通过 属性 gestures 传递给 RawGestureDetector,以便用户产生手势交互事件时能够立刻找到对应的识别方法。事实上,RawGestureDetector 的初始化函数所做的配置工作,就是定义不同手势识别器和其工厂类的映射关系。

由于只需要在父容器监听子容器的点击事件,所以只需要将父容器用 RawGestureDetector 包装起来就可以了,而子容器保持不变。

RawGestureDetector(
  gestures: _gestures, // 构造父 Widget 的手势识别映射关系
  child: Container(
    color: Colors.green,
    child: Center(
      child: GestureDetector(
        onTap: () => flog('子视图的点击回调'), // 子视图可以继续使用 GestureDetector
        child: Container(color: Colors.blue, width: 200, height: 200),
      ),
    ),
  ),
),

效果

// 初始化
[log] _constructor
[log] _initializer
[log] _initializer

// 点击子视图区域
[log] 子视图的点击回调
[log] rejectGesture(拒绝响应手势) pointer=7
[log] instance.onTap

// 点击父视图区域
[log] instance.onTap

总结

在 Flutter 中,尽管可以对一个 Widget 监听多个手势,或是对多个 Widget 监听同一个手势,但 Flutter 会使用手势竞技场来进行各个手势的 PK,以保证最终只会有一个手势能够响应用户行为。如果希望同时能有多个手势去响应用户行为,需要去自定义手势,利用 RawGestureDetector 和手势工厂类,在竞技场 PK 失败时,手动把它复活。

在处理多个手势识别场景时,很容易出现手势冲突的问题,如果想要精确地处理复杂交互手势,势必需要介入手势识别过程解决异常。

注意:冲突的只是手势的语义化识别过程,原始指针事件是不会冲突的。所以,在通过手势很难搞定的复杂场景,也可以通过 Listener 直接识别原始指针事件的方式解决手势识别的冲突。

2023-1-7

posted @ 2023-01-07 17:52  白乾涛  阅读(552)  评论(0编辑  收藏  举报