Start Magazin Android-Anwendungen mit Multitouch ausstatten

Android-Anwendungen mit Multitouch ausstatten

Seit Version 2.0 von Android stehen allen Entwicklern auch Multitouch-Funktionen offen, die zuvor Systemanwendungen vorbehalten waren. Dieser Artikel zeigt, wie Sie das Feature in der Praxis einsetzen.

Das iPhone macht vor, dass Multitouch-Bedienung echte Vorteile über Spielereien hinaus haben kann. Gerade auf Smartphones sind Gesten nützlich. Kein Wunder, dass auch der Wettbewerb sich genau anschaut, was sich für die Android-Plattform nutzen lässt. Google hat das API ab Version 2.0 so erweitert, dass Entwickler und Anwender sowohl von Gesten als auch in individuellen Anwendungen von der Mehrfingertechnik profitieren.

Unterschiedliche Touchpads

Den Grad der Multitouch-Unterstützung bei Android zu betrachten ist eine wichtige Voraussetzung, um die Funktionsweise des API zu verstehen. Im Gegensatz zum iPhone kommt es hier entscheidend auf die Kombination von Hard- und Software an. Zunächst müssen Entwickler Multitouch unterstützen, was erst ab API-Level 5 (Android 2.0 oder höher) gelingt. Die verbaute Hardware beeinflusst den Grad der Unterstützung erheblich: Obwohl beispielsweise HTC sein G1 zunächst mit Android 1.0 ausgeliefert hatte, als Multitouch noch nicht einmal angekündigt war, erkannte dessen Hardware bereits einige Gesten.

Aber nicht alle Bedienflächen sind dazu in der Lage: Gerade Geräte mit resistiven Touchscreens erkennen oft nur einen Finger, etwa das HTC Tatoo. Andere Geräte liefern nur einen umschließenden Rahmen: Hier ist das Rechteck zwischen zwei Fingern bestimmbar, aber nicht zuverlässig die einzelnen Finger unabhängig voneinander.

Diese Hardwarebeschränkung illustriert Abbildung 1: Es gibt zwei unterschiedliche Positionen von zwei Fingern in einer solchen Bounding Box. Bewegen sich Finger von der einen zur anderen Position, verwechselt das Gerät oft die Fingerpositionen und kann nur noch den Rahmen eindeutig bestimmen. Viele HTC-Geräte wie das Desire verwenden diese Technologie.

Abbildung 1: Problem sich kreuzender Finger: Der umschließende Rahmen der Fingerpositionen ist bei beiden Varianten gleich. Das HTC Desire etwa kann daher ihre genaue Haltung nicht bestimmen.
Abbildung 1: Problem sich kreuzender Finger: Der umschließende Rahmen der Fingerpositionen ist bei beiden Varianten gleich. Das HTC Desire etwa kann daher ihre genaue Haltung nicht bestimmen.

Das erste Smartphone mit Android 2.0, das Motorola Milestone, erkennt die Position zweier Finger individuell. Das Galaxy S ist gar in der Lage, bis zu vier Finger unabhängig voneinander zu registrieren und zu verarbeiten. Auch aufgrund dieser Hardwarebeschränkungen ergibt sich für Entwickler in der Regel, dass sie Multitouch für Apps noch nicht voraussetzen, da sonst viele Geräte ausgeschlossen wären.

Daher vereinfacht Mutltitouch zwar optional eine App, Entwickler sollten die Technik aber nicht überall als zwingend voraussetzen. Bei Apps, für die Multitouch eine essenzielle Funktion ist, sollten sie einen Vermerk im Manifest eintragen, um sicherzustellen, dass nur Multitouch-fähige Geräte die App aus dem Android Market installieren dürfen:

<uses-feature android:name = 
"android.hardware.touchscreen.multitouch" />

Wer diese einschränkenden Hinweise verinnerlicht hat, darf sich dem Multitouch-API zuwenden: Es basiert auf dem Touch-API, das bereits seit Android 1.0 existiert. Die wesentliche Erweiterung bezieht sich auf das MotionEvent, das ab Android 2.0 zusätzliche Daten mit sich führt [1]. Ansonsten dürfen Entwickler ihre Apps genauso wie für einfache Touch-Interaktion aufbauen.

