2.2 Benutzeroberfläche 

Die Benutzeroberfläche ist das Aushängeschild einer Anwendung. Gerade auf mobilen Geräten sollte jede Funktion leicht zugänglich und intuitiv erfassbar sein. Android unterstützt Sie bei der Gestaltung durch eine große Auswahl an Bedienelementen.
2.2.1 Grafiken 

Neben Smartphones, deren Bildschirmdiagonalen üblicherweise zwischen 5 und 7 Zoll groß sind, gibt es Tablets, deren Displays bis zu 13 Zoll betragen. Auch die Zahl der horizontal und vertikal darstellbaren Pixel variiert drastisch. Um den durch diese Vielfalt entstehenden Aufwand für Entwickler in Grenzen zu halten, definiert Android einige Klassen für Bildschirmgrößen und Pixeldichten. Fordert eine App zur Laufzeit eine Grafik an, sucht das System die am besten zur Hardware passende aus. Wie das funktioniert, erkläre ich Ihnen in diesem Abschnitt. Zuvor möchte ich aber noch ein paar Begriffe erklären:
-
Die Bildschirmgröße wird üblicherweise in Zoll angegeben. Sie beschreibt den Abstand von der linken unteren zur rechten oberen Ecke der Anzeige.
-
Das Seitenverhältnis (engl. Aspect Ratio) entspricht dem Quotienten aus physikalischer Breite und Höhe. Android kennt in diesem Zusammenhang die beiden Resource-Bezeichner long und notLong. Ob ein Bildschirm lang oder nicht lang ist, hat übrigens nichts mit dessen Ausrichtung zu tun. WVGA (800 × 480 Pixel) und FWVGA (854 × 480 Pixel) sind lang, VGA (640 × 480 Pixel) hingegen nicht. Deshalb bleibt das Seitenverhältnis zur Laufzeit auch stets gleich.
-
Die Auflösung gibt die Zahl der horizontal und vertikal ansprechbaren physikalischen Pixel an.
-
Die Pixeldichte schließlich wird in Punkten pro Zoll angegeben und ist letztlich ein Maß für die Größe eines Pixels. Sie errechnet sich aus der Bildschirmgröße und der physikalischen Auflösung.
Wie Sie bereits wissen, legt Android Studio beim Erzeugen eines Projekts das App-Icon in Verzeichnissen ab, die alle mit mipmap beginnen. Konkret heißen sie mipmap-mdpi, mipmap-hdpi, mipmap-xhdpi, mipmap-xxhdpi und mipmap-xxxhdpi und enthalten das Programm-Symbol als Rastergrafik in unterschiedlichen Pixeldichten. Zur Laufzeit der App lädt die Plattform je nach Bildschirmkonfiguration die am besten geeignete Datei aus dem Verzeichnis für mittlere (-mdpi), hohe (-hdpi), sehr hohe (-xhdpi), sehr, sehr hohe (-xxhdpi) oder sehr, sehr, sehr hohe (-xxxhdpi) Dichte. Das x steht übrigens für das englische »extra«. Hat die Plattform einen API-Level von 26 oder höher, wird (sofern vorhanden) stattdessen die Vektorgrafik im Verzeichnis mipmap-anydpi-v26 verwendet. Die Dateinamen der Grafiken ohne Erweiterung sind stets gleich, zum Beispiel ic_launcher.png und ic_launcher_round.png für die Bitmap-App-Icons, die der Projektassistent erstellt hat.
Wenn Sie an anderer Stelle Rastergrafiken anzeigen möchten, sollten auch diese in unterschiedlichen Pixeldichten vorliegen. Damit Android sie findet, müssen die Dateien in Verzeichnissen liegen, die mit drawable beginnen und mit einem der genannten Suffixe (-mdpi, -xxhdpi ...) enden. Bitte achten Sie darauf, dass der Dateiname stets gleich ist.
Bildschirme mit niedriger Dichte stellen etwa 120 Punkte pro Zoll (engl. dots per inch – dpi) dar. Bei mittlerer Dichte sind dies ungefähr 160 dpi, was übrigens den beiden ersten Android-Geräten G1 und Magic entspricht (und deshalb als Basislinie für die Umrechnung gilt). Smartphones oder Tablets mit hoher Pixeldichte lösen ca. 240 dpi auf, bei sehr hoher Dichte sind es 320 dpi. -xxhdpi und -xxxhdpi entsprechen etwa 480 bzw. 640 dpi.
Vielleicht fragen Sie sich, warum Android diesen Aufwand treibt. Aus Sicht des Anwenders soll die Pixeldichte keine Auswirkung auf die Größe der Benutzeroberfläche haben. Genau das ist bei einer Bitmap aber der Fall. Mit zunehmender Pixeldichte wirkt sie immer kleiner. Um das zu kompensieren, muss sie entweder zur Laufzeit skaliert werden (was zu Qualitätseinbußen führen kann) oder schon in der richtigen Größe vorliegen. Die folgende Tabelle zeigt, welchen Einfluss die Pixeldichte auf Breite und Höhe einer Bitmap hat.
Größe in Pixel |
Umrechnungsfaktor |
Pixeldichte |
---|---|---|
36 × 36 |
0,75 |
ldpi |
48 × 48 |
1,0 |
mdpi |
72 × 72 |
1,5 |
hdpi |
96 × 96 |
2,0 |
xhdpi |
144 × 144 |
3,0 |
xxhdpi |
192 × 192 |
4,0 |
xxxhdpi |
Tabelle 2.1 Umrechnungstabelle für Pixeldichten
Bitmaps, die im Verzeichnis drawable (also ohne Postfix) abgelegt werden, skaliert das System zur Laufzeit. Android geht in diesem Fall davon aus, dass solche Grafiken für eine mittlere Dichte vorgesehen sind. Eine solche Konvertierung unterbleibt für Dateien im Verzeichnis drawable-nodpi.
Um das Erstellen von Layouts zu vereinfachen, kennt Android sogenannte density-independent pixels. Diese abstrakte Einheit basiert auf der Pixeldichte des Bildschirms in Relation zu 160 dpi. 160dp entsprechen also immer einem Zoll. Die Formel zur Umrechnung ist sehr einfach:
pixels = dps × (density ÷ 160)
Normalerweise müssen Sie solche Berechnungen in Ihrer App aber gar nicht durchführen. Wichtig ist eigentlich nur, in allen Layouts diese Einheit zu verwenden.
Adaptive Icons
Bevor ich Ihnen im folgenden Abschnitt den Umgang mit Text erkläre, möchte ich noch auf eine Spezialität von Android eingehen, die sogenannten adaptiven Symbole. Ab Android 7.1 konnten Apps kreisrunde Icon-Ressourcen für ihre Programmstarter-Symbole bereitstellen. Auf welchen Geräten diese dann angezeigt wurden, hing von der sogenannten Device Build Configuration ab. Tatsächlich waren nur Smartphones der Pixel-Baureihe entsprechend konfiguriert. Wenn ein Programmstarter Icons beim System erfragte, lieferte das Android-Framework entweder ein Icon aus dem Manifestattribut (mehr dazu etwas später) android:icon oder android:roundIcon.
Mit Oreo hat Google diese Idee zu adaptiven Icons weiterentwickelt. Gerätehersteller können eine Maske definieren, die mit dem eigentlichen Programmsymbol verknüpft wird. Stellen Sie sich das am besten wie eine Form zum Ausstechen von Plätzchen vor – alles außerhalb der Maske fällt weg. Der Vorteil für den Entwickler: Das eigene Icon erscheint stets in der richtigen Form. Damit adaptive Icons funktionieren, müssen Sie Ihr Symbol in zwei Ebenen aufteilen, jeweils eine für Vorder- und Hintergrund. Effekte wie Schlagschatten oder Maskierungen sind leider tabu. Denn diese können unter Umständen durch das System hinzugefügt werden. Dazu gleich mehr.
Traditionell waren Programmstarter-Icons 48 × 48 dp groß. Die beiden Ebenen adaptiver Icons hingegen messen 108 × 108 geräteunabhängige Pixel. Die inneren 72 dp erscheinen innerhalb der vom Gerätehersteller gelieferten Maske. Der sichtbare Bereich kann an bestimmten Punkten auf einen Radius von 33 dp begrenzt werden. Wie viel vom eigentlichen App-Icon zu sehen ist, lässt sich deshalb nicht so ohne Weiteres vorhersagen. Darüber hinaus können das System bzw. der Programmstarter für visuelle Effekte oder Animationen bis zu 18 geräteunabhängige Pixel an allen vier äußeren Rändern verwenden.
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Listing 2.2 Beschreibungsdatei eines adaptiven Icons
Android Studio unterstützt Sie beim Erstellen von adaptiven Icons mit dem sogenannten Asset Studio. Es generiert eine XML-Datei (siehe Listing 2.2) mit den zwei Tags <foreground /> und <background />. Diese verweisen auf Drawables, die mit dem Attribut android:drawable referenziert werden. Wie Sie mit dem Asset Studio ein App-Icon erstellen, zeige ich Ihnen in Abschnitt 3.3.1, »Die App vorbereiten«.
2.2.2 Texte 

