Multitouch ist besonders intuitiv und effektiv, wenn Eingabefläche und Display identisch sind, wie es bei Smartphones, Tablets oder Touchscreens der Fall ist. Man spricht hier auch von direkter Manipulation.

In diesem Kapitel wird gezeigt, wie man sehr leicht Processing-Programme auf Android-Geräten ausführen kann, so dass Sie Smartphones und Tablets für Ihre Prototypen verwenden können.

In einem zweiten Teil wird auf Java eingegangen. Dort wird gezeigt, wie Sie direkte Manipulation auf aktuellen Windows-Notebooks mit Touchscreen realisieren können. Dies funktioniert mit JavaFX, welches seit Java 8 dort als Bibliothek integriert ist.

Der hier verwendete Code ist auch zum Download auf GitHub verfügbar:

3.1 Android

Sie können Processing-Programme unter Android laufen lassen. Die beste Referenz dazu ist die offizielle Seite Processing for Android.

Stellen Sie sicher, dass Sie Version 3 oder höher von Processing installiert haben. Zunächst mal müssen Sie das Android SDK runterladen und installieren. Folgen Sie dazu den Anweisungen unter Getting Started.

In Processing können Sie jetzt in dem Menü rechts oben (dort steht regulär "Java") auf Android Mode schalten:

Schließen Sie Ihr Android-Gerät per USB an. Sie müssen dann noch in den Einstellungen unter System > Entwickleroptionen den Schalter bei USB-Debugging auf EIN setzen.

Sobald Sie RUN drücken, wird Ihr Sketch compiliert, in ein Android-Paket gepackt, auf Ihr Endgerät geladen und direkt gestartet. Sie können sowohl im statischen Modus (siehe oben) als auch im aktiven Modus (setup/draw) Programme schreiben.

3.1.1 Single Touch

Wenn Sie die Variablen mouseX und mouseY verwenden, dann wird immer der erste Finger als Mauszeiger genommen. In setup() denken Sie daran, den Befehl fullScreen() aufzurufen, um den ganzen Bildschirm des Gerät einzunehmen. Es kann auch sinnvoll sein, die Orientierung (Portrait vs Landscape) zu fixieren. Das machen Sie mit

orientation(LANDSCAPE);

Sie können auch wie gewohnt die Systemvariablen width und height benutzen, um die Bildschirmgröße (in Pixeln) abzufragen.

Wie schauen uns ein einfaches Programm an:

void setup() {
  fullScreen();
  fill(255);
  noStroke();
}

void draw() {
  background(0, 255, 0);
  ellipse(mouseX, mouseY, 80, 80);
}

Sie können auch mousePressed() und mouseReleased() benutzen, um zu sehen, ob ein Finger auf dem Bildschirm liegt. Hier ein entsprechendes Programm:

boolean isTouching = false;

void setup() {
  fullScreen();
  orientation(LANDSCAPE);
}

void draw() {
  background(0, 255, 0);

  // Finger "Cursor"
  if (isTouching) {
    noStroke();
    fill(255);
    ellipse(mouseX, mouseY, 80, 80);
  }

  // Rahmen
  stroke(255);
  noFill();
  strokeWeight(5);
  rect(20, 20, width - 50, height - 50);
}

void mousePressed() {
  isTouching = true;
}

void mouseReleased() {
  isTouching = false;
}

Jetzt können Sie schon mit Ihrem Gerät interagieren! Aber wir wollen natürlich die Multitouch-Fähigkeiten der Anroid-Geräte ausnutzen.

3.1.2 Multitouch

Stand: 22.12.2017

Über den Array touches können Sie in jedem Frame die aktuellen Touch-Punkte als Objekte abfragen (siehe auch Touches array).

Jedes Touch-Objekt hat die folgenden Eigenschaften:

Mit dem folgenden Programm können Sie diese Eigenschaften in Aktion sehen.

void setup() {
  fullScreen();
  textFont(createFont("SansSerif", 60));
  textAlign(CENTER, CENTER);
}    

