Pointer Event
指针事情(也称为触摸事情) 分为三个阶段
- 手指按下
- 手指移动
- 手指抬起
Listener 组件
Flutter中运用Listener Widget来监听指针事情
Listener({
Key key,
this.onPointerDown, //手指按下回调
this.onPointerMove, //手指移动回调
this.onPointerUp,//手指抬起回调
this.onPointerCancel,//指针事情撤销回调
Widget child // 子组件
})
// 检查手指相关于容器的方位
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自身不会参与
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 列表。
子Render Object比父object先呼应事情。
因为hit test依照深度优先遍历的,所以子object会比父object先参加 HitTestResult 列表。
// 事情处理流程
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过程
@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(事情分发)
// 事情分发
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
点击、双击、长按
// 运用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的示例:
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);
},
),
)
],
);
}
}
I/flutter ( 8513): 用户手指按下:Offset(26.3, 101.8)
I/flutter ( 8513): Velocity(235.5, 125.8)
缩放
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来辨认手势
// 点击富文本时文本变色
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 办法实现:
@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,
);
}
@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 的几个相关办法
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,它能够左右拖动,现在咱们也想检测在它上面手指按下和抬起的事情,代码如下:
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”拖动然后抬起手指,控制台日志如下:
I/flutter (17539): down
I/flutter (17539): onHorizontalDragEnd
没有打印”up”,因为在拖动时,刚开端按下手指且没有移动时,拖动手势还没有完整的语义,此时TapDown手势胜出(win),打印”down”,而拖动时,拖动手势会胜出,当手指抬起时,onHorizontalDragEnd 和 onTapUp产生了抵触,因为是在拖动的语义中,所以onHorizontalDragEnd胜出,就会打印 “onHorizontalDragEnd”。
处理手势抵触
有如下两种办法:
1. Listener
竞赛只是针对手势的,而 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");
},
),
),
)
2. 自定义 Recognizer
自定义手势辨认器(Recognizer),重写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,
),
),
),
);