Bilder und Symbole sind ein wichtiges Gestaltungsmittel. Sinnvoll eingesetzt, helfen sie dem Anwender nicht nur beim Bedienen des Programms, sondern sorgen zudem für ein angenehmes und schönes Äußeres. Dennoch spielen auch Texte eine sehr wichtige Rolle. Sie werden in den unterschiedlichsten Bereichen einer Anwendung eingesetzt:
-
für erläuternde Texte, die durch einen Screenreader vorgelesen werden
-
für Hinweis- und Statusmeldungen
Die fertige Version von Hallo Android soll den Benutzer zunächst begrüßen und ihn nach seinem Namen fragen. Im Anschluss wird ein persönlicher Gruß angezeigt. Nach dem Anklicken einer Schaltfläche beendet sich die App.
Aus dieser Beschreibung ergeben sich die folgenden Texte. Die Bezeichner vor dem jeweiligen Text werden Sie später im Programm wiederfinden:
-
welcome – Guten Tag. Schön, dass Sie mich gestartet haben. Bitte verraten Sie mir Ihren Namen.
-
next – Weiter
-
hello – Hallo <Platzhalter>. Ich freue mich, Sie kennenzulernen.
-
finish – Fertig
Ein Großteil der Texte wird zur Laufzeit so ausgegeben, wie sie schon während der Programmierung erfasst wurden. Eine kleine Ausnahme bildet die Grußformel, denn sie besteht aus einem konstanten und einem variablen Teil. Letzterer ergibt sich erst, nachdem der Anwender seinen Namen eingetippt hat. Wie Sie gleich sehen werden, ist es in Android sehr einfach, dies zu realisieren. Da Sie Apps in Kotlin schreiben, könnten Sie die auszugebenden Meldungen einfach als Raw String im Quelltext ablegen. Das ist praktisch, weil Sie auf diese Weise die Zeilenumbrüche an der gewünschten Stelle setzen können. Das sähe folgendermaßen aus:
message.text = """
Guten Tag. Schön, dass Sie mich gestartet haben.
Bitte verraten Sie mir Ihren Namen.
""".trimIndent()
Das hat allerdings mehrere Nachteile: Zum einen müssen Sie die aus Gründen der Lesbarkeit eingefügten Leerzeichen am Zeilenanfang mit trimIndent() wieder löschen. Sonst werden sie ebenfalls ausgegeben, was nicht gewünscht ist. Wenn man seinen App-Code auf mehrere Quelltextdateien aufteilt (und das ist bei umfangreicheren Programmen auf jeden Fall eine gute Idee), merkt man oft nicht, wenn man gleiche Texte mehrfach definiert. Das kann die Installationsdatei der App vergrößern und unnötig Speicher kosten. Außerdem wird es auf diese Weise sehr schwer, mehrsprachige Anwendungen zu bauen. Wenn Sie aber eine App über Google Play vertreiben möchten, sollten Sie neben den deutschsprachigen Texten mindestens eine englische Lokalisierung ausliefern. Unter Android werden Texte daher zentral in der Datei strings.xml abgelegt. Sie befindet sich im Verzeichnis values. Ändern Sie die durch den Projektassistenten angelegte Fassung folgendermaßen ab:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Name der App -->
<string name="app_name">Hallo Android!</string>
<!-- Willkommensmeldung -->
<string name="welcome">
Guten Tag. Schön, dass Sie mich gestartet haben.
Bitte verraten Sie mir Ihren Namen.
</string>
<!-- Persönlicher Gruß -->
<string name="hello">
Hallo %1$s. Ich freue mich, Sie kennenzulernen.
</string>
<!-- Beschriftungen für Schaltflächen -->
<string name="next">Weiter</string>
<string name="finish">Fertig</string>
</resources>
Listing 2.3 »strings.xml«
Das Attribut name des Elements <string> wird später im Quelltext als Bezeichner verwendet. Der Name muss deshalb innerhalb des Projekts eindeutig sein. Ich betone das, weil das Ablegen von Zeichenketten in strings.xml nur eine Konvention ist (der Sie unbedingt folgen sollten). Zeichenketten können auch in anders genannten XML-Dateien definiert werden, die unter res/values abgelegt wird. Ist Ihnen im Listing die fett gesetzte Zeichenfolge %1$s aufgefallen? Android wird an dieser Stelle den vom Benutzer eingegebenen Namen einfügen. Wie dies funktioniert, zeige ich Ihnen später.
[»] Hinweis
Die Zeilen Hallo %1$s… und Guten Tag. sind nicht eingerückt, weil die führenden Leerzeichen sonst in die App übernommen werden, was in der Regel nicht gewünscht ist.
Vielleicht fragen Sie sich, wie Sie Ihr Programm mehrsprachig ausliefern können, wenn es genau eine zentrale Datei strings.xml gibt. Neben dem Verzeichnis values kann es lokalisierte Ausprägungen geben, die auf das Minuszeichen und auf ein Sprachkürzel aus zwei Buchstaben enden, zum Beispiel values-en oder values-fr. Die Datei strings.xml in diesen Ordnern enthält Texte in den entsprechenden Sprachen, also auf Englisch oder Französisch. Muss Android auf eine Zeichenkette zugreifen, geht das System vom Speziellen zum Allgemeinen. Ist die Standardsprache also beispielsweise Englisch, wird zuerst versucht, den Text in values-en/strings.xml zu finden. Gelingt dies nicht, wird values/strings.xml verwendet. In dieser Datei müssen also alle Strings definiert werden, Lokalisierungen hingegen können unvollständig sein. Bei der Erstellung mehrsprachiger Texte unterstützt Sie Android Studio mit dem Translations Editor. Ich werde in einem späteren Kapitel darauf zurückkommen.
Im folgenden Abschnitt stelle ich Ihnen sogenannte Views vor. Bei ihnen handelt es sich um die Grundbausteine, aus denen die Benutzeroberfläche einer App zusammengesetzt wird.
2.2.3 Views 

