参阅书籍:
《Flutter跨平台开发入门与实践》-- 向治洪(著)
7. 事件处理
7.1 原始指针事件
一个完整的原始指针事件主要由手指按下、手指移动、手指抬起以及触摸取消构成,更高基本的手势都基于这些原始事件。
在Flutter的原始指针事件模型中,在手指接触屏幕发起触摸事件时,Flutter会首先确定手指与屏幕发生接触的位置上究竟有哪些组件,然后通过命中测试(Hit Test)交给最内层的组件去响应。
Flutter无法像浏览器冒泡那样取消或者停止事件的进一步分发,只能通过执行命中测试去调整组件的事件触发时机。
PointerDownEvent、PointerMoveEvent和PointerUpEvent是Flutter的原始指针事件的基本组成部分,分别对应手指按下、移动和抬起事件,它们都是PointerEvent的子类。
在Flutter的事件模型中PointerEvent是Flutter原始指针事件的基础类,可以用它获取当前指针的一些信息:
1)position:全局坐标的偏移量;
2)delta:两次指针移动事件的距离;
3)pressure:按压力度,如果手机屏幕支持压力传感器,此属性会返回压力值,如果手机不支持则始终返回1;
4)orientation:指针移动方向,是一个角度值。
对于组件层面的原始指针事件的监听,Flutter提供了一个Listener,可以用它监听包裹的子组件的原始指针事件。
Listener(
onPointerDown: (downPointEvent) {
//按下回调
...
},
onPointerMove: (movePointEvent) {
//移动回调
...
},
onPointerUp: (upPointEvent) {
//抬起回调
...
},
child: Container(
child: Text(‘Listener事件监听’);
)
)
原始指针事件还提供了behavior属性,它决定子组件如何响应命中测试,它的值类型为HitTestBehavior,是一个枚举类型,有3个枚举值:
1)deferToChild:子组件一个接一个地进行命中测试,如果子组件中有通过命中测试的,则当前组件会收到指针事件,并且其父组件会收到指针事件;
2)opaque:在进行命中测试时,当前组件会被当成不透明进行处理,单击的响应区域即为单击区域;
3)translucent:设置此属性后,组件自身和底部可视区域都能够响应命中测试,即点击顶部组件时,顶部组件和底部组件都可以接收到指针事件。
7.1.2 忽略事件
如果不想让某个子组件响应原始指针事件,可以使用AbsorbPointer或IgnorePointer组件包裹子组件来阻止子组件接收指针事件。
AbsorbPointer组件会参与命中测试,它本身可以接收指针事件,其包裹的子组件不能;而IgnorePointer组件不会参与命中测试,它完全不能接收指针事件。
import 'package:flutter/material.dart';
void main() => runApp(PointerWidget());
class PointerWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理',
home: Scaffold(
appBar: AppBar(title: Text('事件处理')),
body: Listener(
child: AbsorbPointer(
child: Listener(
child: Container(
color: Colors.red,
width: 200,
height: 200,
),
onPointerDown: (e) => print('point in'),
)
),
onPointerDown: (e) => print('point up'),
)
)
);
}
}
7.2 手势识别组件
7.2.1 基本用法
在Flutter开发中,Gesture API代表手势语义的抽象,从组件层面监听手势可以使用GestureDetector等手势响应组件。
GestureDetector组件是一个处理各种高级用户触摸行为的组件,使用时只需要将它作为父组件包裹在其他子组件外面即可。
7.2.2 常用事件
GestureDetector常用事件:
如果同时监听onTap和onDoubleTap事件时,onTap事件会有200ms左右的延迟。
示例代码:
import 'package:flutter/material.dart';
void main() => runApp(GestureDetectorPage());
class GestureDetectorPage extends StatefulWidget {
State<StatefulWidget> createState() {
return GestureDetectorPageState();
}
}
class GestureDetectorPageState extends State<GestureDetectorPage> {
String operation = 'No Gesture';
void updateGesture(String text) {
setState(() => operation = text);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理',
home: Scaffold(
appBar: AppBar(title: Text('事件处理 -- $operation')),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
child: Container(
margin: EdgeInsets.only(top: 20),
alignment: Alignment.center,
color: Colors.blue,
width: 150,
height: 80,
child: Text(
operation,
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
onTap: () => updateGesture('Tap'),
onDoubleTap: () => updateGesture('DoubleTap'),
onLongPress: () => updateGesture('LongPress'),
)
],
)
)
);
}
}
示例效果:
7.2.3 拖拽与缩放
在处理拖拽事件时,GestureDetector会将需要监听组件的原点作为本次手势的起点,当用户在监听组件上按下手指时手势识别就开始运行。
示例代码:
import 'package:flutter/material.dart';
void main() => runApp(DragPage());
class DragPage extends StatefulWidget {
State<StatefulWidget> createState() {
return DragState();
}
}
class DragState extends State<DragPage> {
String operation = 'No Gesture';
void updateGesture(String text) {
setState(() => operation = text);
}
double _top = 0.0;
double _left = 0.0;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理--拖拽',
home: Scaffold(
appBar: AppBar(title: Text('事件处理--拖拽')),
body: Stack(
children: <Widget>[
Positioned(
top: _top,
left: _left,
child: GestureDetector(
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.blue,
child: Text(
'Drag',
style: TextStyle(color: Colors.white),
),
),
onPanDown: (DragDownDetails e) => print('onPanDown: ${e.globalPosition}'),
onPanUpdate: (DragUpdateDetails e) {
setState(() {
_left += e.delta.dx;
_top += e.delta.dy;
});
},
onPanEnd: (DragEndDetails e) => print('onPanEnd: ${e.velocity.toString()}'),
),
)
],
)
)
);
}
}
示例效果:
如果只需要沿一个方向拖动,可以将onPanUpdate改为onVerticalDragUpdate或者onHorizontalDragUpdate。
可以使用GestureDetector组件的onScaleUpdate实现缩放效果。
示例代码:
import 'package:flutter/material.dart';
void main() => runApp(ScalePage());
class ScalePage extends StatefulWidget {
State<StatefulWidget> createState() {
return ScaleState();
}
}
class ScaleState extends State<ScalePage> {
double width = 100.0;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理--缩放',
home: Scaffold(
appBar: AppBar(title: Text('事件处理--缩放')),
body: Center(
child: GestureDetector(
child: Image.asset('images/qq.png', width: width),
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
width = 200 * details.scale.clamp(0.8, 10.0);
});
},
),
)
)
);
}
}
7.2.4 手势识别器
GestureDetector封装了Listener的原始指针事件,可以很容易地对各种手势进行识别。GestureDetector是一个抽象类,有多个实现子类,通常一种手势识别器即对应一个GestureDetector的实现类。
示例代码:动态改变富文本文字大小
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() => runApp(GestureRecognizerPage());
class GestureRecognizerPage extends StatefulWidget {
State<StatefulWidget> createState() {
return GestureRecognizerState();
}
}
class GestureRecognizerState extends State<GestureRecognizerPage> {
TapGestureRecognizer recognizer = TapGestureRecognizer();
bool toggle = false;
@override
void dispose() {
recognizer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理--手势识别器',
home: Scaffold(
appBar: AppBar(title: Text('事件处理--手势识别器')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('点击文字放大'),
Text.rich(
TextSpan(
text: '你好,Flutter',
style: TextStyle(fontSize: toggle ? 30 : 60),
recognizer: recognizer..onTap = () {
setState(() {
toggle = !toggle;
});
}
),
)
],
)
)
)
);
}
}
示例效果:
使用手势识别器后一定要调用dispose()来释放资源,因为手势识别器内部使用了计时器,不释放的话会造成大量的资源消耗。
7.2.5 手势竞争
对于需要处理多个手势识别的场景,Flutter引入了手势竞技场的概念,用来识别究竟哪个手势最终响应用户事件。手势竞技场通过综合对比用户触摸屏幕的时长、位移以及拖拽方向来确定最终手势。
示例代码:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() => runApp(GestureCompetePage());
class GestureCompetePage extends StatefulWidget {
State<StatefulWidget> createState() {
return GestureCompeteState();
}
}
class GestureCompeteState extends State<GestureCompetePage> {
double _top = 0.0;
double _left = 0.0;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理--手势竞争',
home: Scaffold(
appBar: AppBar(title: Text('事件处理--手势竞争')),
body: Stack(
children: <Widget>[
Positioned(
top: _top,
left: _left,
child: GestureDetector(
child: CircleAvatar(
minRadius: 40,
child: Text(
'手势竞争',
style: TextStyle(fontSize: 16)
),
),
onVerticalDragUpdate: (DragUpdateDetails details) {
setState(() {
_top += details.delta.dy;
});
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
_left += details.delta.dx;
});
},
)
)
],
)
)
);
}
}
示例效果:
每次拖动小球时,小球只会沿着一个方向移动。