1 原始指针事件
1.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,可以用它监听包裹的子组件的原始指针事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Listener( onPointerDown: (downPointEvent) { ... }, onPointerMove: (movePointEvent) { ... }, onPointerUp: (upPointEvent) { ... }, child: Container( child: Text(‘Listener事件监听’); ) )
|
原始指针事件还提供了behavior属性,它决定子组件如何响应命中测试,它的值类型为HitTestBehavior,是一个枚举类型,有3个枚举值:
1)deferToChild:子组件一个接一个地进行命中测试,如果子组件中有通过命中测试的,则当前组件会收到指针事件,并且其父组件会收到指针事件;
2)opaque:在进行命中测试时,当前组件会被当成不透明进行处理,单击的响应区域即为单击区域;
3)translucent:设置此属性后,组件自身和底部可视区域都能够响应命中测试,即点击顶部组件时,顶部组件和底部组件都可以接收到指针事件。
1.2 忽略事件
如果不想让某个子组件响应原始指针事件,可以使用AbsorbPointer或IgnorePointer组件包裹子组件来阻止子组件接收指针事件。
AbsorbPointer组件会参与命中测试,它本身可以接收指针事件,其包裹的子组件不能;而IgnorePointer组件不会参与命中测试,它完全不能接收指针事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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'), ) ) ); } }
|
2 手势识别组件
2.1 基本用法
在Flutter开发中,Gesture API代表手势语义的抽象,从组件层面监听手势可以使用GestureDetector等手势响应组件。
GestureDetector组件是一个处理各种高级用户触摸行为的组件,使用时只需要将它作为父组件包裹在其他子组件外面即可。
2.2 常用事件
GestureDetector常用事件:
如果同时监听onTap和onDoubleTap事件时,onTap事件会有200ms左右的延迟。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| 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'), ) ], ) ) ); } }
|
示例效果:
2.3 拖拽与缩放
在处理拖拽事件时,GestureDetector会将需要监听组件的原点作为本次手势的起点,当用户在监听组件上按下手指时手势识别就开始运行。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| 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实现缩放效果。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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); }); }, ), ) ) ); } }
|
2.4 手势识别器
GestureDetector封装了Listener的原始指针事件,可以很容易地对各种手势进行识别。GestureDetector是一个抽象类,有多个实现子类,通常一种手势识别器即对应一个GestureDetector的实现类。
示例代码:动态改变富文本文字大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| 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()来释放资源,因为手势识别器内部使用了计时器,不释放的话会造成大量的资源消耗。
2.5 手势竞争
对于需要处理多个手势识别的场景,Flutter引入了手势竞技场的概念,用来识别究竟哪个手势最终响应用户事件。手势竞技场通过综合对比用户触摸屏幕的时长、位移以及拖拽方向来确定最终手势。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| 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; }); }, ) ) ], ) ) ); } }
|
示例效果:
每次拖动小球时,小球只会沿着一个方向移动。