Stand: 08.09.2018

12.1 Event-Handling mit Lambda-Ausdrücken

Wenn wir über einen Button mit einer App interagieren wollen, müssen wir technisch gesehen "ein Stück Code" an einen Button "hängen". Das Grundproblem ist aber, dass man in Java nicht einfach Funktionen oder Methoden hin- und herreichen kann. Man kann höchstens Objekte weiterreichen. Also haben wir bislang mit neuen Klassen und anonymen Klassen gearbeitet, die jeweils nur eine Methode mit besagtem Code hatten.

12.1.1 Voraussetzungen

Seit Java 8 gibt es eine elegantere Art: man verwendet sogenannte Lambda-Ausdrücke. Ein Lambda-Ausdruck ist eine Funktion, die nur aus Code besteht, ohne dass sie einen Namen hat.

Android hinkt den Entwicklung der Sprache "Java" etwas hinterher. Daher müssen wir in Android Studio explizit den Sprachstandard hochsetzen. Dazu gehen Sie in das Menü File > Project Structure.

Dort müssen Sie den Bereich "app" anklicken und dort die zwei Eigenschaften "Source Compatibility" und "Target Compatibility" auf 1.8 setzen.

In Android Studio Sprachstandard hochsetzen

Es kann sein, dass Sie nach dieser Umstellung noch ein Build > Rebuild Project durchführen müssen, damit alles wieder rund läuft.

12.1.2 Lambda-Ausdruck

Nehmen wir eine Funktion, die einen String bekommt und diesen ausgibt:

public void printString(String x) {
  System.out.println(x);
}

Als Lambda-Ausdruck würde man lediglich schreiben:

(String x) -> {
  System.out.println(x);
}

Ein Lambda-Ausdruck hat drei Teile:

  1. Parameterliste
  2. Pfeil
  3. Code-Block

In Situationen, wo Java den Typ der Parameter erschließen kann (wie es bei GUIs der Fall sein wird), kann man sogar den Typ bzw. die Typen weglassen:

x -> {
  System.out.println(x);
}

12.1.3 Bindung

Sie möchten, dass ein Stück Code ausgeführt wird, wenn z.B. Ihr Button angeklickt wird. Der Klick wird auch als "Ereignis" (engl. event) aufgefasst und zu jedem Ereignis gibt der Button einige Informationen mit, die in einem Objekt vom Typ View gespeichert sind.

Zur Erinnerung sei hier nochmal gezeigt, wie man den Event-Handler mit Hilfe einer anonymen inneren Klasse definiert:

Button b1 = (Button)findViewById(R.id.button);

b1.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
        Toast.makeText(view.getContext(), "button ONE clicked", Toast.LENGTH_SHORT).show();
    }
});

Sie können jetzt einen Lambda-Ausdruck schreiben, den Sie dem Button mitgeben. Der Parameter ist vom Typ View , allerdings kann Java das erschließen. Daher schreiben Sie einfach "v ->".

Button b2 = (Button)findViewById(R.id.button2);

b2.setOnClickListener(v -> Toast.makeText(v.getContext(),
                                          "button TWO clicked",
                                          Toast.LENGTH_SHORT).show());

Jetzt erscheint auf Ihrer Konsole "OK", sobald Sie auf den OK-Button drücken. Sie können in Ihrem Lambda-Ausdruck natürlich auch auf andere GUI-Komponenten zugreifen.

12.1.4 Alternative Schreibweisen

Die oben gezeigte Schreibweise mit einem Lambda-Ausdruck ist die derzeit eleganteste und kürzeste Form. Ich zeige hier kurz die "alten" Schreibweisen, um Events von GUI-Komponenten zu handhaben.

Alternativ können Sie auch den Typ des Parameters e mit angeben:

b2.setOnClickListener((View v) -> Toast.makeText(v.getContext(),
                                      "button TWO clicked",
                                      Toast.LENGTH_SHORT).show());

Ursprünglich wurde eine anonyme Klasse benutzt. Eine anonyme Klasse stellt eine einzige Instanz einer neuen Klasse her, wobei diese neue Klasse gar keinen Namen bekommt.

b1.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
        Toast.makeText(view.getContext(), "button ONE clicked", Toast.LENGTH_SHORT).show();
    }
});