Dafür nehmen sie zunächst MotionEvent-Objekte in Empfang, wofür es unterschiedliche Möglichkeiten gibt: Typischerweise installiert der Programmierer mit setOnTouchListener() für eine View einen Listener oder er überschreibt die Methode onTouchEvent() der Klassen Activity oder View: Ein Gerüst für einen onTouchListener zeigt Listing 1.

Listing 1

onTouchListener

01 public boolean onTouch(View v, MotionEvent event) {
02   if (event.getAction() == MotionEvent.ACTION_DOWN) {
03     int x = event.getX();
04     int y = event.getX();
05     doSomething(x,y);
06   }
07 }

Berühr-Ereignisse

Das Beispiel zeigt, dass der Entwickler alle relevanten Daten wie die durchgeführte Aktion oder die x- und y-Koordinate über Zugriffsmethoden im Objekt MotionEvent erreicht. Nach einem ACTION_DOWN-Event, das Android auslöst, wenn der Finger zum ersten Mal den Screen berührt, sendet das Betriebssystem ACTION_MOVE-Events für Bewegungen des Fingers.

Dies geschieht so lange, bis der Anwender seinen Finger vom Screen nimmt. Das signalisiert Android der Anwendung mit ACTION_UP. Wie die Methodensignatur von onTouch() andeutet, lässt sich ein OnTouchListener auf mehreren Views registrieren. Zur Unterscheidung bekommt der Entwickler das jeweils auslösende View-Objekt übergeben.

Mit dieser kleinen Auffrischung zu MotionEvents lassen sich bereits Multitouch-Erweiterungen entwickeln. Strukturell sind keine Anpassungen an der App nötig, da alle Multitouch-Daten den gleichen Weg nehmen. Damit erhält der Code der Methode getAction() eine weitere Bedeutung: In dem 32-Bit-Integer sind zwei Werte kodiert. Sie lassen sich über entsprechende Bitmasken und Bit-Shifts voneinander trennen:

int action = event.getAction() &
             MotionEvent.ACTION_MASK;
int pointerIndex = (event.getAction() &
        MotionEvent.ACTION_POINTER_ID_MASK)
    >> MotionEvent.ACTION_POINTER_ID_SHIFT;

Damit entspricht die Variable action wieder dem auch ohne Mutltitouch bekannten Code. Die Variable pointerIndex enthält einen Index auf den für das Event verantwortlichen Pointer.

Unter Pointer versteht Android an dieser Stelle einen Finger, der mit dem Touchscreen interagiert. Streng genommen ist er unter Android aber nicht auf Fingerbedienung beschränkt, sodass ein Pointer in Zukunft auch eine andere Eingabequelle sein könnte, zum Beispiel ein Mauszeiger. Jedenfalls ermitteln Programmierer über getPointerCount() deren Anzahl. Anstelle der parameterlosen Zugriffsmethoden des MotionEvent treten nun überladene Methoden mit jeweils dem gewünschten Pointer-Index wie beispielsweise getX(int pointerIndex). Je nach Anwendungsfall genügen dem Entwickler diese Informationen, um eine Multitouch-Anwendung zu entwerfen.

Mehr Freiheit für Finger

Möchte er jedoch zusätzlich einzelne Finger über einen Zeitraum hinweg verfolgen, kommen die Pointer-IDs ins Spiel. Für deren Ermittlung sorgt die Methode getPointerId(), die den Pointer-Index als Parameter erwartet. Das folgende Beispiel veranschaulicht die Bedeutung von Pointer-IDs: Zeige- und Mittelfinger berühren nacheinander den Touchscreen. Nimmt der Anwender dann einen Finger vom Screen, können Entwickler ohne die Pointer-ID nicht feststellen, welcher Finger das war.

Der Zeigefinger bekommt in dem Beispiel die Pointer-ID 0 zugewiesen, da er der erste Pointer auf dem Screen war, während der Mittelfinger die darauf folgende Pointer-ID 1 bekommt. Anhand der IDs identifiziert der Programmierer die einzelnen Finger und bestimmt damit die verbleibenden Finger.

Die Verwendung von mehreren Pointern bringt zwei neue Action-Codes mit sich: ACTION_POINTER_DOWN und ACTION_POINTER_UP entsprechen ACTION_DOWN und ACTION_UP mit dem Unterschied, dass Android sie erst ab dem zweiten Finger einsetzt.

