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 nativ

Wenn Sie Android direkt in Java programmieren wollen, schauen Sie sich gern mein Vorlesungsskript an, insbesondere das Modul Interaktion II.

3.2 Android mit Processing

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.2.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.2.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:

3.3 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.3.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.3.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.