点击、拖动和其他手势

这个章节将会讲解如何监听和响应 Flutter 的手势操作 gestures。典型的手势操作包括点击、拖动和缩放。

Flutter 中的手势有两个不同的层次:第一层是原始的指针指向事件,描述了屏幕上由触摸板、鼠标、指示笔等触发的指针的位置和移动。第二层包含 gestures,描述了由上述一个或多个指针移动组成的具有特殊语义的操作。

指针

#

Pointer 代表的是人机界面交互的原始数据。一共有四种指针事件:

PointerDownEvent
指针在特定位置与屏幕接触

PointerMoveEvent
指针从屏幕的一个位置移动到另外一个位置

PointerUpEvent
指针与屏幕停止接触

PointerCancelEvent
指针的输入已经不再指向此应用

在指针下落事件中,框架做了一个 hit test 的操作确定与屏幕发生接触的位置上有哪些组件以及分发给最内部的组件去响应。事件会沿着组件树从这个最内部的组件向组件树的根部冒泡分发。并且不存在用于取消或停止指针事件进行进一步分发的机制。

使用 Listener 可以在组件层直接监听指针事件。然而,一般情况下,请考虑使用下面的 gestures 替代。

手势

#

Gesture 代表的是语义操作(比如点击、拖动、缩放)。通常由一系列单独的指针事件组成,甚至是一系列单独的指针组成。 Gesture 可以分发多种事件,对应着手势的生命周期(比如开始拖动、拖动更新、结束拖动)。

点击

onTapDown
指针在发生接触的屏幕的特定位置可能引发点击事件。

onTapUp
触发点击事件的触点已经在某个点停止与屏幕交互。

onTap
触发 onTapDown 的触点此时已触发了 onTapUp,即产生了点击事件。

onTapCancel
指针已经触发了 onTapDown,但是最终不会形成一个点击事件。

双击

onDoubleTap
用户在屏幕的相同位置上快速点击了两次。

长按

onLongPress
指针在屏幕的相同位置上保持接触持续一长段时间。

纵向拖动

onVerticalDragStart
指针和屏幕产生接触并可能开始纵向移动。

onVerticalDragUpdate
指针和屏幕产生接触,在纵向上发生移动并保持移动。

onVerticalDragEnd
指针先前和屏幕产生了接触,以特定速度纵向移动,并且此后不会在屏幕接触上发生纵向移动。

横向拖动

onHorizontalDragStart
指针和屏幕产生接触并可能开始横向移动。

onHorizontalDragUpdate
指针和屏幕产生接触,在横向上发生移动并保持移动。

onHorizontalDragEnd
指针先前和屏幕产生了接触,以特定速度横向移动,并且此后不会在屏幕接触上发生横向移动。

移动

onPanStart
指针和屏幕产生接触并可能开始横向移动或者纵向移动。如果设置了 onHorizontalDragStart 或者 onVerticalDragStart,该回调方法会引发崩溃。

onPanUpdate
指针和屏幕产生接触,在横向或者纵向上发生移动并保持移动。如果设置了 onHorizontalDragUpdate 或者 onVerticalDragUpdate,该回调方法会引发崩溃。

onPanEnd
指针先前和屏幕产生了接触,并且以特定速度移动,此后不再在屏幕接触上发生移动。如果设置了 onHorizontalDragEnd 或者 onVerticalDragEnd,该回调方法会引发崩溃。

为 widgets 添加手势检测

#

从组件层监听手势,需要用到 GestureDetector

如果使用 Material 风格的组件,其中的许多组件都能够支持响应点击或者手势事件。比如 IconButtonTextButton 响应了按压事件(点击事件), ListView 响应了滚动事件。如果使用了上述组件,你也可以使用 InkWell 来实现点击的“水波纹”效果。

手势消歧处理

#

在屏幕的指定位置上,可能有多个手势捕捉器。例如:

  • A ListTile has a tap recognizer that responds to the entire ListTile, and a nested one around a trailing icon button. The screen rect of the trailing icon is now covered by two gesture recognizers that need to negotiate for who handles the gesture if it turns out to be a tap.
  • A single GestureDetector covers a screen area configured to handle multiple gestures, such as a long press and a tap. The tap recognizer must now negotiate with the long press recognizer when the user touches that part of the screen. Depending on what happens next with that pointer, one of the two recognizers receives the gesture, or neither receives the gesture if the user performs something that's neither a tap nor a long press.

所有的手势捕捉器监听了指针输入流事件并判断出特定手势。 GestureDetector widget 能够基于手势的回调是否非空决定是否应该尝试去识别该手势。

当屏幕上的指定指针有多个手势识别器时,框架会通过给每个手势识别器加入 gesture arena 来处理手势消歧。 gesture arena,也称作手势竞技场,会利用下述规则确定哪个手势在竞争中胜出:

  • 在任何时候,识别器都可以宣告失败并离开竞技场。如果竞技场中只有一个识别器,那么这个识别器就是胜者。

  • 在任何时候,任何识别器都可以宣告胜利,这将导致这个识别器胜出,其他识别器失败。

比如,当纵向拖动和横向拖动需要处理消歧,当指针下落事件发生时,纵向和横向识别器都会进入竞技场,观测指针移动事件。如果用户在横向上移动超过了特定像素,横向识别器会宣告胜利,手势也会被当作横向拖动处理。同样的,如果用户在纵向上移动超过了特定的像素,纵向识别器会宣告胜利。