Flutter提供了很多处理触摸事件的控件,例如InkWell
和InkResponse
可以处理点击、双击、长按等事件,将它们包裹在需要响应触摸事件的控件外部就可以了,而且InkWell
和InkResponse
还会添加一个水波纹的点击效果,InkResponse
还可以设置水波纹的形状。但是,InkWell
和InkResponse
都不会做任何的渲染工作,它们只是更新了父级Material Widget。一个简单的例子就是image,如果你将一个image用InkWell
包裹住,那么你会发现,水波纹效果不见了。这是因为水波纹是被绘制在image的下层的,所以被遮挡住了。如果想要水波纹可见,那么请使用Ink.Image
。
但是如果你想要捕捉更多的触摸事件,比如用户的拖拽行为,那么你就必须使用GestureDetector
来实现了。
什么是GestureDetector
最基本的解释就是:一个处理各种 touch event 的 stateless widget。GestureDetector是一个纯粹的用来处理手势的控件,没有任何UI上的表现(不像Ink那样会有水波纹的触摸反馈)。
下面的表格是GestureDetector
提供的各种 callbacks 和对这些回调的简单解释:
Property/Callback | Description |
---|---|
onTapDown | 用户每次和屏幕交互时都会被调用 |
onTapUp | 用户停止触摸屏幕时触发 |
onTap | 短暂触摸屏幕时触发 |
onTapCancel | 用户触摸了屏幕,但是没有完成Tap的动作时触发 |
onDoubleTap | 用户在短时间内触摸了屏幕两次 |
onLongPress | 用户触摸屏幕时间超过500ms时触发 |
onVerticalDragDown | 当一个触摸点开始跟屏幕交互,同时在垂直方向上移动时触发 |
onVerticalDragStart | 当触摸点开始在垂直方向上移动时触发 |
onVerticalDragUpdate | 屏幕上的触摸点位置每次改变时,都会触发这个回调 |
onVerticalDragEnd | 当用户停止移动,这个拖拽操作就被认为是完成了,就会触发这个回调 |
onVerticalDragCancel | 用户突然停止拖拽时触发 |
onHorizontalDragDown | 当一个触摸点开始跟屏幕交互,同时在水平方向上移动时触发 |
onHorizontalDragStart | 当触摸点开始在水平方向上移动时触发 |
onHorizontalDragUpdate | 屏幕上的触摸点位置每次改变时,都会触发这个回调 |
onHorizontalDragEnd | 水平拖拽结束时触发 |
onHorizontalDragCancel | onHorizontalDragDown没有成功完成时触发 |
onPanDown | 当触摸点开始跟屏幕交互时触发 |
onPanStart | 当触摸点开始移动时触发 |
onPanUpdate | 屏幕上的触摸点位置每次改变时,都会触发这个回调 |
onPanEnd | pan操作完成时触发 |
onScaleStart | 触摸点开始跟屏幕交互时触发,同时会建立一个焦点为1.0 |
onScaleUpdate | 跟屏幕交互时触发,同时会标示一个新的焦点 |
onScaleEnd | 触摸点不再跟屏幕有任何交互,同时也表示这个scale手势完成 |
GestureDetector
并不会监听上面所有的手势,只有传入的callbacks非空时,才会监听。所以,如果你想要禁用某个手势时,可以给对应的callback传null。
我们以onTap为例,看一下GestureDetector时如何工作的
我们首先创建一个带有onTap
回调的GestureDetector
,每次tap事件发生的时候,都会触发我们这个回调。而在GestureDetector
内部,会创建一个Gesture Factory
,Gesture Recognizer
则决定了需要处理哪一个手势。当有多个回调时,也是同样的处理过程。然后,GestureFactories
会被传给RawGestureDetector
。
RawGestureDetector
负责检测手势。它是一个stateful widget,当它的状态发生改变时,会同步所有的手势,处理recognizers,然后将所有发生的pointer events
传给注册了的recognizers
,之后它会和Gesture Arena
来一场battle,决定将这个事件交给谁。
RawGestureDetector
通过Listener
类创建了一个基础的监听pointer events
的listener。如果你想要使用来自platform的未处理过的 input,比如 up、down、cancel 事件,你需要使用的就是这个Listener
类。但是,Listener
类并不会给你提供具体的手势信息,它只会提供四个基本的事件:onPointerDown
、 onPointerUp
、onPointerMove
和onPointerCancel
。每一件事都需要手动进行,包括将你自己报告给Gesture Arena
,如果不这么做,之后没法自动收到cancel消息,也没法收到接下来的任何交互信息。
Listener
是一个由RenderPointerListener
组成的SingleChildRenderObjectWidget,用来汇报未处理的pointer events,RenderPointerListener
继承自RenderProxyBoxWithHitTestBehavior
,当定制一个HitTestBehavior
时,它会模拟所有子类的属性。如果你想要更多地了解Render Boxes
,可以看下这篇文章:Flutter, what are Widgets, RenderObjects and Elements?
HitTestBehavior
有三个属性:deferToChild
、opaque
和translucent
,这三个属性来源于GestureDetector
中的配置。deferToChild
会将事件在widget tree中向下传递;opaque
防止background widgets收到事件;translucent
则允许background widgets收到事件。
如果想要父控件和子控件都能收到手势事件呢?
想象一个场景:你有一个嵌套的list,想要它们能够同时滑动。这时,你就需要父控件和子控件都能收到pointer events了。你可能觉得,我设置一下translucent不就行了嘛,这样两个控件都能收到事件,但是,事实并没有想象的这么美好······那么,为什么行不通呢?
这时候就需要用到之前提到的GestureArena
了。GestureArena
是在 gesture disambiguation 中被用到的,所有的recognizer都会被传送到这里,然后来一场大乱斗。屏幕上的每一个点,都可能形成多个gesture recognizers。Arena通过分析用户触摸屏幕的时长和用户拖拽的角度,决定胜出者是谁。
父控件和子控件都会有自己的recognizers被传递到Arena这里,只有一个recognizers会取胜,而且大部分情况下胜者都是子控件。
(注:Flutter源码注释就是用的win和lose这两个词,确实不太好翻译,暂且称为获胜和失败,一个gesture的结果只有两种,要么wins the arena,要么loses the arena,所以一定要理解win、lose和arena这三个词的意思。)
解决方法就是使用你自定义的GestureFactory
的RawGestureDetector
,强行改变Arena的行为。
我们用一个简单的例子来解释下,下面的这个App里面包括了两个containers,我们的目的是父控件和子控件都能收到手势。
两个控件都会被包裹在RawGestureDetector
里面,接下来我们自定义一个gesture recognizer AllowMultipleGestureRecognizer
,GestureRecognizer
是所有recognizers的基类,自定义的recognizers都应该继承自它,它提供了最基本的和gesture recognizers交互的API,GestureRecognizer
并不关心子recognizers各自的具体特性。
// 自定义 Gesture Recognizer.
// 重写rejectGesture(). 当一个手势被拒绝时,会调用这个函数。
// 默认情况下,它会处理Recognizer,并在处理完毕后销毁。
// 我们重写这个方法,自己处理Recognizer
// 处理结果就是,我们有两个Recognizer都赢了Arena,而不是一个,因为我们手动接受了两个recognizers
class AllowMultipleGestureRecognizer extends TapGestureRecognizer {
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
acceptGesture()
是在给定的pointer id获胜(win)时的回调;rejectGesture()
是在给定的pointer id失败(lose)时的回调。所以我们在自定义的recognizer中,强行在rejectGesture()
中做了accept操作。
然后,我们在控件中,将我们的自定义gesture-recognizer,通过GestureRecognizerFactoryWithHandlers
传递给RawGestureDetector
:
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<
AllowMultipleGestureRecognizer>(
() => AllowMultipleGestureRecognizer(), //constructor
(AllowMultipleGestureRecognizer instance) { //initializer
instance.onTap = () => print('Episode 4 is best! (parent container) ');
},
)
},
factory的构造器需要两个属性、一个构造器和一个用来初始化gesture recognizer的初始化器,我们通过lambda表达式来传参。这个构造器返回了一个新的AllowMultipleGestureRecognizer
事例,初始化器持有的instance属性则是用来监听tap操作,在console中做一些print操作。
完整的样例代码如下:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
//Main function. The entry point for your Flutter app.
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: DemoApp(),
),
),
);
}
class DemoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<
AllowMultipleGestureRecognizer>(
() => AllowMultipleGestureRecognizer(),
(AllowMultipleGestureRecognizer instance) {
instance.onTap = () => print('Episode 4 is best! (parent container) ');
},
)
},
behavior: HitTestBehavior.opaque,
//Parent Container
child: Container(
color: Colors.blueAccent,
child: Center(
//Wraps the second container in RawGestureDetector
child: RawGestureDetector(
gestures: {
AllowMultipleGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
AllowMultipleGestureRecognizer>(
() => AllowMultipleGestureRecognizer(), //constructor
(AllowMultipleGestureRecognizer instance) { //initializer
instance.onTap = () => print('Episode 8 is best! (nested container)');
},
)
},
//Creates the nested container within the first.
child: Container(
color: Colors.yellowAccent,
width: 300.0,
height: 400.0,
),
),
),
),
);
}
}
class AllowMultipleGestureRecognizer extends TapGestureRecognizer {
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
当某个recognizer获胜(win the arena)会发生什么呢?
当某个recognizer获胜后, Arena会closed and swept
,处理掉没有用的recognizers,并且重置Arena。而这个最终的gesture就会执行最终的action。
回到最初的Tap例子中,最终结果就是调用onTap
方法。一旦一个gesture获胜了,就会立即触发onTapUp
,而没有获胜的gesture则会触发onTapCancel
。