void draw() {
  background(255);
  for (int i = 0; i < touches.length; i++) {
    float d = 200 * (touches[i].area * 10);

    noFill();
    stroke(0);
    strokeWeight(10);
    ellipse(touches[i].x, touches[i].y, d, d);

    fill(100);
    text("id " + touches[i].id, touches[i].x,
    touches[i].y - d/2 - 200);

    text("(" + (int)touches[i].x + ", " + (int)touches[i].y + ")",
    touches[i].x, touches[i].y - d/2 - 150);

    text("pressure " + nf(touches[i].pressure,1,2) +
    ", area " + nf(touches[i].area,1,2),
    touches[i].x, touches[i].y - d/2 - 90);
  }
}

Sie außerdem die Möglichkeit auf die folgenden Touch-Events zu reagieren:

Dieser Teil scheint veraltet zu sein...

Um Multitouch-Punkte abzurufen, bedienen wir uns der Android-Bibliotheken, die ja - genauso wie Processing - in Java geschrieben sind. Genauer gesagt betrachten wir die Klasse android.view.MotionEvent (der Link bringt Sie zur Online-API). Ein MotionEvent-Objekt wird immer dann erzeugt, wenn ein Finger die Oberfläche neu berührt oder verlässt. Das Objekt beschreibt aber immer alle derzeit auf der Oberfläche befindlichen Finger.

Der folgende Code fängt zunächst mal das Objekt ab und speichert es in einer Variablen. Dazu wird die Methode dispatchTouchEvent() überschrieben, die von Android aufgerufen wird.

// Touch-Event abfangen und zwischenspeichern
// Ausgabe zeigt Anzahl der Finger
// Problem: es bleibt immer ein Finger stehen

import android.view.MotionEvent;

MotionEvent motionEvent;

void draw() {
  background(#34DE07);

  if (motionEvent != null) {
    println(motionEvent.getPointerCount() + " touches");
  }
}

// add the following to the bottom of your sketch; this code overrides the
// built-in method, then sends the data on after we capture it
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
  motionEvent = event;
  // pass data along when done!
  return super.dispatchTouchEvent(event);        
}

Sie sehen, dass die Ausgabe fast richtig ist. Wenn alle Finger die Oberfläche verlassen, wird allerdings immer noch "1 touches" angezeigt. Darum kümmern wir uns später.

Jetzt wollen wir erstmal die Punkte zeichnen. Dazu holen wir uns aus dem MotionEvent-Objekt die entsprechenden Daten über IDs. Alle Touchpunkte sind durchnummeriert von 0 ... N. Die x-Koordinate des 4. Punkts bekomme ich dann über motionEvent.getX(3) .

// Touchpunkte zeichnen
// Problem: es bleibt immer ein Punkt stehen

import android.view.MotionEvent;

MotionEvent motionEvent;

void setup() {
  noStroke();
  fill(255);
}

void draw() {
  background(#34DE07);

  if (motionEvent != null) {
    println(motionEvent.getPointerCount() + " touches");
    for (int i = 0; i < motionEvent.getPointerCount(); i++) {
      float x = motionEvent.getX(i);
      float y = motionEvent.getY(i);
      int id = motionEvent.getPointerId(i);
      ellipse(x, y, 100, 100);
      textSize(24);
      text(""+id, x-10, y-72);
    }
  }
}

// add the following to the bottom of your sketch; this code overrides the
// built-in method, then sends the data on after we capture it
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
  motionEvent = event;
  // pass data along when done!
  return super.dispatchTouchEvent(event);
}

Unser letztes Problem ist der stehenbleibende Punkt. Dieser Punkt hat den Zustand "aufgehoben" (ACTION_UP), verbleibt aber in der Liste. Daher müssen wir bei jedem Punkt nachfragen, ob er evtl. schon den Zustand "aufgehoben" hat. In dem Fall zeichnen wir nicht.

Im Code ergänzen wir in draw():