Hallo Android besteht auch nach vollständiger Realisierung aus sehr wenigen Bedienelementen, und zwar aus
-
einem nicht editierbaren Textfeld, das den Gruß unmittelbar nach dem Programmstart sowie nach Eingabe des Namens darstellt,
-
einer Schaltfläche, die je nach Situation mit Weiter oder Fertig beschriftet ist, und aus
-
einem Eingabefeld, das nach dem Anklicken der Schaltfläche Weiter ausgeblendet wird.
Wie die Komponenten auf dem Bildschirm platziert werden sollen, zeigt ein sogenannter Wireframe, den Sie in Abbildung 2.8 sehen. Man verwendet solche abstrakten Darstellungen gern, um die logische Struktur einer Bedienoberfläche in das Zentrum des Interesses zu rücken.
Abbildung 2.8 Prototyp der Benutzeroberfläche von »Hallo Android«
Unter Android sind alle Bedienelemente direkte oder indirekte Unterklassen der Klasse android.view.View. Jede View belegt einen rechteckigen Bereich des Bildschirms. Seine Position und Größe wird durch Layouts bestimmt, die wiederum von android.view.ViewGroup erben, die ebenfalls ein Kind von View ist. Sie haben üblicherweise keine eigene grafische Repräsentation, sondern sind Container für weitere Views und ViewGroups.
Die Text- und Eingabefelder sowie die Schaltflächen, die in Hallo Android verwendet werden, sind also Views. Konkret verwenden wir die Klassen Button, TextView und EditText. Wo sie auf dem Bildschirm positioniert werden und wie groß sie sind, wird hingegen durch die ViewGroup LinearLayout festgelegt.
Zur Laufzeit einer Anwendung manifestiert sich ihre Benutzeroberfläche demnach als Objektbaum. Aber nach welcher Regel wird er erzeugt? Wie definieren Sie als Entwickler den Zusammenhang zwischen einem Layout, einem Textfeld und einer Schaltfläche? Sie könnten die Bedienelemente im Code zusammenfügen:
val v = ScrollView(context)
val layout = LinearLayout(context)
layout.orientation = LinearLayout.VERTICAL
v.addView(layout)
layout.addView(getCheckBox(context, Locale.GERMANY))
layout.addView(getCheckBox(context, Locale.US))
layout.addView(getCheckBox(context, Locale.FRANCE))
Listing 2.4 Beispiel für den programmgesteuerten Bau einer Oberfläche
Allerdings ist dies nicht die typische Vorgehensweise. Die lernen Sie im folgenden Abschnitt kennen.
2.2.4 Oberflächenbeschreibungen 

