1. 命中测试简介
在移动端,各个平台或UI系统的原始指针事件模型基本都是一致,即:一次完整的事件分为三个阶段:手指按下、手指移动、和手指抬起,而更高级别的手势(如点击、双击、拖动等)都是基于这些原始事件的。
当指针按下时,Flutter会对应用程序执行命中测试(Hit Test),以确定指针与屏幕接触的位置存在哪些组件(widget), 指针按下事件(以及该指针的后续事件)然后被分发到由命中测试发现的最内部的组件,然后从那里开始,事件会在组件树中向上冒泡,这些事件会从最内部的组件被分发到组件树根的路径上的所有组件,这和Web开发中浏览器的事件冒泡机制相似, 但是Flutter中没有机制取消或停止“冒泡”过程,而浏览器的冒泡是可以停止的。注意,只有通过命中测试的组件才能触发事件
2. Listener 组件
Listener 定义
const Listener({
Key? key,
this.onPointerDown, // 手指按下回调
this.onPointerMove, // 手指移动回调
this.onPointerUp, // 手指抬起回调
this.onPointerHover, // 悬停
this.onPointerCancel, // 触摸事件取消回调
this.onPointerSignal, //
this.behavior = HitTestBehavior.deferToChild,
Widget? child,
})
示例
class MSListenerDemo1 extends StatefulWidget {
const MSListenerDemo1({Key? key}) : super(key: key);
@override
State<MSListenerDemo1> createState() => _MSListenerDemo1State();
}
class _MSListenerDemo1State extends State<MSListenerDemo1> {
PointerEvent? _event;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("ListenerDemo1")),
body: Center(
child: Listener(
onPointerDown: (PointerDownEvent event) =>
setState(() => _event = event),
onPointerMove: (PointerMoveEvent event) =>
setState(() => _event = event),
onPointerUp: (PointerUpEvent event) => setState(() => _event = event),
child: Container(
color: Colors.amber,
width: 200,
height: 200,
alignment: Alignment.center,
child: Text("${_event?.localPosition ?? ''} "),
),
),
),
);
}
}
image.png
手指在橘黄色矩形区域内移动即可看到当前指针偏移
当触发指针事件时,对应的事件会回调,参数 PointerDownEvent、 PointerMoveEvent、 PointerUpEvent 都是PointerEvent的子类。
注意 Pointer,即“指针”, 指事件的触发者,可以是鼠标、触摸板、手指。
PointerEvent类中包括当前指针的一些信息,如:
-
position
:它是指针相对于当对于全局坐标的偏移。 -
localPosition
: 它是指针相对于当对于本身布局坐标的偏移。 -
delta
:两次指针移动事件(PointerMoveEvent)的距离。 -
pressure
:按压力度,如果手机屏幕支持压力传感器(如iPhone的3D Touch),此属性会更有意义,如果手机不支持,则始终为1。 -
orientation
:指针移动方向,是一个角度值。
3. 忽略指针事件
使用IgnorePointer和AbsorbPointer来忽略指针事件
IgnorePointer和AbsorbPointer,这两个组件都能阻止子树接收指针事件,不同之处在于AbsorbPointer本身会参与命中测试,而IgnorePointer本身不会参与,这就意味着AbsorbPointer本身是可以接收指针事件的(但其子树不行),而IgnorePointer不可以
示例 IgnorePointer
class MSListenerDemo2 extends StatelessWidget {
const MSListenerDemo2({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("ListenerDemo2")),
body: Center(
child: Listener(
onPointerDown: (event) => print("out"),
child: IgnorePointer(
child: Listener(
onPointerDown: (event) => print("in"),
child: Container(
color: Colors.amber,
width: 200,
height: 200,
),
),
),
),
),
);
}
}
点击Container时,无点击事件日志打印。
由于Container在IgnorePointer子树上,所以不会响应指针事件,所以日志不会输出"in",又由于IgnorePointer不会参与命中测试,因而日志不会输出"out"
示例2 AbsorbPointer
class MSListenerDemo3 extends StatelessWidget {
const MSListenerDemo3({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("MSListenerDemo3")),
body: Center(
child: Listener(
onPointerDown: (event) => print("out"),
child: AbsorbPointer(
child: Listener(
onPointerDown: (event) => print("in"),
child: Container(
color: Colors.amber,
width: 200,
height: 200,
),
),
),
),
),
);
}
}
点击Container时,日志会输出"out"
由于Container在AbsorbPointer的子树上,所以不会响应指针事件,所以日志不会输出"in",但AbsorbPointer本身是可以接收指针事件的,所以会输出"out"
网友评论