Flutter-手势识别原理

251 阅读7分钟

手势识别原理

手势识别和处理都是在事件分发阶段的,GestureDetector是一个StatelessWidget,包含了RawGestureDetector:

@override
Widget build(BuildContext context) {
  final  gestures = <Type, GestureRecognizerFactory>{};
  // 构建 TapGestureRecognizer 
  if (onTapDown != null ||
      onTapUp != null ||
      onTap != null ||
      ... //省略
  ) {
    gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
      () => TapGestureRecognizer(debugOwner: this),
      (TapGestureRecognizer instance) {
        instance
          ..onTapDown = onTapDown
          ..onTapUp = onTapUp
          ..onTap = onTap
          //省略
      },
    );
  }

  
  return RawGestureDetector(
    gestures: gestures, // 传入手势识别器
    behavior: behavior, // 同 Listener 中的 HitTestBehavior
    child: child,
  );
}

RawGestureDetector中会通过Listener组件监听PointerDownEvent事件:

@override
Widget build(BuildContext context) {
  ... // 省略无关代码
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
}  
 
void _handlePointerDown(PointerDownEvent event) {
  for (final GestureRecognizer recognizer in _recognizers!.values)
    recognizer.addPointer(event);
}  

TapGestureRecognizer几个相关方法:

class CustomTapGestureRecognizer1 extends TapGestureRecognizer {

  void addPointer(PointerDownEvent event) {
    //会将 handleEvent 回调添加到 pointerRouter 中
    GestureBinding.instance!.pointerRouter.addRoute(event.pointer, handleEvent);
  }
  
  @override
  void handleEvent(PointerEvent event) {
    //会进行手势识别,并决定是是调用 acceptGesture 还是 rejectGesture,
  }
  
  @override
  void acceptGesture(int pointer) {
    // 竞争胜出会调用
  }

  @override
  void rejectGesture(int pointer) {
    // 竞争失败会调用
  }
}

可以看到当PointerDownEvent事件触发时,会调用TapGestureRecognizer的addPointer,在addPointer中会将handleEvent方法添加到pointerRouter中保存起来。这样一来,当手势发生变化时只需要在pointerRouter中取出GestureRecognizer的handleEvent方法进行手势识别即可。

正常情况下应该是手势直接作用的对象应该来处理手势,所以一个简单的原则就是同一个手势应该只有一个手势识别器生效,为此,手势识别才映入了手势竞技场(Arena)的概念:

  1. 每一个手势识别器(GestureRecognizer)都是一个竞争者(GestureArenaMember),当方式指针事件时,它们都要在竞技场中竞争本次事件的处理权,默认情况最终只有一个竞争者会win。
  2. GestureRecognizer的handleEvent中会识别手势,如果手势发生了某个手势,竞争者可以宣布自己是否胜出,一旦有一个竞技者胜出,竞技场管理者(GestureArenaManager)就会通知其他竞争者失败。
  3. 胜出者的acceptGesture会被调用,其余的rejectGesture将会被调用。

上一节讲过命中测试是从RenderBinding的hitTest开始的:

@override
void hitTest(HitTestResult result, Offset position) {
  // 从根节点开始进行命中测试
  renderView.hitTest(result, position: position); 
  // 会调用 GestureBinding 中的 hitTest()方法,我们将在下一节中介绍。
  super.hitTest(result, position); 
}
渲染树命中测试完成后会调用GestureBinding中的hitTest()方法:
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
  result.add(HitTestEntry(this));
}

如果GestureBinding也通过命中测试了,这样的话在事件分发阶段,GestureBinding的handleEvent便也会被调用,由于它是最后被添加到HitTestResult中,所以在事件分发阶段GestureBinding的handleEvent会在最后被调用:

@override 
void handleEvent(PointerEvent event, HitTestEntry entry) {
  // 会调用在 pointerRouter 中添加的 GestureRecognizer 的 handleEvent
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
    // 分发完毕后,关闭竞技场
    gestureArena.close(event.pointer);
  } else if (event is PointerUpEvent) {
    gestureArena.sweep(event.pointer);
  } else if (event is PointerSignalEvent) {
    pointerSignalResolver.resolve(event);
  }
}

gestureArena是GestureArenaManager类实例,负责管理竞技场。

上面关键代码是第一行,功能是会调用之前在pointerRouter中添加GestureRecognizer的handleEvent,不同GestureRecognizer的handleEvent会识别不同的手势,然后它会和gestureArena交互(如果当前的GesrtureRecognizer胜出,需要gestureArean去通知其他竞争者失败了),最终,如果当前GestureRecognizer胜出,则最终它的acceptGesture会被调用,如果失败则其rejectGesture将会被调用,因为这部分代码不同的GestureRecognizer会不同,可以自行查看源码。

手势竞争

如果一个组件同时监听了水平和垂直方向的拖动手势,当我们斜着拖动时,那个方向的拖动手势回调会被触发?实际上取决于第一次移动时两个轴上的位移分量,哪个轴的大,哪个轴在本次滑动竞争中就胜出。

实例:

GestureDetector( //GestureDetector2
  onTapUp: (x)=>print("2"), // 监听父组件 tapUp 手势
  child: Container(
    width:200,
    height: 200,
    color: Colors.red,
    alignment: Alignment.center,
    child: GestureDetector( //GestureDetector1
      onTapUp: (x)=>print("1"), // 监听子组件 tapUp 手势
      child: Container(
        width: 50,
        height: 50,
        color: Colors.grey,
      ),
    ),
  ),
);

