Stand: 03.07.2018
In der Welt der Computer ist mit "Interaktion" gemeint, dass der Computer auf den Benutzer reagiert. Am offensichtlichsten ist ein Mausklick oder ein Touch auf einem Button. Dann gibt es weitere UI-Komponenten, mit denen man interagieren kann wie Slider, Tabs (Reiter), Listen, Menüs etc. Auf Touchoberflächen gibt es "Gesten" wie den Swipe (horizontales Streichen) oder etwas vom Bildschirmrand runter- oder raufziehen. Schließlich gibt es noch neuere Formen der Interaktion wie Schütteln oder Gesichtserkennung.
4.1 Event-Handler/Listener
Wir beschäftigen uns hier mit Interaktion über UI-Komponenten. Das Grundproblem ist:
Wenn das Ereignis (event) "Button angeklickt" eintritt,
führe ein bestimmtes Stück Code aus.
Man nennt diesen Mechanismus auch Event-Handling. Der Code, der ausgeführt werden soll, ist i.d.R. eine Funktion, die man auch Event-Handler nennt. In Java wird beim Event-Handling oft der Begriff Listener verwendet, weil der entsprechende Code nach dem Event "lauscht".
Dieser Mechanismus wird innerhalb der betreffenden Activity eingebettet. Es gibt drei verschiedene Möglichkeiten, das zu realisieren:
- eine eigene (innere) Klasse
- innerhalb der Activity über ein Interface
- eine anonyme innere Klasse
Prinzipiell wird das Event-Handling über Interfaces realisiert. Wie funktioniert das? Aus Sicht des Buttons ist es nur wichtig, dass eine bestimmte Methode namens onClick
existiert, die immer dann ausgeführt wird, wenn der Button gedrückt wird. Es kann sich ein beliebiges Objekt, das diese Methode hat, bei dem Button "anmelden" (über die Button-Methode setOnClickListener
). In der Softwaretechnik nennt man diese Technik auch das Observer-Pattern.
4.1.1 Interfaces
Um die drei Mechanismen des Event-Handlings zu verstehen, müssen Sie das Konzept Interface kennen. Es handelt sich um ein wichtiges Konstrukt objektorientierter Sprachen wie Java.
Sie kennen das Konzept einer abstrakten Klasse und abstrakter Methoden. Eine abstrakte Klasse kann nicht instanziiert werden. Abstrakte Methoden wiederum sind "Versprechungen", dass alle Unterklassen diese Methoden überschreiben.
Ein Interface ist wie eine noch konsequentere abstrakte Klasse. Ein Interface enthält keinen Code, sondern nur eine Reihe von "Versprechungen", also abstrakten Methoden. Ein Interface ist so etwas wie ein Vertrag. Beispielsweise könnten wir ein Interface MyClickInterface
schreiben. Das sähe so aus:
public interface MyClickInterface {
void onClick(View v);
}
Dieses Interface verlangt nur, dass alle Klassen, die von sich behaupten, dieses Interface zu realisieren, eine Methode onClick
mit einem Parameter vom Typ View haben.
Eine Klasse Foo
, die verspricht, sich an dieses Interface zu halten, muss dann wie folgt im Header aussehen:
public class Foo implements MyClickInterface {
...
}
Der Unterschied zu einer abstrakten Klasse ist also auf den ersten Blick sehr gering. Man hat hier "implements" statt "extends". Der Vorteil von Interfaces ist aber, dass z.B. die Klasse Foo zusätzlich auch eine Elternklasse haben kann:
public class Foo extends DaddyFoo implements MyClickInterface {
...
}
Außerdem kann eine Klasse mehrere Interfaces gleichzeitig realisieren. Das Interface Runnable
fordert z.B. dass es eine Methode run
gibt. Auch das kann unsere Klasse Foo leisten und realisiert somit sowohl Runnable
als auch MyClickListener
:
public class Foo extends DaddyFoo implements Runnable, MyClickInterface {
...
}
Haben Interfaces auch Nachteile? In der Tat: im Gegensatz zu abstrakten Klassen dürfen Interfaces keinen Code enthalten, d.h. ein Interface kann keine Funktionalität vererben, sondern ist eine reine Vereinbarung.
Bei Android wird uns am meisten das (existierende) Interface View.OnClickListener
interessieren. Dort wird tatsächlich auch nur gefordert, dass eine Methode onClick
mit einem Parameter vom Typ View
vorhanden ist.
4.1.2 Textanzeige mit Toasts [optional]
Bevor wir die verschiedenen Event-Handling-Methoden kennen lernen, möchten wir "Toasts" vorstellen, weil man damit gut testen kann, ob eine Interaktion tatsächlich funktioniert.
Ein "Toast" ist eine Textnachricht, die für kurze Zeit im unteren Bereich der Screen angezeigt wird. So etwas kann man z.B. verwendent, um anzuzeigen, dass etwas abgeschickt wurde oder das ein Hintergrund-Vorgang (Speichern, Download etc.) abgeschlossen wurde.
Toast ist eine Klasse. Die statische Methode makeText() liefert ein Toast-Objekt zurück, das mit der Methode show() angezeigt werden kann. Hier ein Beispiel:
Toast toast = Toast.makeText(view.getContext(), "Hallo, Welt",
Toast.LENGTH_SHORT);
toast.show();
Wir werden als Reaktion auf einen Button-Klick je einen Toast zeigen.
4.2 Eigene (innere) Klasse
Wir nehmen an, dass wir eine schlichte Oberfläche mit drei Buttons haben, die jeweils button1, button2, button3 heißen.
Hier schreibt man eine eigene Klasse, die das Interface View.OnClickListener
realisiert. In der Regel definiert man diese Klasse als innere Klasse in der Activity.
public class MainActivity extends AppCompatActivity {
// Innere Klasse:
class MyListener implements View.OnClickListener {
@Override
public void onClick(View view) {
// Code für das Event-Handling
}
}
// restlicher Code der Activity, z.B. onCreate()
}
Die Schreibweise View.OnClickListener
zeigt an, dass das Interface OnClickListener
ein inneres Interface von View
ist. Das braucht uns aber nicht weiter zu stören.
Im "Hauptcode", also in der Methode onCreate()
erzeugt man ein Objekt, das man bei allen drei Buttons anmeldet:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button b1 = (Button)findViewById(R.id.button1);
Button b2 = (Button)findViewById(R.id.button2);
Button b3 = (Button)findViewById(R.id.button3);
MyListener listener = new MyListener();
b1.setOnClickListener(listener);
b2.setOnClickListener(listener);
b3.setOnClickListener(listener);
}
Wir können jetzt eine kurz einzublendende Nachricht, einen sogenannten Toast, anstoßen. Zu beachten ist bei unserer Technik, dass wir einen Code für alle drei Buttons haben. Wir müssen also im Code unterscheiden, welcher Button gedrückt wurde.
4.2.1 Text vom Button auslesen und ändern
Sie fragen sich vielleicht, was genau in dem Parameter von onClick
drinsteckt? Da wird praktischerweise die Komponente mitgeliefert, die den Klick ausgelöst hat, d.h. in unserem Fall der Button.
Wir können jetzt die Button-Methode getText
verwenden, um uns die Beschriftung des Buttons zu holen! Sie müssen allerdings vorher Java garantieren, dass in der Variable view
wirklich ein Button drinsteckt - das machen Sie durch Casting.
((Button)view).getText();
Hinweis: Sie benötigen hier zusätzlich toString
, weil getText
keinen String, sondern eine CharSequence zurückgibt, eine verwandte Klasse zu String, die sich mit dem Befehl toString leicht umwandeln lässt. Diese Umwandlung wird in vielen Kontexten automatisch vollzogen, so dass Sie eben nur manchmal das toString benötigen:
((Button)view).getText().toString();
Im Code verwenden Sie den Text direkt im Zusammenhang mit einem Toast:
class MyListener implements View.OnClickListener {
@Override
public void onClick(View view) {
String beschriftung = ((Button)view).getText().toString();
Toast toast = Toast.makeText(view.getContext(), "Das war " + beschriftung,
Toast.LENGTH_SHORT);
toast.show();
}
}
Sie könnten auch den Text des Buttons verändern, wenn er gedrückt wird, statt nur einen Toast auszugeben. Dazu verwenden Sie die Button-Methode setText
. Auch hier müssen Sie Casting verwenden:
class MyListener implements View.OnClickListener {
@Override
public void onClick(View view) {
((Button)view).setText("Gedrückt!");
}
}
4.2.2 Klassendiagramm
Im Klassendiagramm kann man das so darstellen:
Man sieht eine klare Aufgabenteilung: in MainActivity befindet sich Code zum Ablauf der Activity, in MyListener befindet sich der Code für die Reaktion auf die Buttons.
4.2.3 Pros und Cons
Diese Technik, eine eigene Klasse anzulegen, ist für Anfänger die am besten verständliche Option, weil es technisch relativ klar ist, wie der Gesamtmechanismus funktioniert. Störend ist, dass viel Code geschrieben werden muss, der die Activity-Klasse schnell unübersichtlich macht.
4.3 Activity als Listener
Was im obigen Ansatz etwas stört, ist die Tatsache, dass man für relativ wenig Funktionalität eine Klasse anlegt. Kann man das bisschen Code nicht in eine Klasse packen, die eh schon da ist?
In der Tat: das geht. Wir nehmen jetzt eine existierende Klasse - bei uns MainActivity - und versprechen, dass diese das Interface View.OnClickListener
erfüllt:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
// Code
}
Wir müssen dazu nur die Methode onClick
korrekt implementieren.
4.3.1 Klassendiagramm
Im Klassendiagramm sieht man im Vergleich zur oberen Variante, dass MainActivity die Aufgabe des Listeners mit übernimmt:
4.3.2 Umsetzung
Wir müssen also die Methode onClick()
implementieren. Wir benutzen - wie schon im ersten Ansatz - die Toast-Kurznachricht, um den gedrückten Button anzuzeigen:
public void onClick(View view) {
Toast toast = Toast.makeText(view.getContext(), "Das war " + ((Button)view).getText(),
Toast.LENGTH_SHORT);
toast.show();
}
Was noch fehlt: die Buttons müssen wissen, dass ab sofort das MainActivity-Objekt ihr Event-Handler ist. Das teilt man ihnen mit der Methode setOnClickListener
mit. Das MainActivity-Objekt bekommt man mit this
. Das regeln wir am besten direkt in onCreate()
. Hier gleich der komplette Code der Klasse:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button b1 = (Button)findViewById(R.id.button1);
Button b2 = (Button)findViewById(R.id.button2);
Button b3 = (Button)findViewById(R.id.button3);
b1.setOnClickListener(this);
b2.setOnClickListener(this);
b3.setOnClickListener(this);
}
@Override
public void onClick(View view) {
Toast toast = Toast.makeText(view.getContext(), "Das war " + ((Button)view).getText(),
Toast.LENGTH_SHORT);
toast.show();
}
}
4.3.3 Andere Komponenten modifizieren
Wenn Sie nach einem Buttonklick auf eine andere Komponente zugreifen wollen, z.B. auf ein Textfeld, müssen Sie diese Komponente irgendwie zugänglich machen. Das erreichen Sie z.B. durch eine Instanzvariable.
Nehmen wir an, Sie haben ein Textfeld (TextView), in das Sie "Foo!" schreiben wollen, sobald ein Button gedrückt wurde. Dann müssen Sie die Komponente als Instanzvariable anlegen:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private TextView textView; // [1] Instanzvariable anlegen
@Override
protected void onCreate(Bundle savedInstanceState) {
...
Sie müssen jetzt das entsprechende Objekt in diese Variable füllen. Das können Sie erst in onCreate machen. Wir nehmen an, dass Ihre Komponente den ID "mytextview" hat:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView)findViewById(R.id.mytextview); // [2] Objekt zuweisen
...
Jetzt können Sie auch im Code von onClick
auf diese Komponente zugreifen, das die Instanzvariable ja überall in der Klasse sichtbar ist:
@Override
public void onClick(View view) {
textView.setText("Foo!"); // [3] Text setzen
}
4.3.4 Pros und Cons
Der Vorteil der Methode ist, dass der Code kompakter wird. Der Nachteil ist, dass die Zusammenlegung der Klasse MainActivity mit dem Listener nicht intuitiv ist, da keine klare Aufgabenteilung vorliegt. Nichtsdestotrotz kommt diese Vorgehensweise in der Praxis sehr häufig vor.
4.4 Anonyme innere Klasse
Jetzt betrachten wir nochmal die erste Methode: wir schreiben eine eigene Klasse - z.B. MyListener
- und brauchen aber nur drei Instanzen, häufig sogar nur eine einzige. Jetzt gibt es die Möglichkeit, eine anonyme Klasse herzustellen, die noch nicht mal einen Namen bekommt, weil Sie nur genau einmal instanziiert wird.
4.4.1 Klassendiagramm
Das Klassendiagramm ist identisch mit dem der ersten Methode (mit eigener Klasse MyListener), nur dass die neue Klasse anonym ist. In der Realisierung ist der Unterschied allerdings größer, da wir hier für jeden Button eine neue anonyme Klasse verwenden.
4.4.2 Umsetzung
Jetzt zum Code. Schauen wir uns einen Button an:
Button b1 = (Button)findViewById(R.id.button1);
An dieser Stelle brauchen wir das Listener-Objekt:
b1.setOnClickListener( ... );
Was, wenn wir an dieser Stelle eine neue Klasse definieren könnten, diese Klasse instanziieren und das resultierende Objekt hier in die Methode stecken könnten?
Das geht tatsächlich:
b1.setOnClickListener(new View.OnClickListener() {
// ...
});
Hier wird eine neue, namenlose Klasse definiert, die dem Interface View.OnClickListener
gehorchen muss. Die Klasse wird direkt einmal instanziiert (deshalb das new
) und das Objekt wird der Methode gegeben.
Was muss in der Klasse stehen? Gemäß des Interfaces die Methode onClick
:
b1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast toast = Toast.makeText(view.getContext(), "Das war " + ((Button)view).getText(),
Toast.LENGTH_SHORT);
toast.show();
}
});
Wir schauen uns wieder den Gesamtcode an:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button b1 = (Button)findViewById(R.id.button1);
Button b2 = (Button)findViewById(R.id.button2);
Button b3 = (Button)findViewById(R.id.button3);
b1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast toast = Toast.makeText(view.getContext(), "Das war " + ((Button)view).getText(),
Toast.LENGTH_SHORT);
toast.show();
}
});
b2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast toast = Toast.makeText(view.getContext(), "Das war " + ((Button)view).getText(),
Toast.LENGTH_SHORT);
toast.show();
}
});
b3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast toast = Toast.makeText(view.getContext(), "Das war " + ((Button)view).getText(),
Toast.LENGTH_SHORT);
toast.show();
}
});
}
}
4.4.3 Pros und Cons
Der Vorteil dieser Methode ist, dass sehr klar wird, welche Funktionalität zu welchem Button gehört und dass eine klare Aufgabenteilung vorliegt. Durch die vielen anonymen Klassen kann der Code unübersichtlich werden und es droht die Gefahr der Code-Duplizierung. Auch hier im Beispiel haben wir drei mal die gleichen zwei Code-Zeilen. Konsequent wäre es, diesen Code in eine private Methode auszulagern.
Ich persönlich würde diese dritte Methode empfehlen.
4.5 Übungen
(A) Drei Buttons
Erstellen Sie eine App mit drei Buttons. Bei Druck auf einen Button soll jeweils "Button 1" oder "Button 2" oder "Button 3" als Toast erscheinen.
Erstellen Sie die App in drei Varianten (jeweils als eigenes Projekt):
- Handler als eigene innere Klasse
- Activity ist Handler
- Handler als anonyme, innere Klasse
(B) Taschenrechner
Schreiben Sie eine einfache Taschenrechner-App für Addition und Subtraktion ganzer Zahlen. Verwenden Sie für die GUI verschachtelte LinearLayout-Objekte. Die benötigen lediglich einen TextView und viele Buttons als sichtbare Komponenten:
Wenn Sie auf die Ziffern-Buttons drücken, wird die Ziffer rechts an die sichtbare Zahl angehängt. Nutzen Sie die Methoden getText
und setText
der jeweiligen Objekte (z.B. Button). Schauen Sie sich auch den Abschnitt "Andere Komponenten modifizieren" oben an, um auf den TextView zugreifen zu können.
Denken Sie daran, dass Sie bei der Methode onClick
ein View-Objekt übergeben bekommen. Wenn Sie wissen, dass dieses View-Objekt ein Button ist, müssen Sie casten, um eine Button-Methode verwenden zu dürfen, also z.B.
String beschriftung = ((Button)v).getText().toString();
für den Fall, dass der Parameter v
heißt.
Um den Mechanismus möglichst einfach zu halten, wird folgendes Verhalten vorgeschlagen:
- Zu Beginn zeigt das Display eine "0"
- Wird eine erste Ziffer gedrückt, erscheint die Ziffer. Jede weitere Ziffer wird rechts angehängt.
- Wird ein Operator (+/-) gedrückt, erscheint wieder eine "0"
- Die zweite Zahl wird mit Zifferentasten wie oben eingegeben
- Ein Druck auf "=" zeigt das Ergebnis, ein Druck auf "CE" löscht die aktuelle Zahl und den eingegebenen Operator
Hinweis: Sie müssen Strings in Zahlen (int) umwandeln. Dazu verwenden Sie die (statische) Methode Integer.parseInt
, die einen String bekommt und eine ganze Zahl zurückgibt.
Tipp: Für die Ziffern empfiehlt sich die Methode "Activity als Listener". Für die anderen Buttons (Plus, Minus, Clear, Equals) nimmt man vielleicht eher die anonyme innere Klasse.