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。
最后
以上就是活泼大碗最近收集整理的关于解析Flutter中的手势控制Gestures的全部内容,更多相关解析Flutter中内容请搜索靠谱客的其他文章。
发表评论 取消回复