Flutter中用户交互事件
手势操作在 Flutter 中分为两类:
- 第一类是原始的指针事件(Pointer Event),即原生开发中常见的触摸事件,表示屏幕上触摸(或鼠标、手写笔)行为触发的位移行为;
- 第二类则是手势识别(Gesture Detector),表示多个原始指针事件的组合操作,如点击、双击、长按等,是指针事件的语义化封装。
指针事件
Listener({
Key key,
this.onPointerDown, //手指按下回调
this.onPointerMove, //手指移动回调
this.onPointerUp,//手指抬起回调
this.onPointerCancel,//触摸事件取消回调
this.behavior = HitTestBehavior.deferToChild, //在命中测试期间如何表现
Widget child
})
指针事件表示用户交互的原始触摸数据,如手指接触屏幕 PointerDownEvent、手指在屏幕上移动 PointerMoveEvent、手指抬起 PointerUpEvent,以及触摸取消 PointerCancelEvent,这与原生系统的底层触摸事件抽象是一致的。
在手指接触屏幕,触摸事件发起时,Flutter 会确定手指与屏幕发生接触的位置上究竟有哪些组件,并将触摸事件交给最内层的组件去响应。与浏览器中的事件冒泡机制类似,事件会从这个最内层的组件开始,沿着组件树向根节点向上冒泡分发。
不过 Flutter 无法像浏览器冒泡那样取消或者停止事件进一步分发,我们只能通过 hitTestBehavior 去调整组件在命中测试期内应该如何表现,比如把触摸事件交给子组件,或者交给其视图层级之下的组件去响应。
Listener(
child: Container(
color: Colors.red,// 背景色红色
width: 300,
height: 300,
),
onPointerDown: (PointerDownEvent event) {
// 手势按下回调
print("down $event");
},
onPointerMove: (PointerMoveEvent event){
// 手势移动回调
print("move $event");
},
onPointerUp: (PointerUpEvent event){
// 手势抬起回调
print("up $event");
},
)
我们试着在红色正方形区域内进行触摸点击、移动、抬起,可以看到 Listener 监听到了一系列原始指针事件,并打印出了这些事件的位置信息:
flutter: down PointerDownEvent#b96b7(position: Offset(194.0, 437.7))
flutter: up PointerUpEvent#80dfc(position: Offset(194.0, 437.7))
flutter: down PointerDownEvent#d6970(position: Offset(160.3, 491.7))
参数PointerDownEvent、PointerMoveEvent、PointerUpEvent都是PointerEvent的一个子类,PointerEvent类中包括当前指针的一些信息,如:
- position:它是鼠标相对于当对于全局坐标的偏移。
- delta:两次指针移动事件(PointerMoveEvent)的距离。
- pressure:按压力度,如果手机屏幕支持压力传感器(如iPhone的3D Touch),此属性会更有意义,如果手机不支持,则始终为1。
- orientation:指针移动方向,是一个角度值。
behavior属性
behavior属性,它决定子组件如何响应命中测试,它的值类型为HitTestBehavior,这是一个枚举类,有三个枚举值:
-
deferToChild:子组件会一个接一个的进行命中测试,如果子组件中有测试通过的,则当前组件通过,这就意味着,如果指针事件作用于子组件上时,其父级组件也肯定可以收到该事件。
-
opaque:在命中测试时,将当前组件当成不透明处理(即使本身是透明的),最终的效果相当于当前Widget的整个区域都是点击区域。举个例子:
class TestOpaque extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: red,
child: 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") 。 如果我们想让整个300×150的矩形区域都能点击我们可以将behavior设为HitTestBehavior.opaque。注意,该属性并不能用于在组件树中拦截(忽略)事件,它只是决定命中测试时的组件大小。
- translucent:当点击组件透明区域时,可以对自身边界内及底部可视区域都进行命中测试,这意味着点击顶部组件透明区域时,顶部组件和底部组件都可以接收到事件,例如
class TestTranslucent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return 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, //放开此行注释后可以"点透"
)
],
);
}
}
当注释掉behavior: HitTestBehavior.translucent
,在左上角200*100范围内非文本区域点击时(顶部组件透明区域),控制台只会打印“down0”,也就是说顶部组件没有接收到事件,而只有底部接收到了。当放开注释后,再点击时顶部和底部都会接收到事件,此时会打印
flutter: down0
flutter: down1
忽略事件
有的时候,我们不需要相应PointEvent,这个时候可以使用IgnorePointer和AbsorbPointer。但是两个是有区别的
- IgnorePointer:此节点和其子节点都惊忽略点击事件
- AbsorbPointer:这个控件本身是能够响应点击事件的,他做的是阻止事件传递到他的子节点上
class IgnoreWidget extends StatefulWidget {
@override
_IgnoreWidget createState() => new _IgnoreWidget();
}
class _IgnoreWidget extends State<IgnoreWidget> {
bool _ignore = true;
@override
Widget build(BuildContext context) {
return Center(
child:Column(
children: <Widget>[
Switch(value: _ignore, onChanged: (value){
setState(() {
_ignore = value;
});
}),
GestureDetector(
onTap: () => print("GestureDetector Clicked"),
child: IgnorePointer(
ignoring: _ignore,
child: RaisedButton(
onPressed: () => print("IgnorePointer clicked"),
child: Text("IgnorePointer"),
),
),
),
GestureDetector(
onTap: () => print("GestureDetector Clicked"),
child: AbsorbPointer(
absorbing: _ignore,
child: RaisedButton(
onPressed: () => print("AbsorbPointer clicked"),
child: Text("AbsorbPointer"),
),
),
)
],
)
);
}
}
当我们开始忽略事件时,点击AbsorbPointer,控制台就会打印。而点击IgnorePointer按钮之后,控制台不会打印
手势识别
1、GestureDetector
GestureDetector是一个用于手势识别的功能性组件,我们通过它可以来识别各种手势。GestureDetector实际上是指针事件的语义化封装
- 点击事件
- onTapDown:按下时回调。
- onTapUp:抬起时回调。
- onTap:点击事件回调。
- onTapCancel:点击取消事件回调。
- 双击事件:双击是快速且连续2次在同一个位置点击,双击监听使用onDoubleTap方法
- 长按事件:长按事件(LongPress)包含长按开始、移动、抬起、结束事件
- onLongPressStart:长按开始事件回调
- onLongPressMoveUpdate:长按移动事件回调。
- onLongPressUp:长按抬起事件回调。
- onLongPressEnd:长按结束事件回调。
- onLongPress:长按事件回调。
- 拖动事件
- onPanDown:手指按下时会触发此回调
- onPanUpdate:手指滑动时会触发此回调
- onPanEnd:结束回调
- 水平/垂直拖动事件:垂直/水平拖动事件包括按下、开始、移动更新、结束、取消事件
- onVerticalDragDown:垂直拖动按下事件回调
- onVerticalDragStart:垂直拖动开始事件回调
- onVerticalDragUpdate:指针移动更新事件回调
- onVerticalDragEnd:垂直拖动结束事件回调
- onVerticalDragCancel:垂直拖动取消事件回调
- 缩放事件:缩放(Scale)包含缩放开始、更新、结束。
- onScaleStart:缩放开始事件回调。
- onScaleUpdate:缩放更新事件回调。
- onScaleEnd:缩放结束事件回调。
class _GestureDetectorWidgetState extends State<GestureDetectorWidget> {
// 红色 container 坐标
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: Container(color: Colors.red,width: 50,height: 50),// 红色子视图
onTap: ()=>print("Tap"),// 点击回调
onDoubleTap: ()=>print("Double Tap"),// 双击回调
onLongPress: ()=>print("Long Press"),// 长按回调
//手指按下时会触发此回调
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);
},
),
)
],
);
}
}
- DragDownDetails.globalPosition:当用户按下时,此属性为用户按下的位置相对于屏幕(而非父组件)原点(左上角)的偏移。
- DragUpdateDetails.delta:当用户在屏幕上滑动时,会触发多次Update事件,delta指一次Update事件的滑动的偏移量。
- DragEndDetails.velocity:该属性代表用户抬起手指时的滑动速度(包含x、y两个轴的),示例中并没有处理手指抬起时的速度,常见的效果是根据用户抬起手指时的速度做一个减速动画
GestureRecognizer
GestureDetector内部是使用一个或多个GestureRecognizer来识别各种手势的,而GestureRecognizer的作用就是通过Listener来将原始指针事件转换为语义手势,GestureDetector直接可以接收一个子widget。GestureRecognizer是一个抽象类,一种手势的识别器对应一个GestureRecognizer的子类,Flutter实现了丰富的手势识别器,我们可以直接使用。
假设我们要给一段富文本(RichText)的不同部分分别添加点击事件处理器,但是TextSpan并不是一个widget,这时我们不能用GestureDetector,但TextSpan有一个recognizer属性,它可以接收一个GestureRecognizer。
class _GestureRecognizerStatusState extends State<GestureRecognizerStatus> {
TapGestureRecognizer _tapGestureRecognizer = new TapGestureRecognizer();
bool _toggle = false;
@override
void dispose() {
//用到GestureRecognizer的话一定要调用其dispose方法释放资源
_tapGestureRecognizer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
child: Text.rich(
TextSpan(
children: [
TextSpan(text: "Hello Flutter "),
TextSpan(
text: "点我有变化",
style: TextStyle(
fontSize: _toggle ? 15.0 : 20.0,
color: Colors.red
),
recognizer: _tapGestureRecognizer
..onTap = () {
setState(() {
_toggle = !_toggle;
});
},
),
]
)
),
);
}
}
注意:使用GestureRecognizer后一定要调用其dispose()方法来释放资源(主要是取消内部的计时器)。
手势竞争与冲突
实际上,GestureDetector 内部对每一个手势都建立了一个工厂类(Gesture Factory)。而工厂类的内部会使用手势识别类(GestureRecognizer),来确定当前处理的手势。
而所有手势的工厂类都会被交给 RawGestureDetector 类,以完成监测手势的大量工作:使用 Listener 监听原始指针事件,并在状态改变时把信息同步给所有的手势识别器,然后这些手势会在竞技场决定最后由谁来响应用户事件。
像这样的手势识别发生在多个存在父子关系的视图时,手势竞技场会一并检查父视图和子视图的手势,并且通常最终会确认由子视图来响应事件。而这也是合乎常理的:从视觉效果上看,子视图的视图层级位于父视图之上,相当于对其进行了遮挡,因此从事件处理上看,子视图自然是事件响应的第一责任人。
在下面的示例中,我定义了两个嵌套的 Container 容器,分别加入了点击识别事件:
GestureDetector(
onTap: () => print('Parent tapped'),// 父视图的点击回调
child: Container(
color: Colors.pinkAccent,
child: Center(
child: GestureDetector(
onTap: () => print('Child tapped'),// 子视图的点击回调
child: Container(
color: Colors.blueAccent,
width: 200.0,
height: 200.0,
),
),
),
),
);
运行这段代码,然后在蓝色区域进行点击,可以发现:尽管父容器也监听了点击事件,但 Flutter 只响应了子容器的点击事件。
为了让父容器也能接收到手势,我们需要同时使用 RawGestureDetector 和 GestureFactory,来改变竞技场决定由谁来响应用户事件的结果。 在此之前,我们还需要自定义一个手势识别器,让这个识别器在竞技场被 PK 失败时,能够再把自己重新添加回来,以便接下来还能继续去响应用户事件。
在下面的代码中,我定义了一个继承自点击手势识别器 TapGestureRecognizer 的类,并重写了其 rejectGesture 方法,手动地把自己又复活了:
class MultipleTapGestureRecognizer extends TapGestureRecognizer {
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
接下来,我们需要将手势识别器和其工厂类传递给 RawGestureDetector,以便用户产生手势交互事件时能够立刻找到对应的识别方法。事实上,RawGestureDetector 的初始化函数所做的配置工作,就是定义不同手势识别器和其工厂类的映射关系。
这里,由于我们只需要处理点击事件,所以只配置一个识别器即可。工厂类的初始化采用 GestureRecognizerFactoryWithHandlers 函数完成,这个函数提供了手势识别对象创建,以及对应的初始化入口。
在下面的代码中,我们完成了自定义手势识别器的创建,并设置了点击事件回调方法。需要注意的是,由于我们只需要在父容器监听子容器的点击事件,所以只需要将父容器用 RawGestureDetector 包装起来就可以了,而子容器保持不变:
RawGestureDetector(// 自己构造父 Widget 的手势识别映射关系
gestures: {
// 建立多手势识别器与手势识别工厂类的映射关系,从而返回可以响应该手势的 recognizer
MultipleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
MultipleTapGestureRecognizer>(
() => MultipleTapGestureRecognizer(),
(MultipleTapGestureRecognizer instance) {
instance.onTap = () => print('parent tapped ');// 点击回调
},
)
},
child: Container(
color: Colors.pinkAccent,
child: Center(
child: GestureDetector(// 子视图可以继续使用 GestureDetector
onTap: () => print('Child tapped'),
child: Container(
color: Colors.blueAccent,
width: 200.0,
height: 200.0
)
),
),
),
);
运行一下这段代码,我们可以看到,当点击蓝色容器时,其父容器也收到了 Tap 事件
flutter: Child tapped
flutter: parent tapped
转载自:https://juejin.cn/post/6919402878754881543