void draw() {
  background(#34DE07);

  if (motionEvent != null) {
    println(motionEvent.getPointerCount() + " touches");
    for (int i = 0; i < motionEvent.getPointerCount(); i++) {

      // Zustand des aktuellen Punkts abfragen!
      if (! (i == motionEvent.getActionIndex() &&
        motionEvent.getActionMasked() ==
        MotionEvent.ACTION_UP)) {
        float x = motionEvent.getX(i);
        float y = motionEvent.getY(i);
        int id = motionEvent.getPointerId(i);
        ellipse(x, y, 100, 100);
        textSize(24);
        text(""+id, x-10, y-72);
      }
    }
  }
}

3.1.3 Multitouch-Punkte als Liste [veraltet]

In der Regel benötigen Sie alle Multitouch-Punkte z.B. in einer Liste, damit Sie diese weiterverarbeiten können. Dazu müssen Sie jeden einzelnen Punkt als Objekt speichern, z.B. in der Klasse TouchPoint. Diese Klasse kann entweder immutable sein, d.h. die Objekte werden nie mehr verändert (sondern immer neu erzeugt), oder sie ist mutable, d.h. die Objekte werden immer wieder geupdatet. Wir schauen uns zunächst die immutable-Variante an.

List mit immutable Touchpoints

Die Klasse TouchPoint sieht wie folgt aus (sie behandelt auch das Zeichnen der Punkte):

/**
 * IMMUTABLE representation of a single touch point.
 */
class TouchPoint {

  final static float MAX_RING_WIDTH = 18;
  final static float CIRC_RADIUS = 60;

  final int id;
  final float x;
  final float y;
  final float pressure;
  final float size;
  final float orient; // orientation in radians, 0 = up

  public TouchPoint(int id, float x, float y, float pressure,
  float size, float orient) {
    this.id = id;
    this.x = x;
    this.y = y;
    this.pressure = pressure;
    this.size = size;
    this.orient = orient;
  }

  void draw() {
    fill(255);
    noStroke();
    ellipse(x, y, 40, 40);
    stroke(255, 80);
    noFill();

    // outer ring width is proportional to size
    strokeWeight(size * MAX_RING_WIDTH);

    // inner circle
    ellipse(x, y, 2*CIRC_RADIUS, 2*CIRC_RADIUS);

    // orientation line
    stroke(#FF3B3B); // red
    strokeWeight(2);
    line(x - CIRC_RADIUS * cos(PI/2-orient),
    y - CIRC_RADIUS * sin(PI/2-orient),
    x + CIRC_RADIUS * sin(orient),
    y + CIRC_RADIUS * cos(orient));

    // info text
    textAlign(LEFT, CENTER);
    text("x: " + x, x + CIRC_RADIUS + 15, y - 8);
    text("y: " + y, x + CIRC_RADIUS + 15, y + 8);
    text("s: " + size, x + CIRC_RADIUS + 15, y + 24);
    text("p: " + pressure, x + CIRC_RADIUS + 15, y + 40);
    text("o: " + orient, x + CIRC_RADIUS + 15, y + 56);
    textAlign(CENTER, CENTER);
    fill(0);
    text("" + id, x, y - CIRC_RADIUS);
    text("" + id, x, y + CIRC_RADIUS);
  }
}

Im Hauptcode sammeln wir die Touchpunkte in der Methode dispatchTouchEvent(). Da diese Methode von außen aufgerufen und nebenläufig aufgerufen wird, kann es sein, dass genau in dem Moment, in dem draw() die Punkte zeichnet, die Liste verändert wird. Daher wird mit einem Lock gearbeitet, welches erzwingt, dass die zwei mit synchronized (lock) markierten Bereiche gleichzeitig (nebenläufig) abgearbeitet werden.

import java.util.*;
import android.view.MotionEvent;

Object lock = new Object(); // for sync lock
List points = new ArrayList();

void setup() {
  fill(255);
  stroke(255);
  textSize(14);
}

void draw() {
  background(#BA5EF5);

  // draw touch points
  synchronized (lock) {
    for (TouchPoint p: points) {
      p.draw();
    }
  }
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {

  // collect touch points
  synchronized (lock) {
    points.clear();
    for (int i = 0; i < event.getPointerCount(); i++) {

      // ignore finger being lifted right now
      if (i != event.getActionIndex() ||
        event.getActionMasked() != MotionEvent.ACTION_UP) {

          // create point and add to list
          points.add(new TouchPoint(event.getPointerId(i),
          event.getX(i), event.getY(i), event.getPressure(i),
          event.getSize(i), event.getOrientation(i)));
      }
    }
  }
  return super.dispatchTouchEvent(event);      
}

Die Anwendung zeigt alle Touchpunkte mit verschiedenen Angaben, die Android zur Verfügung stellt, z.B. ID, Druckstärke, Druckgröße und Orientierung (Orientierung scheint derzeit nicht aktiviert zu sein).

Liste mit mutable Touchpoints

Manchmal ist es notwendig, dass die Identität der Touchobjekte immer gleich bleibt. Das heißt, die Objekte werden nicht, wie in der obigen Version, in jeder Runde neu erzeugt, sondern modifiziert.

Das erfordert ein bisschen mehr Arbeit beim Hinzufügen der Punkte. Ich stelle Ihnen hier den Code der Klasse TouchPoint und des Hauptprogramms ein:

Klasse TouchPoint (unterscheidet sich lediglich darin, dass die Eigenschaften nicht final sind, und fügt neue Eigenschaft active hinzu):

/**
 * MUTABLE representation of a single touch point.
 */
class TouchPoint {

  final static float MAX_RING_WIDTH = 18;
  final static float CIRC_RADIUS = 60;

  boolean active = true;
  int id;
  float x;
  float y;
  float pressure;
  float size;
  float orient; // orientation in radians, 0 = up

  public TouchPoint(int id, float x, float y, float pressure,
    float size, float orient) {
    this.id = id;
    this.x = x;
    this.y = y;
    this.pressure = pressure;
    this.size = size;
    this.orient = orient;
  }

  void draw() {
    fill(255);
    noStroke();
    ellipse(x, y, 40, 40);
    stroke(255, 80);
    noFill();

    // outer ring width is proportional to size
    strokeWeight(size * MAX_RING_WIDTH);

    // inner circle
    ellipse(x, y, 2*CIRC_RADIUS, 2*CIRC_RADIUS);

    // orientation line
    stroke(#FF3B3B); // red
    strokeWeight(2);
    line(x - CIRC_RADIUS * cos(PI/2-orient),
    y - CIRC_RADIUS * sin(PI/2-orient),
    x + CIRC_RADIUS * sin(orient),
    y + CIRC_RADIUS * cos(orient));

    // info text
    textAlign(LEFT, CENTER);
    text("x: " + x, x + CIRC_RADIUS + 15, y - 8);
    text("y: " + y, x + CIRC_RADIUS + 15, y + 8);
    text("s: " + size, x + CIRC_RADIUS + 15, y + 24);
    text("p: " + pressure, x + CIRC_RADIUS + 15, y + 40);
    text("o: " + orient, x + CIRC_RADIUS + 15, y + 56);
    textAlign(CENTER, CENTER);
    fill(0);
    text("" + id, x, y - CIRC_RADIUS);
    text("" + id, x, y + CIRC_RADIUS);
  }
}

Im Hauptcode nehmen wir jetzt eine HashMap statt einer einfachen Liste. Wir kümmern uns darum, dass die entsprechenden Objekte immer geupdatet bzw. entfernt werden.

import java.util.*;
import android.view.MotionEvent;

Object lock = new Object(); // for sync lock
HashMap points = new HashMap();

void setup() {
  fill(255);
  stroke(255);
  textSize(14);
}

void draw() {
  background(#BA5EF5);

  // draw touch points
  synchronized (lock) {
    for (TouchPoint p: points.values()) {
      if (p.active)
        p.draw();
    }
  }
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {

  // update HashMap of touch points
  synchronized (lock) {

    // Marker active means: new touch point
    for (TouchPoint p: points.values())
      p.active = false;

    for (int i = 0; i < event.getPointerCount(); i++) {

      // ignore finger being lifted right now
      if (i != event.getActionIndex() ||
          event.getActionMasked() != MotionEvent.ACTION_UP)
        {
        TouchPoint p = points.get(i);
        if (p == null) {
          // new touch point => create
          points.put(event.getPointerId(i),
          new TouchPoint(event.getPointerId(i),
          event.getX(i), event.getY(i), event.getPressure(i),
          event.getSize(i), event.getOrientation(i)));
        }
        else {
          // touch point exists already => update
          p.active = true;
          p.id = event.getPointerId(i);
          p.x = event.getX(i);
          p.y = event.getY(i);
          p.pressure = event.getPressure(i);
          p.orient = event.getOrientation(i);
          p.size = event.getSize();
        }
      }
    }
  }

  return super.dispatchTouchEvent(event);
}

3.2 Java

In Java 8 können Sie auf Touchpunkte reagieren, die direkt vom Betriebssystem kommen, also z.B. von Windows 8 oder Windows 10. Dies funktioniert über JavaFX, welches fest in Java 8 integriert ist.

Falls Sie sich mit JavaFX nicht auskennen, schauen Sie sich mein Skript unter michaelkipp.de/processing an. Dort finden Sie ab Kapitel 22 eine Einführung in die Oberflächenprogrammierung mit Java und JavaFX. Den folgenden Code finden Sie auch in GitHub als MultitouchFX.

Wenn Sie ein JavaFX-Projekt (oder eine JavaFX-Startklasse) erzeugt haben, können Sie an das Scene-Objekt verschiedene Handler hängen, die auf die Events "Finger aufgesetzt", "Finter bewegt" und "Finger weggenommen" reagieren.

Scene scene = new Scene(root, 1000, 800); // root ist eine Pane

scene.setOnTouchPressed(e -> {
  // Code
});

scene.setOnTouchMoved(e -> {
  // Code
});

scene.setOnTouchReleased(e -> {
  // Code
});

Bei diesen Handlern wird ein TouchPoint-Objekt übergeben, das einen eindeutigen ID vom Typ int hat.

3.2.1 Touch-Cursor

Eine Möglichkeit, die vielen Touchpunkte darzustellen, ist, für jeden Punkt ein darstellbares Objekt (z.B. einen Circle) zu erschaffen und per Hashtabelle mit dem ID zu verknüpfen.

Sobald wir etwas komplexeres als einen Kreis darstellen wollen, benötigen wir eine eigene Klasse:

public class TouchCursor extends Group {

    private int radius;
    private Circle circle;
    private Line lineHor;
    private Line lineVert;

    public TouchCursor(int radius) {
        this.radius = radius;
        circle = new Circle(radius);
        circle.setFill(Color.TRANSPARENT);
        circle.setStroke(Color.BLACK);
        circle.setStrokeWidth(2);
        lineHor = new Line();
        lineHor.setStroke(Color.BLACK);
        lineHor.setStrokeWidth(2);
        lineVert = new Line();
        lineVert.setStroke(Color.BLACK);
        lineVert.setStrokeWidth(2);
        getChildren().addAll(circle, lineHor, lineVert);
    }

    public void setCenter(double x, double y) {
        circle.setCenterX(x);
        circle.setCenterY(y);
        lineHor.setStartX(x - radius);
        lineHor.setStartY(y);
        lineHor.setEndX(x + radius);
        lineHor.setEndY(y);
        lineVert.setStartX(x);
        lineVert.setStartY(y - radius);
        lineVert.setEndX(x);
        lineVert.setEndY(y + radius);
    }
}

Im Hauptprogramm speichern wir für jeden Touchpunkt (über seinen ID) ein eigenes TouchCursor-Objekt und vernichten dies, sobald der Touchpunkt verschwindet.

private HashMap id2cursor = new HashMap<>();

Hier das vollständige Hauptprogramm:

public class TouchGui extends Application {

    private HashMap id2cursor = new HashMap<>();

    @Override
    public void start(Stage stage) {
        Pane root = new Pane();
        root.setPrefSize(1000, 800);

        Scene scene = new Scene(root, 1000, 800);

        scene.setOnTouchPressed(e -> {
            TouchCursor cur = id2cursor.get(e.getTouchPoint().getId());
            if (cur == null) {
                TouchCursor c = new TouchCursor(40);

                id2cursor.put(e.getTouchPoint().getId(), c);
                root.getChildren().add(c);
                c.setCenter(e.getTouchPoint().getX(), e.getTouchPoint().getY());
            } else {
                cur.setCenter(e.getTouchPoint().getX(), e.getTouchPoint().getY());
                cur.setVisible(true);
            }
        });

        scene.setOnTouchMoved(e -> {
            TouchCursor cur = id2cursor.get(e.getTouchPoint().getId());
            cur.setCenter(e.getTouchPoint().getX(), e.getTouchPoint().getY());
        });

        scene.setOnTouchReleased(e -> {
            TouchCursor c = id2cursor.remove(e.getTouchPoint().getId());
            c.setVisible(false);
            root.getChildren().remove(c);
        });

        stage.setTitle("TouchGui");
        stage.setScene(scene);
        stage.setOnCloseRequest(e -> {
            Platform.exit();
        });
        stage.setFullScreen(true);
        stage.show();
    }
}

3.2.2 Rotation, Skalierung, Translation (RST)

Eine der meist bekannten Anwendungen für Multitouch ist das bewegen von einfachen Formen, z.B. rechteckige Fotos. Diese kann man mit einem Finger verschieben (Translation) oder mit zwei Finger drehen (Rotation) oder vergrößern/verkleinern (Skalieren). Diese drei Operationen werden manchmal auch mit RST abgekürzt.

Wir zeigen hier ein kleines Beispiel mit einem Setup, wo verschiedene primitive Formen (Rechteck, Kreis, Polygon) bewegt werden können:

Um die Operationen RST zu handhaben, gibt es in JavaFX eigene Handler für Rotation (setOnRotate) und Skalierung (setOnZoom), die uns viel Arbeit abnehmen.

Für die Translation berechnen wir zunächst einen Offset-Vektor (touchOffsetX, touchOffsetY) zwischen Finger und Objektursprung und verwenden diesen dann als Korrektur, wenn wir das Objekt verschieben.

Die folgende Klasse ist ein leichtgewichtiger Wrapper um eine Shape, welche den Touchinput auf dem Objekt abfängt und sich z.B. den Offset-Vektor für die Translation merkt.

public class Touchable {

    private Shape shape;
    private double touchOffsetX;
    private double touchOffsetY;
    private int translateTouchID = -1;

    public Touchable(Shape s) {
        shape = s;

        shape.setOnRotate(e -> {
            shape.setRotate(shape.getRotate() + e.getAngle());
            e.consume();
        });

        shape.setOnZoom(e -> {
            shape.setScaleX(shape.getScaleX() * e.getZoomFactor());
            shape.setScaleY(shape.getScaleY() * e.getZoomFactor());
            e.consume();
        });

        shape.setOnTouchPressed(e -> {
            if (translateTouchID == -1) {
                touchOffsetX = e.getTouchPoint().getSceneX() - shape.getTranslateX();
                touchOffsetY = e.getTouchPoint().getSceneY() - shape.getTranslateY();
                translateTouchID = e.getTouchPoint().getId();
            }
            e.consume();
        });

        shape.setOnTouchMoved(e -> {
            if (e.getTouchPoint().getId() == translateTouchID) {
                shape.setTranslateX(e.getTouchPoint().getSceneX() - touchOffsetX);
                shape.setTranslateY(e.getTouchPoint().getSceneY() - touchOffsetY);
            }            
            e.consume();
        }
        );

        shape.setOnTouchReleased(e -> {
            if (e.getTouchPoint().getId() == translateTouchID) {
                translateTouchID = -1;
            }
            e.consume();
        }
        );
    }
}

Im Hauptprogramm wird die GUI erzeugt und die Objekte in eine Pane gefügt. Zuvor werden die Objekte mit unseren Wrapper "eingewickelt". Wir zeigen hier nur den Teil, wo die Objekte hinzugefügt werden. Den kompletten Code finden Sie als Netbeans-Projekt in dem GitHub-Projekt MultitouchRST.

// make pane
 Pane centerPane = new Pane();

 // get screen dimensions (may not work on mobile devices)
 Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
 double w = d.getWidth();
 double h = d.getHeight();

 // create touchable objects
 Rectangle o1 = new Rectangle(w/2, h/2, 300, 300);
 o1.setFill(Color.RED);
 new Touchable(o1);

 Rectangle o2 = new Rectangle(w/5, h/4, 350, 210);
 o2.setFill(Color.ORANGE);
 new Touchable(o2);

 Circle o3 = new Circle(3*w/4, 3*h/4, 100, Color.BROWN);
 new Touchable(o3);

 Polygon o4 = new Polygon(new double[]{
     0.0, 0.0,
     250.0, 10.0,
     280.0, 240.0,
     20.0, 210.0});
 o4.setFill(Color.GREEN);
 o4.setTranslateX(3*w/4);
 o4.setTranslateY(h/3);

 new Touchable(o4);

 centerPane.getChildren().addAll(o1, o2, o3, o4);
 

Den kompletten Code finden Sie hier: MultitouchRST.