Dies war wiederum eine Erleichterung gegenüber der Notwendigkeit, eine eigene Klasse anzulegen, die das Interface EventHandler implementiert (das Konzept der Interfaces haben wir noch nicht durchgenommen, ist so etwas ähnliches wie eine abstrakte Klasse):

class MyHandler implements EventHandler<ActionEvent> {
  public void handle(ActionEvent event) {
    System.out.println("OK");
  }
}

Erst nachdem man diese Klasse geschrieben hatte, konnte man ein Handler-Objekt anlegen und dem Button mitgeben:

MyHandler handler = new MyHandler();
bOk.setOnAction(handler);

Mittlerweile hat man erkannt, dass man hier nur eine Funktion übergeben muss und dazu nicht eine ganze Klasse (anonym oder nicht) benötigt - man braucht noch nicht mal einen Namen für diese Funktion. Daher sind die in Java 8 eingeführten Lambda-Ausdrücke ideal für GUI-Handler.

12.2 Interaktion per Multitouch

Bislang haben wir einen Listener benutzt, der auf Clicks horcht. Damit ist ein beliebiger Touch-Input gemeint. Um echte Multitouch-Interaktion zu programmieren, muss man die Finger differenzieren können. Damit ist folgendes gemeint: Wenn zwei Finger f1 und f2 die Oberfläche berühren und sich darauf bewegen, dann werden entsprechende Informationen über die Position kommuniziert. Damit man zwischen f1 und f2 unterscheiden kann, muss jedem Finger ein ID gegeben worden sein (z.B. eine ganz Zahl).

Zunächst mal unterscheiden wir zwischen drei möglichen "Events":

12.2.1 Touch-Listener

Wie auch bei einem Button hängen wir ein Listener-Objekt an unsere Schaltfläche. Das ist oft die gesamte Screen. Also geben wir unserem "obersten" Layout (i.d.R. ein ConstraintLayout) erstmal eine ID:

<android.support.constraint.ConstraintLayout
    ...
    android:id="@+id/viewMain"
    ...

Jetzt können wir in der Activity einen Listener vom Typ View.OnTouchListener anhängen. In dem Listener überschreiben wir die Methode onTouch. Wir verwenden hier also eine anonyme, innere Klasse:

public class MainActivity extends AppCompatActivity {

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ConstraintLayout view = (ConstraintLayout)findViewById(R.id.viewMain);

        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                ...
            }
        });
    }
}

In onTouch bekommen wir ein Objekt vom Typ MotionEvent. Dort stehen alle wichtigen Infos zum Event drin. Unter anderem, um welchen der drei Typen von Event es sich handelt (DOWN, MOVE, UP), aber auch die Koordinaten des Fingers auf der Schaltfläche.

12.2.2 Touch-Infos zeigen

Nehmen wir an, wir erzeugen drei Textfelder, die die Koordinaten unserer Touch-Aktionen anzeigen sollen. Erstellen Sie ein entsprechendes Layout mit drei TextView-Komponenten mit IDs textDown, textMove und textUp.

In MainActivity schreiben Sie den Listener:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ConstraintLayout view = (ConstraintLayout)findViewById(R.id.viewMain);
        final TextView textDown = (TextView)findViewById(R.id.textDown);
        final TextView textMove = (TextView)findViewById(R.id.textMove);
        final TextView textUp = (TextView)findViewById(R.id.textUp);

        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int action = motionEvent.getAction();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        textDown.setText("Down: " + (int)motionEvent.getX()
                        + ", " + (int)motionEvent.getY());
                        return true;
                    case MotionEvent.ACTION_MOVE:
                        textMove.setText("Move: " + (int)motionEvent.getX()
                        + ", " + (int)motionEvent.getY());
                        return true;
                    case MotionEvent.ACTION_UP:
                        textUp.setText("Up: " + (int)motionEvent.getX()
                        + ", " + (int)motionEvent.getY());
                        return true;
                }
                return false;
            }
        });
    }
}

12.2.3 Multitouch visualisieren

In einem zweiten Beispiel wollen wir unser Wissen aus dem Modul Grafik nutzen, um die Touch-Punkte grafisch anzuzeigen.

Multitouch visualisiert

Wir schauen uns zuerst den groben Rahmen an. Wir erstellen eine eigene View-Klasse und setzen eine Instanz dieser Klasse in onCreate als Haupt-View ein (setContentView).

Wir legen auch schon einen Touch-Listener an.

