解析Flutter中的手势控制Gestures

  Flutter提供了很多处理触摸事件的控件,例如InkWellInkResponse可以处理点击、双击、长按等事件,将它们包裹在需要响应触摸事件的控件外部就可以了,而且InkWellInkResponse还会添加一个水波纹的点击效果,InkResponse还可以设置水波纹的形状。但是,InkWellInkResponse都不会做任何的渲染工作,它们只是更新了父级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 FactoryGesture 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、 onPointerUponPointerMoveonPointerCancel。每一件事都需要手动进行,包括将你自己报告给Gesture Arena,如果不这么做,之后没法自动收到cancel消息,也没法收到接下来的任何交互信息。
  Listener是一个由RenderPointerListener组成的SingleChildRenderObjectWidget,用来汇报未处理的pointer events,RenderPointerListener继承自RenderProxyBoxWithHitTestBehavior,当定制一个HitTestBehavior时,它会模拟所有子类的属性。如果你想要更多地了解Render Boxes,可以看下这篇文章:Flutter, what are Widgets, RenderObjects and Elements?
  HitTestBehavior有三个属性:deferToChildopaquetranslucent,这三个属性来源于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这三个词的意思。)

  解决方法就是使用你自定义的GestureFactoryRawGestureDetector,强行改变Arena的行为。
  我们用一个简单的例子来解释下,下面的这个App里面包括了两个containers,我们的目的是父控件和子控件都能收到手势。
  两个控件都会被包裹在RawGestureDetector里面,接下来我们自定义一个gesture recognizer AllowMultipleGestureRecognizerGestureRecognizer是所有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);
  }
}

解析Flutter中的手势控制Gestures

输出结果

当某个recognizer获胜(win the arena)会发生什么呢?

  当某个recognizer获胜后, Arena会closed and swept,处理掉没有用的recognizers,并且重置Arena。而这个最终的gesture就会执行最终的action。
  回到最初的Tap例子中,最终结果就是调用onTap方法。一旦一个gesture获胜了,就会立即触发onTapUp,而没有获胜的gesture则会触发onTapCancel