Pointer Event
指针事件(也称为触摸事件) 分为三个阶段
- 手指按下
- 手指移动
- 手指抬起
Listener 组件
Flutter中使用Listener Widget来监听指针事件
kotlin
Listener({
Key key,
this.onPointerDown, //手指按下回调
this.onPointerMove, //手指移动回调
this.onPointerUp,//手指抬起回调
this.onPointerCancel,//指针事件取消回调
Widget child // 子组件
})
less
// 查看手指相对于容器的位置
class _PointerMoveIndicatorState extends State<PointerMoveIndicator> {
PointerEvent? _event;
@override
Widget build(BuildContext context) {
return Listener(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: 300.0,
height: 150.0,
child: Text(
'${_event?.localPosition ?? ''}',
style: TextStyle(color: Colors.white),
),
),
onPointerDown: (PointerDownEvent event) => setState(() => _event = event),
onPointerMove: (PointerMoveEvent event) => setState(() => _event = event),
onPointerUp: (PointerUpEvent event) => setState(() => _event = event),
);
}
}
PointerDownEvent、 PointerMoveEvent、 PointerUpEvent 都是PointerEvent
的子类 PointerEvent参数
- position:指针的全局坐标
- localPosition: 指针本身布局坐标。
- delta:两次PointerMoveEvent的间距。
忽略指针事件
- 使用IgnorePointer和AbsorbPointer,让某个子树不响应PointerEvent
- AbsorbPointer
本身
会参与Hit Test(响应指针事件),但其子树不行
,而IgnorePointer本身不会参与
less
Listener(
child: AbsorbPointer(
child: Listener(
child: Container(
color: Colors.red,
width: 200.0,
height: 100.0,
),
onPointerDown: (event)=>print("in"),
),
),
onPointerDown: (event)=>print("up"),
)
点击Container时,由于它在AbsorbPointer的子树上,所以不会响应指针事件,所以日志不会输出"in",但AbsorbPointer本身是可以接收指针事件的,所以会输出"up"。如果将AbsorbPointer换成IgnorePointer,那么两个都不会输出。
事件处理流程
-
hit test
当手指按下时,触发 PointerDownEvent 事件, 遍历当前渲染对象(render object)树,对每一个object进行hit test(命中测试),如果hit test通过,则该object添加到
HitTestResult
列表当中 -
event dispatch
(事件分发)2.1 hit test完毕后,遍历 HitTestResult 列表,调用每一个Render Object的事件处理方法(handleEvent)
2.2 随后当手指移动时,便会分发 PointerMoveEvent 事件。
-
事件结束
当手指抬( PointerUpEvent )起或事件取消时(PointerCancelEvent),清空 HitTestResult 列表。
css
子Render Object比父object先响应事件。
因为hit test按照深度优先遍历的,所以子object会比父object先加入 HitTestResult 列表。
csharp
// 事件处理流程
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent ) {
hitTestResult = HitTestResult();
// 发起命中测试
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
//获取命中测试的结果,然后移除
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) { // PointerMoveEvent
//获取命中测试的结果
hitTestResult = _hitTests[event.pointer];
}
// 事件分发
if (hitTestResult != null) {
dispatchEvent(event, hitTestResult);
}
}
hit test过程
arduino
@override
void hitTest(HitTestResult result, Offset position) {
//从渲染树(RenderObject)根节点开始按照深度优先顺序递归进行命中测试
renderView.hitTest(result, position: position);
// 调用 GestureBinding 的 hitTest()方法
super.hitTest(result, position);
}
// Render View的 hit test过程
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(result, position: position); //递归对子树进行命中测试
//Render View根节点始终被添加到HitTestResult列表中
result.add(HitTestEntry(this));
return true;
}
// 以RenderBox 为例说明child的hitTest过程
bool hitTest(HitTestResult result, { @required Offset position }) {
if (_size.contains(position)) { // 判断事件的触发位置是否位于组件范围内
// hitTestChildren: 判断有没有子节点通过hit test
// 重写hitTestSelf函数并返回true,"强行声明"当前节点通过了命中测试
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true; // 当前节点通过hit test
}
}
return false;
}
event dispatch(事件分发)
csharp
// 事件分发
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
// 遍历HitTestResult,调用每一个节点的 handleEvent
for (final HitTestEntry entry in hitTestResult.path) {
entry.target.handleEvent(event.transformed(entry.transform), entry);
}
}
所以组件只需重写 handleEvent
就可以处理事件。
GestureDetector(手势识别控件)
- 用于手势识别的
Widget
- 内部封装了 Listener
点击、双击、长按
less
// 使用GestureDetector对Container进行手势识别,触发后, 在Container上显示事件名
class _GestureTestState extends State<GestureTest> {
String _operation = "No Gesture detected!"; //保存事件名
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: 200.0,
height: 100.0,
child: Text(
_operation,
style: TextStyle(color: Colors.white),
),
),
onTap: () => updateText("Tap"), //点击
onDoubleTap: () => updateText("DoubleTap"), //双击
onLongPress: () => updateText("LongPress"), //长按
),
);
}
void updateText(String text) {
//更新显示的事件名
setState(() {
_operation = text;
});
}
}
拖动、滑动
- GestureDetector 对于拖动和滑动事件是没有区分的,本质上是一样的。
- GestureDetector 会将要监听的组件的原点(左上角)作为本次手势的原点,当用户在监听的组件上按下手指时,手势识别就会开始。下面我们看一个拖动圆形字母A的示例:
scala
class _Drag extends StatefulWidget {
@override
_DragState createState() => _DragState();
}
class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
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")),
//手指按下时会触发此回调
onPanDown: (DragDownDetails e) {
//打印手指按下的位置(相对于屏幕,不是父组件)
print("用户手指按下:${e.globalPosition}");
},
//手指滑动时会触发此回调
onPanUpdate: (DragUpdateDetails e) {
//用户手指滑动时,更新偏移,重新构建
setState(() {
_left += e.delta.dx;
_top += e.delta.dy;
});
},
onPanEnd: (DragEndDetails e){
//打印滑动结束时在x、y轴上的速度
print(e.velocity);
},
),
)
],
);
}
}
css
I/flutter ( 8513): 用户手指按下:Offset(26.3, 101.8)
I/flutter ( 8513): Velocity(235.5, 125.8)
缩放
scala
class _Scale extends StatefulWidget {
const _Scale({Key? key}) : super(key: key);
@override
_ScaleState createState() => _ScaleState();
}
class _ScaleState extends State<_Scale> {
double _width = 200.0; //通过修改图片宽度来达到缩放效果
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
//指定宽度,高度自适应
child: Image.asset("./images/sea.png", width: _width),
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
//缩放倍数在0.8到10倍之间
_width=200*details.scale.clamp(.8, 10.0);
});
},
),
);
}
}
GestureRecognizer(手势识别器)
- GestureDetector内部是使用GestureRecognizer来识别各种手势的
- GestureRecognizer通过Listener将指针事件转换为手势 TextSpan不是一个widget,但是可以接收GestureRecognizer来识别手势
scala
// 点击富文本时文本变色
import 'package:flutter/gestures.dart';
class _GestureRecognizer extends StatefulWidget {
const _GestureRecognizer({Key? key}) : super(key: key);
@override
_GestureRecognizerState createState() => _GestureRecognizerState();
}
class _GestureRecognizerState extends State<_GestureRecognizer> {
TapGestureRecognizer _tapGestureRecognizer = TapGestureRecognizer();
bool _toggle = false; //变色开关
@override
void dispose() {
//用到GestureRecognizer的话一定要调用其dispose方法释放资源
_tapGestureRecognizer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Text.rich(
TextSpan(
children: [
TextSpan(text: "你好世界"),
TextSpan(
text: "点我变色",
style: TextStyle(
fontSize: 30.0,
color: _toggle ? Colors.blue : Colors.red,
),
recognizer: _tapGestureRecognizer
..onTap = () {
setState(() {
_toggle = !_toggle;
});
},
),
TextSpan(text: "你好世界"),
],
),
),
);
}
}
手势识别原理
手势的识别是在事件分发阶段的,GestureDetector 是一个 StatelessWidget, 包含了 RawGestureDetector,我们看一下它的 build 方法实现:
ini
@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,
child: child,
);
}
scss
@override
Widget build(BuildContext context) {
// 使用 Listener 监听指针事件
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 的几个相关方法
less
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 方法进行手势识别。
-
同一个手势应该只有一个GestureRecognizer生效,所以引入手势竞技场(Arena)的概念
3.1 每一个GestureRecognizer都是一个GestureArenaMember,当发生指针事件时,他们都要在Arena去竞争本次事件的处理权,最终只有一个"竞争者"会胜出(win)。
3.2 竞技场管理者(GestureArenaManager)就会通知其他竞争者失败。
3.3 胜出者的 acceptGesture 会被调用,其余的 rejectGesture 将会被调用。
手势竞争的两个例子
-
如果一个组件同时监听水平和垂直方向的拖动手势,当斜着拖动时哪个方向的拖动手势回调会被触发?
取决于第一次移动时两个轴上的位移分量,哪个轴的大,哪个轴在本次滑动事件竞争中就胜出。
-
一个ListView,它有个ListView子组件,如果滑动这个子ListView,因为
子ListView胜出
而获得滑动事件的处理权, 所以父ListView不会滑动。
多手势冲突
由于手势竞争最终只有一个胜出者,所以通过 GestureDetector 监听多种手势时,可能会产生冲突。假设有一个widget,它可以左右拖动,现在我们也想检测在它上面手指按下和抬起的事件,代码如下:
less
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");
},
),
)
],
);
}
}
现在按住圆形"A"拖动然后抬起手指,控制台日志如下:
bash
I/flutter (17539): down
I/flutter (17539): onHorizontalDragEnd
没有打印"up",因为在拖动时,刚开始按下手指且没有移动时,拖动手势还没有完整的语义,此时TapDown手势胜出(win),打印"down",而拖动时,拖动手势会胜出,当手指抬起时,onHorizontalDragEnd 和 onTapUp发生了冲突,因为是在拖动的语义中,所以onHorizontalDragEnd胜出,就会打印 "onHorizontalDragEnd"。
解决手势冲突
有如下两种方法:
1. Listener
竞争只是针对手势的,而 Listener 是监听原始指针事件,指针事件并非语义的手势,所以不会走手势竞争的逻辑,所以不会相互影响。
less
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");
},
),
),
)
2. 自定义 Recognizer
自定义手势识别器(Recognizer),重写rejectGesture 方法:在里面调用acceptGesture 方法,强制变成竞争的成功者了,这样它的回调也就会执行。
scala
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;
},
)
},
);
}
less
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,
),
),
),
);