public class MainActivity extends AppCompatActivity {

    class DrawingView extends View {
        public DrawingView(Context context) {
            super(context);
            ...
        }

        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            ...
        }
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        DrawingView view = new DrawingView(this);
        setContentView(view);
        ...

        view.setOnTouchListener((v, e) -> {
            ...
        });
    }
}

Die Grundidee ist, dass wir uns das MotionEvent-Objekt, das wir im Touch-Listener bekommen, in einer Instanzvariablen merken, die wir wiederum beim Zeichnen auslesen können.

API MotionEvent →

Wir legen also die Instanzvariable an:

private MotionEvent motionEvent;

Anschließend setzen wir die Variable im Listener:

view.setOnTouchListener((v, e) -> {
    motionEvent = e;
    view.invalidate();
    return true;
});

Wichtig ist, dass der Listener auch den View mit invalidate zum Update zwingt.

Im Code zum Zeichnen können wir jetzt das Objekt auslesen. Hier ist zu beachten, dass die Touch-Punkte in irgendeiner Reihenfolge abgelegt sind. Der ID bleibt allerdings immer gleich. Das heißt, dass der Laufindex (hier: i) nicht gleichzeitig der ID ist. Den ID holt man sich mit getPointerId, ebenso die Koordinaten (getX und getY).

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    if (motionEvent != null && motionEvent.getAction() != MotionEvent.ACTION_UP) {
        for (int i = 0; i < motionEvent.getPointerCount(); i++) {
            float x = motionEvent.getX(i);
            float y = motionEvent.getY(i);
            int id = motionEvent.getPointerId(i);
            canvas.drawCircle(x, y, CURSOR_RADIUS, paint);
            canvas.drawText("" + id,
                    x, y - CURSOR_RADIUS - 30, paintText);
        }
    }
}

Hier nochmal der gesamte Code mit einigen Formatierungen:

public class MainActivity extends AppCompatActivity {

    private final static int CURSOR_RADIUS = 150;
    private MotionEvent motionEvent;
    private Paint paint = new Paint();
    private Paint paintText = new Paint();
    private Paint paintTextBig = new Paint();

    class DrawingView extends View {
        public DrawingView(Context context) {
            super(context);
            paint.setColor(Color.RED);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeWidth(5);
            paintText.setColor(Color.RED);
            paintText.setTextSize(70);
            paintTextBig.setColor(Color.rgb(200,200,200));
            paintTextBig.setTextSize(200);
            paintTextBig.setTextAlign(Paint.Align.CENTER);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.drawText("Touch", canvas.getWidth()/2,
                    canvas.getHeight()/2 + 35, paintTextBig);
            if (motionEvent != null && motionEvent.getAction() != MotionEvent.ACTION_UP) {
                for (int i = 0; i < motionEvent.getPointerCount(); i++) {
                    float x = motionEvent.getX(i);
                    float y = motionEvent.getY(i);
                    int id = motionEvent.getPointerId(i);
                    canvas.drawCircle(x, y, CURSOR_RADIUS, paint);
                    canvas.drawText("" + id,
                            x, y - CURSOR_RADIUS - 30, paintText);
                }
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        DrawingView view = new DrawingView(this);
        setContentView(view);
        view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                // Set the content to appear under the system bars so that the
                // content doesn't resize when the system bars hide and show.
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                // Hide the nav bar and status bar
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_FULLSCREEN);

        view.setOnTouchListener((v, e) -> {
            motionEvent = e;
            view.invalidate();
            return true;
        });
    }
}

Der Bildschirm wurde über setSystemUiVisibility auf Fullscreen-Modus geschaltet (siehe auch den Artikel Enable fullscreen mode).

12.3 Sensoren

Es stehen verschiedene Sensoren zur Verfügung. Welche genau, hängt vom Endgerät ab. Die am häufigsten benutzten Sensoren sind:

Um einen Sensor zu verwenden, müssen Sie folgende Schritte befolgen:

  1. den SensorManager beziehen
  2. vom SensorManager den gewünschten Sensor (z.B. Beschleunigungssensor) beziehen
  3. sich beim Sensor-Manager als Listener registrieren

Wir schauen uns das ganze für einen Beschleunigungssensor (engl. accelerometer) an.

12.3.1 SensorManager beziehen

Der SensorManager ist ein Objekt, den man über die Activity-Methode getSystemService bezieht und am besten gleich als Instanzvariable speichert (wir sehen später warum):

public class MainActivity extends AppCompatActivity {