当点击灰色区域时,会打印1而不会打印2,因为手指抬起后,GestureDetectore1和TestureDetector2会发生竞争,判定获胜的规则是子组件优先,所以GestuerDetector1获胜,GestureDetectore2会被忽略。

class _BothDirectionTest extends StatefulWidget {
  @override
  _BothDirectionTestState createState() => _BothDirectionTestState();
}

class _BothDirectionTestState extends State<_BothDirectionTest> {
  double _top = 0.0;
  double _left = 0.0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //垂直方向拖动事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _top += details.delta.dy;
              });
            },
            onHorizontalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _left += details.delta.dx;
              });
            },
          ),
        )
      ],
    );
  }
}

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

多手势冲突

当一个GestureDetector监听多种手势时,也可能会产生冲突。假设一个widget,它可以左右拖动,现在想检测在它上面手指按下和抬起事件。

实例:

class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
  double _left = 0.0;
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          left: _left,
          child: GestureDetector(
              child: CircleAvatar(child: Text("A")), //要拖动和点击的widget
              onHorizontalDragUpdate: (DragUpdateDetails details) {
                setState(() {
                  _left += details.delta.dx;
                });
              },
              onHorizontalDragEnd: (details){
                print("onHorizontalDragEnd");
              },
              onTapDown: (details){
                print("down");
              },
              onTapUp: (details){
                print("up");
              },
          ),
        )
      ],
    );
  }
}

发现没有打印‘up’,这是因为在拖动时,刚开始按下手指且没有移动时,拖动手势还没有完整的语义,此时TapDown手势胜出,打印down,而拖动时,拖动手势会胜出,当手指抬起时,onHorizontalDragEnd和onTapUp发生了冲突,但是因为是在拖动的语义中,所以onHorizontalDragEnd胜出,会打印‘onHorizontalDragEnd’。

如果在代码中对于手指按下和抬起是强依赖时,比如在一个轮播图组件中,希望手指按下时,暂停轮播,而抬起时恢复轮播,但是由于轮播组件本身可能已经处理了手势拖动(支持手动滑动切换),甚至可能也支持了缩放手势,这时如果在外部再用onTapDown、onTap来监听的话是不行的。这时通过Listener监听原始指针事件就行:

Positioned(
  top:80.0,
  left: _leftB,
  child: Listener(
    onPointerDown: (details) {
      print("down");
    },
    onPointerUp: (details) {
      //会触发
      print("up");
    },
    child: GestureDetector(
      child: CircleAvatar(child: Text("B")),
      onHorizontalDragUpdate: (DragUpdateDetails details) {
        setState(() {
          _leftB += details.delta.dx;
        });
      },
      onHorizontalDragEnd: (details) {
        print("onHorizontalDragEnd");
      },
    ),
  ),
)

解决手势冲突

手势是对原始指针的语义化的识别,手势冲突只是手势级别的,也就是只会在组件树中的多个GestureDetector之间才有冲突的场景,如果压根就没有使用GestureDetector则不存在所谓的冲突,因为每一个节点都能收到事件。只是在GestureDetector中为了识别语义,它会去决定哪些子节点应该忽略事件,哪些可以生效。

解决手势冲突的方法有两种:

  • 使用Listenre,相当于跳出了手势识别的规则,直接监听原始的触摸事件。
  • 自定义手势识别器
  1. 通过Listener解决手势冲突 通过Listener解决手势冲突的原因是竞争只会针对于手势,而Listener是监听原始指针事件,原始指针事件并非语义化的手势,所以不会走手势竞争的逻辑。也就不会相互影响。
Listener(  // 将 GestureDetector 换位 Listener 即可
  onPointerUp: (x) => print("2"),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.red,
    alignment: Alignment.center,
    child: GestureDetector(
      onTap: () => print("1"),
      child: Container(
        width: 50,
        height: 50,
        color: Colors.grey,
      ),
    ),
  ),
);

Listener直接识别原始指针事件来解决冲突的方法很简单,因此,当遇到手势冲突时,优先考虑Listenre。 2. 通过自定义Recognizer解决手势冲突 自定义手势识别器比较麻烦,原理是当确定手势竞争者胜出时,会调用胜出者的acceptGesture方法,表示宣布成功。然后会调用其他手势识别其的rejectGesture方法,表示宣布失败。因此,我们可以自定义手势识别器,然后重写它的rejectGesture方法,在里面调用acceptgesture方法,相当于它失败是强制将它变成竞争成功。

class CustomTapGestureRecognizer extends TapGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
    //不,我不要失败,我要成功
    //super.rejectGesture(pointer);
    //宣布成功
    super.acceptGesture(pointer);
  }
}

//创建一个新的GestureDetector,用我们自定义的 CustomTapGestureRecognizer 替换默认的
RawGestureDetector customGestureDetector({
  GestureTapCallback? onTap,
  GestureTapDownCallback? onTapDown,
  Widget? child,
}) {
  return RawGestureDetector(
    child: child,
    gestures: {
      CustomTapGestureRecognizer:
          GestureRecognizerFactoryWithHandlers<CustomTapGestureRecognizer>(
        () => CustomTapGestureRecognizer(),
        (detector) {
          detector.onTap = onTap;
        },
      )
    },
  );
}

使用实例:

customGestureDetector( // 替换 GestureDetector
  onTap: () => print("2"),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.red,
    alignment: Alignment.center,
    child: GestureDetector(
      onTap: () => print("1"),
      child: Container(
        width: 50,
        height: 50,
        color: Colors.grey,
      ),
    ),
  ),
);