GestureRecognizer
getstureDetector 内部是使用一个或者多个 GestureRecognizer 来识别各种手势的,而 GestureRecognizer 的作用就是通过 Listener 将原始指针转换为语义手势
GestureRecognizer 是一个抽象类,一种手势对应一个子类,Flutter 实现了丰富的手势识别器,我们可以直接使用。
例如:
我们要给一段富文本 (RichText) ,的不同部分添加事件处理器,但是 TextSpan 并不是一个 widget,所以不能用 GestureDetector。但是 TextSpan 有一个 Recongizer 属性,他可以接收一个 GestureRecognizer。
bool _toggle = false; //变色开关 TapGestureRecognizer _recognizer = TapGestureRecognizer(); Widget bothDirectionTest() { return Center( child: Text.rich(TextSpan(children: [ TextSpan(text: "你好世界"), TextSpan( text: "点击变色", style: TextStyle( fontSize: 30, color: _toggle ? Colors.red : Colors.yellow), recognizer: _recognizer ..onTap = () { setState(() { _toggle = !_toggle; }); }), TextSpan(text: "你好世界") ])), ); } @override void dispose() { //用到GestureRecognizer的话一定要调用其dispose方法释放资源 _recognizer.dispose(); super.dispose(); }
注意:使用 GestureRecognizer 之后,一定要调用其 dispose 方法来释放资源(主要是取消内部的计时器),运行效果如下:
手势竞争与冲突
竞争
如在上例中,同时监听水平方向和垂直方向的拖动事件,那么斜着滑动时那个方向会生效? 实际上取决于第一次移动时两个轴上的位移分量,那个轴的大,那么哪个轴就会在本次滑动事件中胜出
实际上 Flutter 中引入了一个 Arenal 的概念,直译为 竞技场 的意思,每一个手势识别器(GestureRecognizer) 都是一个竞争者(GestureArenaMember),当发生滑动事件时,他们都要在 竞技场 去竞争本次事件的处理权,而最终只有一个竞争者会胜出。
例如有一个 ListView,他的第一个子组件也是 ListView,如果滑动子 ListView,父 ListView 会动吗?答案肯定是不会动的,这时只有子 ListView 会动,这是因为子 LsitView 货到了滑动事件的处理权。
示例
var _top1 = 100.0; var _left1 = 100.0; Widget bothDirection() { return Stack( children: [ Positioned( top: _top1, left: _left1, child: GestureDetector( child: CircleAvatar(child: Text("A")), onVerticalDragUpdate: (DragUpdateDetails details) { setState(() { _top1 += details.delta.dy; }); }, onHorizontalDragUpdate: (DragUpdateDetails details) { setState(() { _left1 += details.delta.dx; }); }, ), ) ], ); }
运行之后,每次拖动只会沿着一个方向移动,而竞争者发生在手指按下后首次移动时
上例中获胜的条件是,首次移动时的位置在水平和垂直方向上分量大的一个获胜
手势冲突
由于手势竞争最终只有一个胜出者,所以,当有多个手势识别器时,可能会产生冲突;
例如有一个 Widget,可以左右拖动,现在我们也想检测它上面手指按下和抬起的事件,如下:
var _left2 = 100.0; Widget flictTest() { return Stack( children: [ Positioned( left: _left2, top: 100, child: GestureDetector( child: CircleAvatar(child: Text("A")), onHorizontalDragUpdate: (DragUpdateDetails details) { setState(() { _left2 += details.delta.dx; }); }, onHorizontalDragEnd: (details) { print('onHorizontalDragEnd'); }, onTapDown: (details) { print('down'); }, onTapUp: (details) { print('up'); }, ), ) ], ); }
拖动后,日志如下:
0I/flutter ( 4315): down I/flutter ( 4315): onHorizontalDragEnd
我们发现没有打印 up,这是因为拖动时,在按下手指没有移动时,拖动手势还没有完整的语义,此时 TapDown 手势胜出,此时打印 down,而拖动时,拖动手势胜出,当抬起时, onHorizontalDragEnd 和 onTap 发生冲突,但是应为是在拖动的语义中,所以 onHorizeontalDragend 胜出,所以就会打印 onHorizontalDragEnd。
如果我们的逻辑代码中,对手指的按下和抬起时强依赖的,例如轮播组件,我们希望按下时暂停轮播,抬起时恢复轮播。但是由于轮播组件中本身可能已经处理了拖动手势,甚至支持了缩放手势,这时外部如果再用 onTapDown,onTap 来监听是不行的。
这个时候就可以同个 Listener 监听原始指针事件就行:
Listener( child: GestureDetector( child: CircleAvatar(child: Text("A")), onHorizontalDragUpdate: (DragUpdateDetails details) { setState(() { _left2 += details.delta.dx; }); }, onHorizontalDragEnd: (details) { print('onHorizontalDragEnd'); }, ), onPointerDown: (details){ print('onPointerDown'); }, onPointerUp: (details){ print('onPointerUp'); }, ), )
手势冲突只是手势级别的,而手势是对原始指针的语义化识别,所以在遇到复杂的冲突场景时,都可以通过 Listener 直接识别原始指针事件来解决冲突
事件总线
在 App 中,我们经常需要一个广播机制,用以夸页面事件通知,例如注销登录时,某些页面可能需要进行状态更新。这个时候一个事件总线便会非常有用;
事件总线通常实现了订阅者模式,订阅者包含订阅者和发布者两个角色,可以通过事件总线来触发事件和监听事件;
代码如下:
typedef void EventCallback(arg); class EventBus { //私有构造 EventBus._internal(); static EventBus _singleton = new EventBus._internal(); //工厂构造函数 factory EventBus() => _singleton; //保存时间订阅者队列,key:事件名(id),value:对应的实际订阅者队列 var _eMap = new Map<Object, List<EventCallback>>(); ///添加订阅者 void on(eventName, EventCallback f) { if (eventName == null || f == null) return; _eMap[eventName] ??= []; _eMap[eventName].add(f); } ///移除订阅者 void off(eventName, [EventCallback f]) { var list = _eMap[eventName]; if (eventName == null || list == null) return; if (f == null) { _eMap[eventName] = null; } else { list.remove(f); } } ///触发订阅者 void emit(eventName, [arg]) { var list = _eMap[eventName]; if (list == null) return; int len = list.length - 1; for (var i = len; i > -1; i--) { list[i](arg); } } } ///定义一个 top-level,全局变量,页面引入该文件之后可以直接使用 bug var bus = new EventBus();
使用如下:
//监听登录失效 bus.on(Event.LOGIN_OUT, (arg) { SpUtil.putString(Application.accessToken, null); Application.router.navigateTo(context, Routes.login, clearStack: true); }); //触发失效事件 bus.emit(Event.LOGIN_OUT, null);
注意:Dart 中实现点了模式的标准做法就是使用 static 变量 + 工厂构造函数的方式,这样就可以保证 new EventBus() 始终返回都是同一个实例
事件总线常用于组件之间的状态共享,但是关于组件之间的状态共享也有一些专门的包,如 redux,以及 Provider。
对于一些简单的应用,事件总线总是奏议满足业务需求,如果觉得使用状态管理包的话,一定要想清楚 APP 是否有必要使用它,防止化简为繁的过度设计。