触摸事件让用户可以通过触摸屏与JavaFX程序进行交互。触摸点会标识出一次触摸的每个触摸点。将向你展示如何标识触摸点并处理触摸事件,以此来对触摸动作提供复杂的响应。
一个触摸动作包含触摸屏上一个或多个接触点。该动作可以是一个简单的按下与释放动作,或者是在按下与释放动作之间的更复杂的一系列停留和移动动作。在动作执行期间所有接触点都会产生一系列事件。除了触摸事件之外,还包括鼠标事件和手势事件。如果你的JavaFX程序并不要求对触摸动作有复杂的响应,你可能会更倾向于处理鼠标和手势事件而不是触摸事件。
对触摸事件的支持需要一块触摸屏和Windows7操作系统。
触摸动作概览
“触摸动作”表示用户从接触屏幕开始到所有接触点离开屏幕期间的整个过程。触摸动作执行期间会产生的触摸事件类型包括TOUCH_PRESSED、TOUCH_MOVED、TOUCH_STATIONARY和TOUCH_RELEASED。
与屏幕接触的每个点均被认为是一个“触摸点”,对于每个触摸点都会产生触摸事件。当一个触摸动作包含多个接触点时,就会在触摸动作的每种状态下都产生多个事件的集合,称之为“事件集”,其中为每个触摸点都产生了一个触摸事件。
触摸点
当用户触摸触摸屏时,每个独立的接触点都会产生一个“触摸点”。触摸点用TouchPoint类的一个实例来表示,其中包含位置、状态、接触点的目标等信息。触摸点的状态有按下(Pressed)、移动(Moved)、固定(Stationary)和释放(Released)4种。
小贴士:产生的触摸点的数量可能会受限于触摸屏。例如,如果触摸屏只支持两个接触点而用户用三根手指触摸屏幕,则只会产生两个触摸点。出于本文的目的,我们假定触摸屏可识别所有的接触点。
每个触摸点都有一个ID,它是按照该触摸点加入到触摸动作中的顺序来分配的。触摸点的ID从用户接触触摸屏开始到接触释放期间会保持不变。当接触点释放时,与之关联的触摸点就不再属于该触摸动作的一部分了。例如,如果使用两根手指触摸屏幕,分配给第一个触摸点的ID就是1,分配给第二个接触点的ID就是2。如果第二根手指从触摸屏离开,则只有触摸点1仍然是该触摸动作的一部分。如果又有另外一根手指加入到该触摸动作中,分配给新的触摸点的ID是3,该触摸动作此时有1和3两个触摸点。
触摸事件
“触摸事件”被用来跟踪触摸点的动作。触摸事件由TouchEvent类的实例来表示。只有触摸屏会产生触摸事件,触摸板不会。
触摸事件与其它事件类似,也有源、目标和事件类型,事件类型进一步定义了发生的动作。触摸事件的类型包括TOUCH_PRESSED、TOUCH_MOVED、TOUCH_STATIONARY和TOUCH_RELEASED 4种。一个触摸点可产生多个TOUCH_MOVED和TOUCH_STATIONARY事件,取决于移动的距离和触摸点保持不动的时间长短。
触摸事件还包含以下内容:
● 触摸点:与此事件相关联的主要触摸点;
● 触摸数量:当前与该触摸动作相关联的触摸点的个数;
● 触摸点列表:当前与该触摸动作相关联的触摸点的集合;
● 事件集ID:包含该事件的事件集ID。
事件集
当触摸动作只有一个接触点时,只会为该动作的每种状态产生一个触摸事件。当触摸动作有多个接触点时,就会为该动作的每种状态产生一个触摸事件集合。该集合中的每个事件与其中的对应的接触点一一关联。
每一个事件集都有一个事件集ID。每次对触摸动作的响应所产生的事件集的事件集ID以1为步长递增。事件集中的事件可以有不同的事件类型,这取决于它所关联的触摸点的状态。如果在触摸动作执行期间增加或者移除了接触点,则事件集中的事件数量也会改变。例如,表6-1描述了当一个用户用两根手指触摸屏幕、移动两根手指、使用第三根手指触摸屏幕、移动所有的手指、最后把所有手指从屏幕上离开这一系列动作执行时产生的事件集。
表6-1 一次触摸动作的事件集
事件集ID | 触摸事件数量 | 每个事件的事件类型 |
1 | 1 | TOUCH_PRESSED |
2 | 2 | TOUCH_STATIONARY, TOUCH_PRESSED |
3 | 2 | TOUCH_MOVED, TOUCH_MOVED |
4 | 3 | TOUCH_STATIONARY, TOUCH_STATIONARY, TOUCH_PRESSED |
5 | 3 | TOUCH_MOVED, TOUCH_MOVED, TOUCH_MOVED |
6 | 3 | TOUCH_MOVED, TOUCH_MOVED, TOUCH_MOVED |
7 | 3 | TOUCH_MOVED, TOUCH_MOVED, TOUCH_MOVED |
8 | 3 | TOUCH_RELEASED, TOUCH_STATIONARY, TOUCH_STATIONARY |
9 | 2 | TOUCH_RELEASED, TOUCH_STATIONARY |
10 | 1 | TOUCH_RELEASED |
触摸点目标和触摸事件目标
触摸事件的目标也是与之关联的触摸点的目标。触摸点的初始目标是与触摸屏接触的初始点处的最上层的节点。如果一个触摸动作有多个接触点,则每个触摸点都会有一个不同的目标,同样,每个触摸事件也会有不同的目标。这个特性使你可以处理每一个触摸点而不依赖于其他的触摸点。
一般来说,一个触摸点的所有事件都会传递到同一个目标。然而,你也可以使用触摸点的grab()和ungrab()方法改变后面事件的目标。
grab()方法可以让当前正在处理事件的节点成为触摸点的目标。grab(target)方法可以让另外一个节点成为触摸点的目标。因为事件集中的所有事件都可以访问该事件集的所有触摸点,因此可以用grab()方法引导该触摸动作所有的后续事件都传递给相同的节点。grab()方法也可以用来重置某个触摸点的目标,就像后面的“改变触摸点的目标”中所展示的那样。
ungrab()方法用来从当前目标释放触摸点。该触摸动作的后续事件都会传递到触摸点当前位置的最上层节点。
触摸产生的附加事件
当用户触摸屏幕时,除了触摸事件外还会产生其它类型的事件:
● 鼠标事件
模拟鼠标事件可以让程序运行在有触摸屏的设备上,即便该程序并不处理触摸事件。用isSynthesized()方法来判断鼠标事件是否来自于触摸动作。请参考“处理鼠标事件”章节的样例。
● 手势事件
手势事件由一般触摸事件的滚动、轻扫、旋转和缩放产生。如果你的程序仅需要处理这些类型的触摸动作,那你就可以仅处理这些手势事件,而不用再处理触摸事件。参考“可触摸设备的事件”章节来了解更多有关手势事件的信息。
触摸事件(Touch Events)样例
Touch Events样例使用4个“文件夹”来演示独立处理集合中的每个触摸点的功能。该样例同样也展示了使用grab()方法来让一个圆圈接连从一个矩形跳到另一个矩形。图6-1展示了该样例的用户界面。
图6-1 Touch Events样例
Touch Events样例程序在TouchEventExample.zip文件中。解压该NetBeans工程并在NetBeans IDE中打开。要产生触摸事件,你必须要在有触摸屏的设备上运行该样例程序。
独立处理并发触摸点
在一个典型的手势中,手势的目标是在所有触摸点的中心位置的节点,并且对该手势的响应只会影响到一个节点。通过分开来处理每一个触摸点,就可以影响到触摸到的所有节点。
在Touch Events样例中,你可以通过触摸文件夹以后移动手指来移动每个文件夹,也可以通过用不同的手指触摸每个文件夹并移动所有手指来一次移动多个文件夹。
每个文件夹是一个ToucheImage类实例。TouchImage类创建了一个图像并添加了TOUCHE_PRESSED、TOUCH_RELEASED和TOUCH_MOVED事件的事件处理器。例6-1展示了该类的定义。
例6-1 ToucheImage类的定义
public static class TouchImage extends ImageView {
private long touchId = -1;
double touchx, touchy;
public TouchImage(int x, int y, Image img) {
super(img);
setTranslateX(x);
setTranslateY(y);
setEffect(new DropShadow(8.0, 4.5, 6.5, Color.DARKSLATEGRAY));
setOnTouchPressed(new EventHandler() {
@Override public void handle(TouchEvent event) {
if (touchId == -1) {
touchId = event.getTouchPoint().getId();
touchx = event.getTouchPoint().getSceneX() - getTranslateX();
touchy = event.getTouchPoint().getSceneY() - getTranslateY();
}
event.consume();
}
});
setOnTouchReleased(new EventHandler() {
@Override public void handle(TouchEvent event) {
if (event.getTouchPoint().getId() == touchId) {
touchId = -1;
}
event.consume();
}
});
setOnTouchMoved(new EventHandler() {
@Override public void handle(TouchEvent event) {
if (event.getTouchPoint().getId() == touchId) {
setTranslateX(event.getTouchPoint().getSceneX() - touchx);
setTranslateY(event.getTouchPoint().getSceneY() - touchy);
}
event.consume();
}
});
}
}
当某个文件夹被触摸时,会为每个接触点创建一个触摸点,触摸事件会被发送到该文件夹。touchId用来确保当一个文件夹上有多个接触点时该文件夹仅响应一次。
当接收到一个TOUCH_PRESSED事件时,会检查touchId来判断该次触摸是否是对本文件夹的一次新的触摸。如果是,则touchId会被设置为该触摸点的ID,并且该触摸点的位置也会被记录下来。
当接收到一个TOUCH_RELEASED事件时,会检查touchId以确保其与正在处理的触摸点相匹配。如果匹配,则会重置touchID表示该次处理已经完成。
当接收到一个TOUCH_MOVED事件时,会检查touchId以确保其与正在处理的触摸点相匹配。如果匹配,则该文件夹会被移动到新的触摸点位置。如果不匹配,则可能是在该文件夹上有多个接触点。为了避免对同一个文件夹上的多个动作做出响应,该事件会被忽略掉。
改变触摸点的目标
触摸点的目标在触摸动作持续期间一般都会是同一个节点。但是,在某些情况下,你可能想要在动作持续期间改变触摸点的目标。
在Touch Events样例中,通过用一根手指触摸圆形并用第二根手指触摸另一个矩形可以让该圆形从一个矩形移动到另一个矩形中。因为在圆形移动完以后第二根手指仍然保持在该圆形上,此时拿起第一根手指并触摸不同的矩形将会使圆形再次移动。只有通过修改第二个触摸点的目标才可以完成上述动作。
该圆形是Ball类的一个实例。Ball类创建了一个圆形并为其添加了TOUCH_PRESSED、TOUCH_RELEASED、TOUCH_MOVED和TOUCH_STATIONARY事件的事件处理器。TOUCH_MOVED和TOUCH_STATIONARY事件的事件处理器是一样的。例6-2展示了该类的定义。
例6-2 Ball类的定义
private static class Ball extends Circle {
double touchx, touchy;
public Ball(int x, int y) {
super(35);
RadialGradient gradient = new RadialGradient(0.8, -0.5, 0.5, 0.5, 1,
true, CycleMethod.NO_CYCLE, new Stop [] {
new Stop(0, Color.FIREBRICK),
new Stop(1, Color.BLACK)
});
setFill(gradient);
setTranslateX(x);
setTranslateY(y);
setOnTouchPressed(new EventHandler() {
@Override public void handle(TouchEvent event) {
if (event.getTouchCount() == 1) {
touchx = event.getTouchPoint().getSceneX() - getTranslateX();
touchy = event.getTouchPoint().getSceneY() - getTranslateY();
setEffect(new Lighting());
}
event.consume();
}
});
setOnTouchReleased(new EventHandler() {
@Override public void handle(TouchEvent event) {
setEffect(null);
event.consume();
}
});
// Jump if the first finger touched the ball and is either
// moving or still, and the second finger touches a rectangle
EventHandler jumpHandler = new EventHandler() {
@Override public void handle(TouchEvent event) {
if (event.getTouchCount() != 2) {
// Ignore if this is not a two-finger touch
return;
}
TouchPoint main = event.getTouchPoint();
TouchPoint other = event.getTouchPoints().get(1);
if (other.getId() == main.getId()) {
// Ignore if the second finger is in the ball and
// the first finger is anywhere else
return;
}
if (other.getState() != TouchPoint.State.PRESSED ||
other.belongsTo(Ball.this) ||
!(other.getTarget() instanceof Rectangle) ){
// Jump only if the second finger was just
// pressed in a rectangle
return;
}
// Jump now
setTranslateX(other.getSceneX() - touchx);
setTranslateY(other.getSceneY() - touchy);
// Grab the destination touch point, which is now inside
// the ball, so that jumping can continue without
// releasing the finger
other.grab();
// The original touch point is no longer of interest so
// call ungrab() to release the target
main.ungrab();
event.consume();
}
};
setOnTouchStationary(jumpHandler);
setOnTouchMoved(jumpHandler);
}
}
当接收到一个TOUCH_PRESSED事件时,会检查触摸点的个数以确保只触摸了Ball类的实例。如果是,则该触摸点的位置会被记录下来,并且会为该圆形添加一个高亮效果来表示该圆形已被选中。
当接收到一个TOUCH_RELEASED事件时,高亮效果就会消失,表示该圆形未被选中。
当接收到一个TOUCH_MOVED或者TOUCH_STATIONARY事件时,会检查圆形移动需要的如下条件是否已满足:
● 触摸数量必须是2。
与事件关联的触摸点被认为是该次“跳跃”的起点。该事件可以访问该触摸动作的所有触摸点。触摸点集合中的第二个触摸点会被认为是该次“跳跃”的终点。
● 第二个触摸点的状态必须是PRESSED。
只有当第二个接触点已经产生时该圆形才会移动。第二个触摸点的所有其它状态都将被忽略。
● 第二个触摸点的目标必须是矩形。
该圆形只能从矩形跳跃到矩形,或者是在一个矩形之内。如果第二个触摸点的目标是任何其它类型的东西,圆形都将不会移动。
如果“跳跃”的条件得到了满足,该圆形将会跳到第二个触摸点的位置。要想再次跳跃,需要释放第一个触摸点并且触摸第三个位置,圆形将会如期望的那样跳跃到第三个位置。但是,当释放第一个接触点以后,目标为圆形的触摸点就消失了,所以该圆形就不会再收到触摸事件。如果不把两根手指都抬起来再重新开始的话就无法实现第二次跳跃。
要想在第二根手指保持在圆形上不放开并触摸一个新的位置的情况下实现第二次跳跃,可以使用grab()方法来让圆形成为第二个触摸点的目标。在grab()方法执行以后,第二个接触点的事件会被发送到圆形而不是最开始的矩形目标。圆形就可以监控新的触摸点并再次跳跃。