Ein Beispiel verdeutlicht die Abfolge von Aktionen: Zuerst berührt Finger 1 den Screen und löst eine ACTION_DOWN aus. Berührt Finger 2 den Screen, ergibt das ein ACTION_POINTER_DOWN-Event. Bewegen sich Finger, meldet Android ACTION_MOVE. Verlässt Finger 1 die Glasscheibe, erhält der Code ein ACTION_POINTER_UP-, bei Finger 2 allerdings ein ACTION_ UP-Event.

Die App Multitouch Test in Abbildung 2 fasst die Kernaspekte zusammen: Die im Android Market erhältliche App dient zur Visualisierung von Multitouch-Events und hält als Experimentierfeld für eigene Erweiterungen her [2].

Abbildung 2: Die Multitouch-Test-App erlaubt Ereignisse zu debuggen und zeigt sie dazu farbig an.
Abbildung 2: Die Multitouch-Test-App erlaubt Ereignisse zu debuggen und zeigt sie dazu farbig an.

Multitouch selbst testen

Die Funktionen der App sind überschaubar: Sie visualisiert jeden Pointer durch einen Kreis, der je Action-Code eine andere Farbe annimmt. Er wird blau, wenn der Anwender die Fläche berührt, grün, wenn er seinen Finger bewegt, oder grau, wenn er einen der Codes für das Wegnehmen auslöst. Die App zeigt auch die Pointer-ID und den letzten Action-Code über den Kreisen an. Damit bekommen Android-Entwickler schnell ein Gefühl dafür, was im Hintergrund passiert.

Den Code der App implementiert der Programmierer im Kontext einer Activity. Listing 2 zeigt die Implementierung eines OnTouchListener, den er mittels setOnTouchListener() an die View bindet und der in Listing 3 zu sehen ist. Aus Platzgründen sind Member-Definitionen wie beispielsweise die Arrays points[] und lastActions[] weggelassen. Der komplette Sourcecode ist als Download erhältlich [2].

Listing 2

onTouchListener empfängt Motion-Events

