手势识别原理
手势识别和处理都是在事件分发阶段的,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)的概念:
- 每一个手势识别器(GestureRecognizer)都是一个竞争者(GestureArenaMember),当方式指针事件时,它们都要在竞技场中竞争本次事件的处理权,默认情况最终只有一个竞争者会win。
- GestureRecognizer的handleEvent中会识别手势,如果手势发生了某个手势,竞争者可以宣布自己是否胜出,一旦有一个竞技者胜出,竞技场管理者(GestureArenaManager)就会通知其他竞争者失败。
- 胜出者的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,相当于跳出了手势识别的规则,直接监听原始的触摸事件。
- 自定义手势识别器
- 通过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,
),
),
),
);