    private SensorManager sensorManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
    }
}

12.3.2 Sensor beziehen

Jetzt können Sie vom SensorManager den eigentlichen Sensor beziehen, den Sie am besten auch als Instanzvariable ablegen. Theoretisch kann es mehrere Sensoren eines Typs geben, daher fragen Sie nach dem "Default-Sensor" des Typs "Accelerometer":

public class MainActivity extends AppCompatActivity {

    private Sensor accelerometer;
    private SensorManager sensorManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
        accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
    }

12.3.3 Listener und Sensordaten

Nun ist es so, dass die Daten von allen Sensoren von einem Listenerobjekt abgefangen werden. Darum bietet es sich an, die ganze Activity-Klasse als Listener zu deklarieren. Dazu müssen wir das Interface SensorEventListener realisieren:

public class MainActivity extends AppCompatActivity implements SensorEventListener {
  ...
}

Dazu müssen wir zwei Methoden schreiben: onSensorChanged und onAccuracyChanged. Die interessante Methode ist die erste. Die zweite können wir leer lassen.

@Override
public void onSensorChanged(SensorEvent sensorEvent) {
    if (sensorEvent.sensor == accelerometer) {
        float[] values = sensorEvent.values.clone();
        Log.i("Sensor-App", "x:" + values[0] + " y:" + values[1] + " z:" + values[2]);
    }
}

@Override
public void onAccuracyChanged(Sensor sensor, int i) {
}

Da unser Listener ja auf alle Sensoren horcht, müssen wir mit einem if erst schauen, ob die Daten wirklich vom Beschleunigungssensor kommen. Anschließend bekommen wir aus dem Event-Objekt einen Array von Float-Werten. Diese Werte bedeuten im Fall eines Beschleunigungssensors die x, y, z-Werte der Beschleunigung.

Beschleunigungssensor: die drei Achsen

Die Beschleunigung wird in m/s^2 gemessen. Wenn das Handy auf dem Tisch liegt, hat man eine z-Beschleunigung von 9.81 (Gravitation) und jeweils 0 für x- und y-Beschleunigung. Das sind natürlich nur theoretische Idealwerte, in der Realität hat man immer kleine Schwankungen.

12.3.4 Listener registrieren

Jetzt müssen wir noch dem SensorManager Bescheid sagen, dass unsere Activity der Listener für alle Sensoren sein soll. Wir machen das in der Methode onResume. Wir melden uns auch in jeder Pause wieder ab, also in der Methode onPause.

@Override
protected void onResume() {
    super.onResume();
    if (accelerometer != null) {
        sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);
    }
}

@Override
protected void onPause() {
    super.onPause();
    sensorManager.unregisterListener(this);
}

Jetzt sollten Sie Ihr Programm testen können, am besten mit einem echten Handy. Am besten zeigen Sie die x,y,z-Werte in einem TextView auf der Screen des Smartphones an.

12.4 Übungen

(A) Step Counter App

Android verfügt über einen eigenen Schritt-Erkenner (STEP DETECTOR), den viele Fitness- und sonstige Gesundheits-Apps verwenden. Schreiben Sie eine einfache App, die nur eine große Zahl anzeigt und diese Zahl jedesmal hochzählt, wenn ein Schritt gemeldet wurde.

Folgen Sie dazu den obigen Anweisungen zur Einrichtung eines Sensors. Anstelle der Variablen 'accelerometer' definieren Sie die Variable stepSensor als Instanzvariable:

private Sensor stepSensor;

In onCreate beziehen Sie den Sensor:

stepSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);

Schreiben Sie entsprechende Listener, um die Schritte zu zählen.

Im nächsten Schritt können Sie überlegen, wie Sie verhindern, dass der Zähler jedesmal zurückgesetzt wird, wenn die Orientierung wechselt (z.B. mit einem Singleton, siehe Modul Daten I).

(B) Beschleunigungssensor

Schreiben Sie ein Programm, das zunächst testet, ob ein Beschleunigungssensor vorhanden ist und anschließend die x, y, z-Werte auf dem Bildschirm anzeigt. Für die Anzeige, ob der Sensor vorhanden ist, wurde hier ein Switch verwendet.

Übung Beschleunigungssensor