01 public boolean onTouch(View v, MotionEvent e) {
02   int action = e.getAction() &
03                MotionEvent.ACTION_MASK;
04   int pointerIndex = (e.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;
05   int actionId = e.getPointerId(pointerIndex);
06   pointerCount = e.getPointerCount();
07   if (actionId < MAX_POINTERS) {
08     lastActions[actionId] = action;
09   }
10   for (int i = 0; i < pointerCount; i++) {
11     int pointerId = e.getPointerId(i);
12     if (pointerId < MAX_POINTERS) {
13       points[pointerId] = new PointF(e.getX(i),
14                                      e.getY(i));
15       if (action == MotionEvent.ACTION_MOVE) {
16         lastActions[pointerId] = action;
17       }
18     }
19   }
20   touchView.invalidate();
21   return true;
22 }

Listing 3

Die View visualisiert die Werte

01 class TouchView extends View {
02   public TouchView(Context context) {
03     super(context);
04     setBackgroundColor(Color.WHITE);
05   }
06
07   protected void onDraw(Canvas canvas) {
08     super.onDraw(canvas);
09     for (int i = 0; i < MAX_POINTERS; i++) {
10       PointF point = points[i];
11       if (point != null) {
12         paint.setColor(getColor(i));
13         canvas.drawCircle(point.x, point.y,
14                           radius, paint);
15         String text = getActionText(i);
16         float width = paint.measureText(text);
17         canvas.drawText(text, point.x-width/2,
18             point.y-radius-calcDevicePixels(8),
19             paint);
20       }
21     }
22     canvas.drawText("Pointer: " + pointerCount,
23        10, calcDevicePixels(30), paintInfoText);
24   }
25 }

Die ersten Zeilen in Listing 2 extrahieren den Action-Code und den Pointer-Index. Danach werden der Action-Code sowie die Pointer-Positionen in die Arrays lastAction[] beziehungsweise points geschrieben. Der Index beider Arrays entspricht jeweils der Pointer-ID. Die Pointeranzahl ist durch die Konstante MAX_POINTERS limitiert: Die App verwaltet maximal 20 Pointer.

Eine Besonderheit gibt es beim Setzen der Action-Codes: So liefert das MotionEvent bei ACTION_POINTER_DOWN mehrere Werte, wobei sich ACTION_POINTER_DOWN jedoch nur auf eine Pointer-ID bezieht. Nur im Falle von ACTION_MOVE lassen sich die Werte von lastAction() für alle im MotionEvent enthaltenen Pointer setzen. Schließlich stößt touchView.invalidate() in Zeile 20 ein Neuzeichnen der View an und zeigt die neuen Werte.

Die View der App ist eine eigene Klasse, die direkt von View abgeleitet ist, wie Listing 3 zeigt. Mittels des Canvas-Objekts lassen sich einfach die Kreise in einer Schleife zeichnen. Die dafür nötigen Koordinaten und Action-Codes hat zuvor der onTouchListener aus Listing 2 in den Arrays points[] und lastActions[] gesammelt. Farbe und Text eines Kreises ergeben sich aus den Methoden getColor() und getActionText(). Eine Mehrfachauswahl ermittelt abhängig vom letzten Action-Code des Pointers den Rückgabewert.

Schließlich zeichnet die App noch die Anzahl der zuletzt im MotionEvent vorhandenen Pointer als Text auf. Die Methode calcDevicePixels() ist eine kleine Hilfsmethode für die Ermittlung einer gerätespezifischen Pixelanzahl, die von der jeweiligen Pixeldichte des Screens abhängig ist.

Das Multitouch-API unter Android 2.2 vereinfacht zwar den Code, schränkt aber die Einsatzbasis ein, da es erst wenige Geräte mit diesem Softwarestand gibt. Google hat die Wertermittlung von Action-Code und Pointer-Index vereinfacht: Bitmasken und Bit-Shifts benötigt der Entwickler nicht mehr. Dafür liefern die Methoden getActionMasked() und getActionIndex() gleich die gewünschten Werte aus dem MotionEvent.

Android 2.2 hat übrigens die Konstanten ACTION_POINTER_ID_MASK und ACTION_POINTER_ID_SHIFT angepasst, die in den Beispielen vorkommen. Da sie sich nicht auf die ID, sondern auf den Index eines Pointers beziehen, lauten sie jetzt richtigerweise ACTION_POINTER_INDEX_MASK sowie ACTION_POINTER_INDEX_SHIFT.

Die vielleicht interessanteste Neuerung ist der ScaleGestureDetector, der Pinch-and-Zoom-Gesten erkennt. Für deren Einsatz sind drei Schritte nötig: Erstens die Instanzierung eines ScaleGestureDetector, zweitens das Weiterleiten jedes MotionEvent an den ScaleGestureDetector, drittens das Entgegennehmen der neuen Daten in einem speziellen Listener. Diese drei Schritte in den Methoden init(), onTouchEvent() und der Klasse MyScaleListener zeigt Listing 4.

Listing 4

Pinch-and-Zoom mit dem ScaleGestureDetector

01 // beispielsweise im Konstruktor aufrufen
02 private void init(Context context) {
03   scaleDetector = new ScaleGestureDetector(
04                 context, new MyScaleListener());
05 }
06
07 public boolean onTouchEvent(MotionEvent ev) {
08   scaleDetector.onTouchEvent(ev);
09   // ...
10 }
11 private class MyScaleListener
12 extends ScaleGestureDetector.SimpleOnScaleGestureListener {
13   public boolean
14   onScale(ScaleGestureDetector detector) {
15     skalierung *= detector.getScaleFactor();
16     invalidate();
17     return true;
18   }
19 }

Die Methode onScale() skaliert die View durch das Multiplizieren mit der Property scaleFactor des ScaleGestureDetector. Das Zentrum der Skalierung ließe sich auch mit den Methoden getFocusX() und getFocusY() ermitteln.

Anfassen erlaubt

Hard- und Software auf Android-Geräten üben einen großen Einfluss auf die Multitouch-Unterstützung aus. Neben der Abhängigkeit von Android 2.0 oder höher sind Touchscreens oft limitiert, was Entwickler bei der App-Entwicklung berücksichtigen sollten. Das Multitouch-API mit seinen MotionEvents und Pointern sorgt dafür, dass Android ab 2.2 wahrlich kein Rührmichnichtan ist.

Kommentiere den Artikel

Please enter your comment!
Please enter your name here