概述
在移动端,各个平台或者 UI 系统的事件模型都是基本一致,即:一次完整的事件分为三个阶段,手指按下,移动,抬起,而其他的双击,拖动等都是基于这些事件的
当指针按下时,Flutter 会对应用程序执行命中测试(Hit Test) ,以确定指针与屏幕接触的位置存在哪些 Widget,指针按下事件(以及该指针的后续事件)会被分发到由命中测试发现的最内部的组件,然后从哪里开始,事件会在组件树中向上冒泡,这些事件会从最内部的组件分发的组件树的根路径上的所有组件,这个 Web 开发浏览器的事件冒泡机制相似,但是 Flutter 中没有机制取消或者停止冒泡过程,而浏览器是可以停止的。
注意:只有通过命中测试的组件才能触发事件
原始指针事件处理
Flutter 中可以使用 Listener 来监听原始触摸事件,按照 中的分类,Listener 也是一个功能性组件,下面是 Listener 的构造函数定义:
Listener({ Key key, this.onPointerDown, //手指按下回调 this.onPointerMove, //手指移动回调 this.onPointerUp,//手指抬起回调 this.onPointerCancel,//触摸事件取消回调 this.behavior = HitTestBehavior.deferToChild, //在命中测试期间如何表现 Widget child })
behavior 在后面专门介绍
示例:
class EventTest extends StatefulWidget { @override _EventTestState createState() => _EventTestState(); } class _EventTestState extends State<EventTest> { PointerEvent _event; @override Widget build(BuildContext context) { return Listener( child: Container( margin: EdgeInsets.only(top: 50), color: Colors.blue, alignment: Alignment.center, child: Text(_event?.toString() ?? "", 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:他是鼠标相对于全局坐标的偏移
delta:两次指针移动事件的距离
pressure:按压力度,如果手机屏幕支持压力传感器,此属性才会有意义,如手机不支持,始终为 1。
orientation:指针移动方向,是一个角度值
上面只是一些常用属性,除了这些还有很多其他属性,可自行查看 API
behavior
他决定子组件如何响应命中测试,他的值为 HitTestBehavior,是一个枚举类,有三个枚举值
deferToChild:子组件会一个一个的进行命中测试,如果子组件中有测试通过的,则当前组件通过,这意味着指针事件作用于子组件时,其父级组件也肯定可以接收到事件
opaque:在命中测试时,将当前组件当初不透明处理(即使本身是透明的),最终的效果相当于当前 Widget 的整个区域都是点击区域。栗子:
Listener( child: ConstrainedBox( constraints: BoxConstraints.tight(Size(300.0, 150.0)), child: Center(child: Text("Box A")), ), //behavior: HitTestBehavior.opaque, onPointerDown: (event) => print("down A") ),
上例子,只有点击文本区域才会触发点击事件,因为 deferToChild 会去子组件判断是否命中测试,该例中子组件就是 Text(“Box A”) 。
如果想让整个 300x150 的区域都能点击,我们可以将 behavior 设为 HitTestBehavior.opaque。
注意:该属性不能用于在组件树中拦截(忽略)事件,他只是决定命中测试时的组件大小
translucent:当组件点击透明区域时,可以对自身边界及底部可视区域都进行命中测试。这意味着点击顶部组件透明区域时,顶部组件和底部组件都可以接收到事件,例如:
Stack( children: <Widget>[ Listener( child: ConstrainedBox( constraints: BoxConstraints.tight(Size(300.0, 200.0)), child: DecoratedBox( decoration: BoxDecoration(color: Colors.blue)), ), onPointerDown: (event) => print("down0"), ), Listener( child: ConstrainedBox( constraints: BoxConstraints.tight(Size(200.0, 100.0)), child: Center(child: Text("左上角200*100范围内非文本区域点击")), ), onPointerDown: (event) => print("down1"), //behavior: HitTestBehavior.translucent, //放开此行注释后可以"点透" ) ], )
上栗中,当注释掉最后一行代码,在左上角200x100 范围内非文本区域点击时(顶部组件透明区域),控制台只会打印 down0,也就是说顶部没有接收到事件,只有底部接收到了
当放开注释后,再点击时顶部和底部都会接收到事件
忽略 PinterEvent
如果我们不想让某个子树响应 PointerEvent ,则可以使用 IgnorePointer 和 AbsorbPointer,这两个组件都能阻止子树接受指针事件,不同之处在于 AbsorbPointer 会参与命中测试,而 IgnorePointer 本身不会参与,这就意味着 AbsorbPointer 本身是可以接受指针事件的(但其子树不行),而 IngorePointer 不可以,例:
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 子树上,所以不会响应指针事件,
但是 AbsorbPoniter 本身是可以接受指针事件的,所以会输出 up,如果将 AbsorbPointer 换成 IgnorePointer,那么两个都不会输出;
手势识别
GestuerDetector
GestureDetector 是一个用于手势识别的功能性组件,我们可以通过它来识别各种手势
GestureDetector 实际上是指针事件的语义化封装,下面我们来看一下各种手势识别。
点击,双击,长按
我们通过 GestureDetector 对 Container 进行手势识别,触发相应事件后,在 Container 上显示事件名,如下:
class _EventTestState extends State<EventTest> { //事件名称 String _operation = ""; @override Widget build(BuildContext context) { return Center( child: GestureDetector( child: Container( width: 200, color: Colors.blue, alignment: Alignment.center, height: 100, child: Text(_operation, style: TextStyle(color: Colors.white,fontSize: 20)), ), onTap: () => upDateText("tap"), //单击 onDoubleTap: () => upDateText("doubleTap"), //双击 onLongPress: () => upDateText("longPress"), //长按 ), ); } void upDateText(String text) { setState(() { _operation = text; }); } }
注意:当同时监听 onTop 和 onDoubleTap 时,当用户触发 tap 事件时,会有 200 毫秒的延时,这是因为可能会再次点击触发双击事件
如果只监听了 onTap,则不会有延时
拖动,滑动
一次完整的手势过程是指用户手指按下到抬起的整个过程,期间,用户按下后可能会移动,也可能不移动。
GestureDetector 对拖动和滑动事件时没有区分的,他们本质是一样的。
GestureDetector 会把要监听的组件的原点(左上角)作为本次手势的原点,当监听组件上手指按下时,手势识别就会开始。例:
class _EventTestState extends State<EventTest> with SingleTickerProviderStateMixin { double _top = 100.0; //距离顶部的偏移 double _left = 100.0; //距离左边的偏移 @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: <Widget>[ Positioned( top: _top, left: _left, child: GestureDetector( child: CircleAvatar(child: Text("A")), //手指按下回调 onPanDown: (DragDownDetails e) { print('用户手指按下 ${e.globalPosition}'); }, //手指滑动回调 onPanUpdate: (DragUpdateDetails e) { //滑动时,更新偏移 print('滑动'); setState(() { _left += e.delta.dx; _top += e.delta.dy; }); }, onPanEnd: (DragEndDetails e) { //滑动结束,打印 x,y轴速度 print(e.velocity); }, ), ) ], ), ); } }
globalPosition:此属性为用户按下时相对于屏幕(非父组件)原点的偏移
delta:当用户在屏幕上滑动时,会触发多次 Update 事件,dalta 指一次 Update 事件滑动的偏移量
velocity:该属性代表用户抬起时的滑动速度(包含x,y两个轴的),上例中没有处理抬起的速度,常见的效果是根据抬起手指的速度做一个减速动画
效果如下:
I/flutter ( 8239): 用户手指按下 Offset(134.9, 280.7) I/flutter ( 8239): 滑动 I/chatty ( 8239): uid=10152(com.flutter.flutter_study) 1.ui identical 302 lines I/flutter ( 8239): 滑动 I/flutter ( 8239): Velocity(-59.6, 244.0)
单一方向拖动
在很多场景中,我们只需要沿着一个方向来拖动,如一个垂直方向的列表
GestureDetector 支持特定方向的手势事件,例如:
Positioned( top: _top, child: GestureDetector( child: CircleAvatar(child: Text("A")), //手指按下回调 onPanDown: (DragDownDetails e) { print('用户手指按下 ${e.globalPosition}'); }, onVerticalDragUpdate: (DragUpdateDetails e) { setState(() { _top += e.delta.dy; }); }, onPanEnd: (DragEndDetails e) { //滑动结束,打印 x,y轴速度 print(e.velocity); }, ), )
修改滑动的那个例子如上即可
缩放
GestureDetector 可以监听缩放事件,如下:
Center( child: GestureDetector( child: Image.asset("./images/avatar.jpg", width: _width), onScaleUpdate: (ScaleUpdateDetails details) { setState(() { //缩放倍数在 0.8 到 10 倍之间 _width = 100 * details.scale.clamp(.8, 10.0); }); }, ), );
上例比较简单,实际中我们可能还需要一些其他功能,如双击放大缩小,执行动画等,有兴趣的可以先尝试一下