Eine Android-App beschreibt ihre Benutzeroberflächen mittels XML-basierter Layoutdateien, die zur Laufzeit zu Objektbäumen »aufgeblasen« werden. Alle Bedienelemente von Hallo Android werden in einen Container des Typs LinearLayout gepackt. Seine Kinder erscheinen entweder neben- oder untereinander auf dem Bildschirm. Wie Sie gleich sehen werden, steuert das Attribut android:orientation die Laufrichtung. Für die Größe der Views und der ViewGroups gibt es die beiden Attribute der android:layout_width und android:layout_height.
Oberflächenbeschreibungen werden in layout, einem Unterverzeichnis von res, gespeichert. Beim Anlegen des Projekts hat der Android-Studio-Projektassistent dort die Datei activity_main.xml abgelegt. Öffnen Sie diese mit Doppelklick, und ändern Sie sie entsprechend Listing 2.5 ab. Damit Sie den Quelltext eingeben können, klicken Sie in der oberen rechten Ecke des Editorfensters auf Code.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/next_finish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end" />
</LinearLayout>
Listing 2.5 »activity_main.xml«
Die XML-Datei bildet die Hierarchie der Benutzeroberfläche ab. Demzufolge ist <LinearLayout> das Wurzelelement. Mein Beispiel enthält die drei Kinder <TextView>, <EditText> und <Button>. Jedes Element hat die bereits kurz angesprochenen Attribute android:layout_width und android:layout_height. Deren Wert match_parent besagt, dass die Komponente die Breite oder Höhe des Elternobjekts erben soll. Der Wert wrap_content hingegen bedeutet, dass sich die Größe aus dem Inhalt der View ergibt, beispielsweise aus der Beschriftung einer Schaltfläche. Die Zeile android:layout_gravity="end" sorgt dafür, dass die Schaltfläche rechtsbündig angeordnet wird.
[+] Tipp
Anstelle von match_parent finden Sie im Internet oft noch die ältere Notation fill_parent. Diese wurde schon in Android 2.2 (API-Level 8) von match_parent abgelöst. Für welche Variante Sie sich entscheiden, ist nur von Belang, wenn Sie für sehr alte Plattformversionen entwickeln. Denn abgesehen vom Namen sind beide identisch. Ich rate Ihnen trotzdem, match_parent zu verwenden.
Ist Ihnen aufgefallen, dass keinem Bedienelement ein Text oder eine Beschriftung zugewiesen wird? Und was bedeuten Zeilen, die mit android:id="@+id/ beginnen? Wie Sie bereits wissen, erzeugt Android zur Laufzeit einer Anwendung aus den Oberflächenbeschreibungen entsprechende Objektbäume. Zu der in der XML-Datei spezifizierten Schaltfläche gibt es also eine Instanz der Klasse Button. Um auf diese Instanz eine Referenz ermitteln zu können, wird ein Name definiert, beispielsweise next_finish. Wie auch bei strings.xml sorgen die Android-Entwicklungswerkzeuge dafür, dass nach Änderungen an Layoutdateien korrespondierende Einträge in der generierten Klasse R vorgenommen werden. Wie Sie diese nutzen, sehen Sie gleich.
Speichern Sie Ihre Eingaben, und wechseln Sie zurück zum grafischen Editor, indem Sie auf die Registerkarte Design klicken. Er sollte in etwa so wie in Abbildung 2.9 aussehen. Machen Sie sich über die angezeigte Warnung keine Gedanken, wir kümmern uns etwas später darum.
[»] Hinweis
In XML-Dateien nutzt Google gern den Underscore als verbindendes Element, zum Beispiel in layout_width, layout_height oder match_parent. Sie sollten zumindest in Layoutdateien diesem Stil folgen. Aus diesem Grund habe ich die ID der Schaltfläche zum Weiterklicken und Beenden der App next_finish genannt. In Kotlin-Quelltexten ist aber die sogenannte CamelCase-Schreibweise gebräuchlich, deshalb heißt die Variable der Schaltfläche nextFinish.
Abbildung 2.9 Erstellen der Benutzeroberfläche im Design-Modus