Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
Vorwort
Teil I Grundlagen
1 Android – eine offene, mobile Plattform
2 Hallo Android!
3 Von der Idee zur Veröffentlichung
Teil II Elementare Anwendungsbausteine
4 Wichtige Grundbausteine von Apps
5 Benutzeroberflächen
6 Multitasking
Teil III Gerätefunktionen nutzen
7 Telefonieren und surfen
8 Sensoren, GPS und Bluetooth
Teil IV Dateien und Datenbanken
9 Dateien lesen, schreiben und drucken
10 Datenbanken
Teil V Multimedia und Produktivität
11 Multimedia
12 Kontakte und Organizer
A Einführung in Kotlin
B Jetpack Compose
C Häufig benötigte Codebausteine
D Literaturverzeichnis
E Die Begleitmaterialien
Stichwortverzeichnis

Ihre Meinung?
Spacer
<< zurück
Android 11 von Thomas Künneth
Das Praxisbuch für App-Entwickler
Buch: Android 11

Android 11
Pfeil 5 Benutzeroberflächen
Pfeil 5.1 Views und ViewGroups
Pfeil 5.1.1 Views
Pfeil 5.1.2 Positionierung von Bedienelementen mit ViewGroups
Pfeil 5.1.3 Alternative Layouts
Pfeil 5.2 Vorgefertigte Bausteine für Oberflächen
Pfeil 5.2.1 Listen darstellen mit ListFragment
Pfeil 5.2.2 Programmeinstellungen mit dem PreferencesFragment
Pfeil 5.2.3 Dialoge
Pfeil 5.2.4 Menüs und Action Bar
Pfeil 5.3 Nachrichten und Hinweise
Pfeil 5.3.1 Toast und Snackbar
Pfeil 5.3.2 Benachrichtigungen
Pfeil 5.3.3 App Shortcuts
Pfeil 5.4 Trennung von Oberfläche und Logik
Pfeil 5.4.1 Bedienelemente ohne »findViewById()«
Pfeil 5.4.2 Android Architecture Components
Pfeil 5.5 Dark Mode
Pfeil 5.5.1 Das DayNight-Theme
Pfeil 5.5.2 Dark Mode in eigenen Themes
Pfeil 5.6 Zusammenfassung
 
Zum Seitenanfang

5    Benutzeroberflächen Zur vorigen ÜberschriftZur nächsten Überschrift

Die Bedienoberfläche ist das Aushängeschild einer App. Gerade auf mobilen Geräten ist es wichtig, dem Anwender die Nutzung eines Programms so einfach wie möglich zu machen. Welche Möglichkeiten Android hier bietet, zeige ich Ihnen in diesem Kapitel.

Android stellt Ihnen eine ganze Reihe von Bedienelementen zur Verfügung, mit denen Sie die Benutzeroberfläche Ihrer App gestalten können. Diese Komponenten werden während der Entwicklung in XML-Dateien definiert. Zur Laufzeit macht die Plattform daraus Objektbäume.

 
Zum Seitenanfang

5.1    Views und ViewGroups Zur vorigen ÜberschriftZur nächsten Überschrift

Sie kennen solche Layoutbeschreibungen schon aus den vorherigen Kapiteln. Entsprechende Dateien werden in Unterverzeichnissen von res abgelegt. Ihre Namen beginnen mit layout. Die folgende, recht einfach gehaltene Datei widgetdemo.xml gehört zu dem gleichnamigen Beispielprojekt.

<?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"
android:padding="6dp">
<EditText
android:id="@+id/textfield"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint"
android:imeOptions="actionGo"
android:importantForAutofill="no"
android:inputType="textNoSuggestions"
android:lines="1" />
<Button
android:id="@+id/apply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/apply" />
<FrameLayout
android:id="@+id/frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

Listing 5.1    Die Datei »widgetdemo.xml« des Projekts »WidgetDemo«

Der Ausdruck android:imeOptions="actionGo" sorgt dafür, dass auf Geräten mit physikalischer Tastatur durch Drücken der Eingabetaste eine Aktion ausgelöst wird. Virtuelle Keyboards zeigen eine spezielle »Go«-Schaltfläche an. Programmseitig müssen Sie zusätzlich mit setOnEditorActionListener() einen OnEditorActionListener (Sie haben ihn schon in Abschnitt 2.3.3 kennengelernt) registrieren und dessen Methode onEditorAction() implementieren. Das lässt sich elegant mit einem Lambda-Ausdruck umsetzen (Listing 5.2). Der Ausdruck android:inputType="textNoSuggestions" sorgt dafür, dass für das Eingabefeld keine Vorschläge gemacht werden. Da Sie hier absolute Klassennamen eintippen werden, würden diese ins Leere laufen.

Sicher ist Ihnen aufgefallen, dass die vier Elemente LinearLayout, FrameLayout, Button und EditText einige gemeinsame Attribute haben. android:layout_width und android:layout_height sind überall vorhanden. android:id fehlt nur in LinearLayout. IDs werden benötigt, um sich innerhalb einer Klasse, zum Beispiel einer Activity, auf eine View oder eine ViewGroup beziehen zu können – vielleicht weil Sie die Farbe setzen, einen Status abfragen oder einen Text ändern wollen. In meinem Beispiel ist das für LinearLayout aber nicht nötig.

Zu jedem XML-Element gibt es ein Pendant in der Android-Klassenbibliothek. Instanzen dieser Klassen bilden zur Laufzeit einer App einen Objektbaum, der die Benutzeroberfläche repräsentiert. Die XML-Attribute werden hierbei auf Instanzvariablen abgebildet. Das System nimmt Ihnen die Arbeit des Entfaltens praktisch vollständig ab. Häufig müssen Sie nur die Methode setContentView() aufrufen.

 
Zum Seitenanfang

5.1.1    Views Zur vorigen ÜberschriftZur nächsten Überschrift

Die Basisklasse aller Bedienelemente ist android.view.View. Die Benutzeroberfläche einer App besteht also aus einer oder mehreren Views oder von ihr abgeleiteten Klassen. View fasst die Eigenschaften und Methoden zusammen, die mindestens nötig sind, um an einer bestimmten Position ein rechteckiges Element mit vorgegebener Größe darzustellen. Beispielsweise wird onDraw() aufgerufen, wenn sich die Komponente zeichnen soll. Views sind also für ihr Rendering selbst verantwortlich. Ebenso kümmern sie sich um die Bearbeitung von Tastatur-, Touch- und Trackball-Ereignissen.

Eigenschaften von Views

Wenn die Benutzeroberfläche in einer XML-Datei deklariert und erst zur Laufzeit zu einem Objektbaum entfaltet wird, muss es einer App möglich sein, Referenzen auf spezifische Komponenten zu ermitteln. Dies ist beispielsweise nötig, um auf Benutzeraktionen zu reagieren. Denken Sie an das Anklicken einer Schaltfläche oder an unmittelbare Reaktionen auf Eingaben in ein Textfeld. Activities enthalten hierfür die Methode findViewById(). Der ihr übergebene Wert entspricht üblicherweise einer Konstante aus R.id. Diese wiederum bezieht ihre Informationen aus den Attributen android:id der XML-Dateien. Das Eingabefeld meines Beispiels ist über seine ID textfield erreichbar. Die Klasse WidgetDemoActivity, die Sie gleich kennenlernen werden, greift mit der Anweisung

val e = findViewById<EditText>(R.id.textfield)

auf textfield zu.

[»]  Hinweis

Sie werden im Internet viele Beispiele finden, in denen der von findViewById() zurückgegebene Wert gecastet wird. Erst in Oreo hat Google ihn generisch gemacht (<T extends View> T). Damit wird der Cast unnötig. Sofern Sie in Ihren Projekten compileSdkVersion auf einen Wert größer oder gleich 26 setzen, können (und sollten) Sie ihn problemlos weglassen. Das hat übrigens keine Auswirkungen auf die Lauffähigkeit unter älteren Plattformversionen (sofern alle anderen Voraussetzungen erfüllt sind).

Sie können Views zusätzlich zu diesen IDs ein Tag zuweisen. Tags werden nicht für die Identifizierung von Views verwendet, sondern dienen als eine Art Speicher für Zusatzinformationen. Anstatt solche Daten in einer gesonderten Struktur abzulegen, können Sie diese (oder gegebenenfalls eine Referenz auf sie) direkt in der View ablegen. In Kapitel 3 nutzt die Methode getView() der Klasse TierkreiszeichenAdapter Tags, um Referenzen auf ViewHolder zu speichern.

Views sind Rechtecke. Ihre Positionen werden durch ihre linken oberen Ecken bestimmt. Die Werte entsprechen Pixeln. Sie können sie mit getLeft() und getTop() abfragen. Da Views Hierarchien abbilden, werden Koordinaten stets relativ zur Elternkomponente angegeben, nicht als absolute Werte. Die Größe einer View ergibt sich aus den Dimensionen Breite und Höhe. Tatsächlich gehören sogar zwei solcher Paare zu einer View: Das erste Paar gibt an, wie groß innerhalb ihres Elternobjekts die View sein möchte. Die beiden Dimensionen lassen sich mit getMeasuredWidth() und getMeasuredHeight() abfragen.

Das zweite Paar gibt die tatsächliche Größe einer View auf dem Bildschirm an, und zwar nach dem Layoutvorgang, aber vor dem Zeichnen. Die Werte des zweiten Paares werden von den beiden Methoden getWidth() und getHeight() geliefert. Die beiden Paare können, müssen aber nicht unterschiedlich sein. Die Größe einer View wird auch durch das sogenannte Padding beeinflusst. Die Pixelwerte des Paddings geben an, wie weit der Inhalt einer View von ihrem oberen, unteren, linken und rechten Ende entfernt sein soll. Das Padding können Sie mit der Methode setPadding() setzen und mit getPaddingLeft(), getPaddingTop(), getPaddingRight() und getPaddingBottom() abfragen. Das Konzept von Rändern wird übrigens in den sogenannten ViewGroups umgesetzt, die ich Ihnen im folgenden Abschnitt ausführlich vorstelle.

Komponenten programmatisch erzeugen

Die App WidgetDemo, die Sie in Abbildung 5.1 sehen, besteht im Wesentlichen aus einem Eingabefeld und einer Schaltfläche. Geben Sie den voll qualifizierten Klassennamen eines Bedienelements (zum Beispiel android.widget.AnalogClock, android.widget.RatingBar oder android.widget.DatePicker) ein, und klicken Sie anschließend auf Übernehmen. Das Programm instanziiert das entsprechende Objekt und fügt es in den Komponentenbaum ein, der aus der XML-Datei widgetdemo.xml entfaltet wurde. Wie dies funktioniert, zeigt Listing 5.2.

Die App »WidgetDemo«

Abbildung 5.1    Die App »WidgetDemo«

Wie üblich lädt der Aufruf von setContentView() die Benutzeroberfläche und stellt sie dar. Mit findViewById() ermitteln wir Referenzen auf Objekte, die wir im weiteren Programmverlauf noch benötigen, zum Beispiel um für die Schaltfläche Übernehmen einen OnClickListener und das Eingabefeld einen OnEditorActionListener zu registrieren.

package com.thomaskuenneth.androidbuch.widgetdemo

import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.*
import androidx.appcompat.app.AppCompatActivity

private val TAG = WidgetDemoActivity::class.simpleName
class WidgetDemoActivity : AppCompatActivity() {

private lateinit var frame: FrameLayout
private lateinit var textfield: EditText
private lateinit var apply: Button

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.widgetdemo)
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
frame = findViewById(R.id.frame)
textfield = findViewById(R.id.textfield)
apply = findViewById(R.id.apply)
apply.setOnClickListener {
val name = textfield.text.toString()
try {
val c = Class.forName(name)
val o = c.getDeclaredConstructor(Context::class.java)
.newInstance(this)
if (o is View) {
frame.removeAllViews()
frame.addView(o, params)
frame.forceLayout()
}
} catch (tr: Throwable) {
val str = getString(R.string.error, name)
Toast.makeText(this, str, Toast.LENGTH_LONG).show()
Log.e(TAG, "Fehler beim Instanzieren von $name", tr)
}
}
textfield.setOnEditorActionListener { _, _, _ ->
apply.performClick()
true
}
}
}

Listing 5.2    Die Klasse »WidgetDemoActivity«

Das Erzeugen einer View aus dem voll qualifizierten Klassennamen geschieht mittels Reflection. Zunächst wird mit Class.forName() eine Class-Instanz ermittelt. Diese brauchen wir, um mit getDeclaredConstructor() einen geeigneten (parameterlosen) Konstruktor zu erhalten. newInstance() erzeugt das gewünschte Objekt. Vor dessen Verwendung prüft if (o is View), ob der eingegebene Klassenname tatsächlich eine View war. Andernfalls würde ja das Hinzufügen zu einem Komponentenbaum nicht funktionieren. Falls ein Fehler auftritt, zeigt die App mit Toast.makeText() ein kleines Infotäfelchen an. Weitere Informationen hierzu erhalten Sie in Abschnitt 5.3, »Nachrichten und Hinweise«.

Wir müssen noch klären, was es mit der View namens FrameLayout auf sich hat, denn nach dem Start der App ist von ihr zunächst nichts zu sehen. FrameLayout ist ein Kind der Klasse android.view.ViewGroup. Solche Container nehmen weitere Bedienelemente auf. Zunächst wird mit der Anweisung

val f = findViewById<FrameLayout>(R.id.frame)

eine Referenz auf die Komponente ermittelt. Anschließend können Sie mit addView() Views oder ViewGroups hinzufügen oder mit removeAllViews() entfernen. Auf diese Weise ist problemlos das Mischen von deklarativ und programmatisch erstellten Oberflächen möglich.

Ist Ihnen aufgefallen, dass bei addView() ein Objekt des Typs android.view.ViewGroup.LayoutParams übergeben wurde? Wie auch bei per XML deklarierten Oberflächen müssen Sie beim Hinzufügen einer View bestimmte Werte zwingend angeben, nämlich Breite und Höhe. Dies geschieht mit LayoutParams-Objekten. Der Aufruf der Methode forceLayout() erzwingt das erneute Anordnen der Bedienelemente. Das ist nötig, damit die hinzugefügte Komponente korrekt angezeigt wird.

Auf Benutzeraktionen reagieren

Die Idee, mithilfe von sogenannten Listenern auf Benutzeraktionen zu reagieren, wird in vielen UI-Frameworks umgesetzt. Bedienelemente bieten an, Referenzen von Objekten, die entweder bestimmte Interfaces implementieren oder von bestimmten Basisklassen ableiten, bei sich zu registrieren. Tritt ein festgelegtes Ereignis ein oder löst der Benutzer eine bestimmte Aktion aus, ruft die Komponente eine Methode der ihr übergebenen Objekte auf. Welche Listener ein Bedienelement anbieten, entnehmen Sie bitte Googles Entwicklerdokumentation. Der in Listing 5.3 gezeigte OnClickListener steht zum Beispiel auch bei Ankreuzfeldern (android.widget.CheckBox) zur Verfügung. Wird der Button status angeklickt, wird die Boolean-Eigenschaft isChecked (sie gehört, wie Sie gleich sehen werden, zu einer CheckBox) negiert.

val status = findViewById<Button>(R.id.status)
status.setOnClickListener { checkbox.isChecked = !checkbox.isChecked }

Listing 5.3    Auf das Anklicken einer Schaltfläche per »Listener« reagieren

Bei der Wahl des Listeners müssen Sie allerdings genau überlegen, was Sie in Ihrer App erreichen möchten. Hierzu ein Beispiel: Das in Abbildung 5.2 gezeigte Programm ListenerDemo gibt in einem Textfeld den Status einer CheckBox aus.

Die App »ListenerDemo«

Abbildung 5.2    Die App »ListenerDemo«

Das Anklicken einer Schaltfläche kehrt diesen Status um. Wie in Listing 5.4 zu sehen ist, registriert die Klasse einen OnClickListener, der die Meldung beim Anklicken der CheckBox mit folgender Anweisung aktualisiert:

textview.text = checkbox.isChecked.toString()

Die Schaltfläche Status umkehren kehrt den aktuellen Zustand des Ankreuzfeldes mit dem Code aus Listing 5.3 um. Wenn Sie nach dem Start der App im Emulator oder auf einem realen Gerät die Funktion der Schaltfläche und des Ankreuzfeldes testen, fällt auf, dass beim Anklicken der CheckBox die Meldung korrekt aktualisiert wird. Status umkehren hingegen setzt zwar das Ankreuzfeld richtig (aus angekreuzt wird nicht angekreuzt und umgekehrt), allerdings ändert sich die Meldung des Textfeldes nicht. Sie zeigt einen anderen Wert an, als die CheckBox tatsächlich hat. Das ist natürlich unschön.

package com.thomaskuenneth.androidbuch.listenerdemo

import android.os.Bundle
import android.widget.Button
import android.widget.CheckBox
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class ListenerDemoActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textview = findViewById<TextView>(R.id.textview)
val checkbox = findViewById<CheckBox>(R.id.checkbox)
checkbox.setOnClickListener {
textview.text = checkbox.isChecked.toString()
}
val status = findViewById<Button>(R.id.status)
status.setOnClickListener {
checkbox.isChecked = !checkbox.isChecked
}
}
}

Listing 5.4    Die Klasse »ListenerDemoActivity«

Eine schnelle Umgehung dieses Problems wäre, die Anweisung

textview.text = checkbox.isChecked.toString()

in den Listener der Schaltfläche zu kopieren. Das ist aber unsauber, weil Sie nach Möglichkeit Code nicht duplizieren sollten. Lassen Sie uns stattdessen überlegen, was der Grund für das Fehlverhalten ist. Den Status mit der Eigenschaft isChecked zu setzen, ist ganz sicher das richtige Vorgehen. Vielleicht ist aber der OnClickListener die falsche Wahl, denn er reagiert »nur« auf das Anklicken bzw. Antippen der Komponente. Aber das passiert beim Setzen durch Code ja nicht. Deshalb kennt Android einen OnCheckedChangeListener. Er reagiert auf Statuswechsel. Warum dieser erfolgt, ist der Plattform beim Aufrufen des Listeners egal.

Bitte kommentieren Sie die Anweisung checkbox.setOnClickListener { ... } aus, und fügen Sie stattdessen folgendes Codeschnipsel ein:

checkbox.setOnCheckedChangeListener { _, isChecked ->
textview.text = isChecked.toString()
}

Listing 5.5    Auf das Setzen bzw. Entfernen von Häkchen reagieren

Der erste Parameter enthält die Komponente, deren Status sich geändert hat (entspricht also checkbox). Da wir nicht weiter darauf zugreifen müssen, habe ich _ als Variablennamen verwendet. Nach dieser Änderung führt das Anklicken der Schaltfläche zur Aktualisierung des Textfeldes, weil aufgrund der Zustandsänderung der OnCheckedChangeListener aufgerufen wird.

Im folgenden Abschnitt stelle ich Ihnen die sogenannten ViewGroups ausführlicher vor. Wie viele andere UI-Frameworks ordnet Android Bedienelemente auf Grundlage von Regeln an, die in Layouts implementiert wurden. Diese Klassen leiten von android.view.ViewGroup ab.

 
Zum Seitenanfang

5.1.2    Positionierung von Bedienelementen mit ViewGroups Zur vorigen ÜberschriftZur nächsten Überschrift

ViewGroups sind Container, können also weitere Views und ViewGroups enthalten. In Ihren Apps verwenden Sie normalerweise nicht diese Basisklasse, sondern abgeleitete Implementierungen wie die Ihnen bereits bekannten Klassen LinearLayout oder das (sehr einfache) FrameLayout.

FrameLayout

In der App WidgetDemo dieses Kapitels habe ich in der zugehörigen Layoutdatei ein FrameLayout definiert, um eine zur Laufzeit erzeugte Komponente in den Objektbaum einhängen zu können. Das ist auch der Haupteinsatzbereich dieser Klasse. FrameLayouts enthalten normalerweise nur ein Element, auch wenn sowohl programmatisch mittels addView() als auch in der Layoutdatei mehrere Views hinzugefügt werden können. Dann werden sie in der Reihenfolge, in der sie hinzugefügt wurden, gezeichnet. Die Größe des Elements bei "wrap_content" entspricht dem größten Kindelement. Deren Positionen lassen sich mit android:layout_gravity kontrollieren. Sehen Sie sich bitte die Layoutdatei framelayout_demo.xml an.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray">
<View
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="top|start"
android:background="#ffff00" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@android:color/white"
android:text="@string/hello"
android:textColor="@android:color/black" />
</FrameLayout>

Listing 5.6    »framelayout_demo.xml«

Die TextView erscheint am weitesten vorn. android:textColor bestimmt ihre Farbe (schwarz). Android definiert eine Reihe von Farben, auf die Sie mit @android:color/ zugreifen können. Ihr Hintergrund ist weiß. Der Wert center ihres Attributs android:layout_gravity zentriert sie. Die mittlere Komponente, eine View, erscheint als solides gelbes Rechteck, weil ich mit dem Ausdruck android:background="#ffff00" die Hintergrundfarbe auf Gelb gesetzt habe (vereinfacht ausgedrückt besteht die Klasse View ausschließlich aus einem Hintergrund). Dem Hashzeichen folgen drei hexadezimale Zahlen, die die Rot-, Grün- und Blauanteile der zu verwendenden Farbe repräsentieren.

android:layout_gravity="top|start" sorgt dafür, dass das Element in der linken (genauer gesagt am Beginn der Leserichtung befindlichen) oberen Ecke von FrameLayout gezeichnet wird. Die Größe der View habe ich als sogenannte density-independent pixels angegeben. Die Einheit dp abstrahiert von der Pixeldichte des verwendeten Bildschirms. Auf diese Weise passen sich Ihre Apps besser an unterschiedliche Anzeigen an. Ausführliche Informationen hierzu finden Sie in Abschnitt 2.2.1, »Grafiken«.

Die entfaltete Layoutdatei »framelayout_demo.xml«

Abbildung 5.3    Die entfaltete Layoutdatei »framelayout_demo.xml«

Abbildung 5.3 zeigt die entfaltete Layoutdatei. Um sie selbst auszuprobieren, verwenden Sie am besten das Projekt LeeresProjekt aus Kapitel 4, »Wichtige Grundbausteine von Apps«. Es enthält bereits diese sowie die beiden folgenden Layoutdateien. Sie müssen nur noch in onCreate() den Text R.layout.activity_main durch R.layout.framelayout_demo ersetzen. Außerdem müssen für dieses und die folgenden Beispiele in der Datei strings.xml die folgenden Zeilen vorhanden sein. Die Version in den Begleitmaterialien enthält sie bereits.

<string name="hello">Hallo!</string>
<string name="ok">OK</string>
<string name="cancel">Abbruch</string>
<string name="hint">Bitte Text eingeben</string>

LinearLayout

LinearLayout ist ein sehr häufig verwendetes Layout, nicht nur in Beispielen und Tutorials. Es ist einfach zu verstehen und lässt sich flexibel einsetzen. Die Klasse ordnet Kinder neben- oder untereinander an. Sie können deren Ausrichtung mit android:gravity kontrollieren.

Die entfaltete Layoutdatei »linearlayout_demo.xml«

Abbildung 5.4    Die entfaltete Layoutdatei »linearlayout_demo.xml«

Die in Abbildung 5.4 dargestellte Datei linearlayout_demo.xml zeigt im entfalteten Zustand zwei Schaltflächen und ein Eingabefeld. Mit dem Attribut android:orientation steuern Sie, ob Kindelemente spalten- (horizontal) oder zeilenweise (vertical) angeordnet werden. Der Ausdruck android:gravity="end" führt dazu, dass die beiden Schaltflächen des Beispiels zum rechten Rand hin (genauer: zum Ende der Leserichtung hin) ausgerichtet werden.

<?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"
android:padding="8dp">
<EditText
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:hint="@string/hint"
android:importantForAutofill="no"
android:inputType="text" />
<LinearLayout
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<Button
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel" />
<Button
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok" />
</LinearLayout>
</LinearLayout>

Listing 5.7    »linearlayout_demo.xml«

Mit dem Attribut android:layout_weight können Sie die Größen der Kinder dem freien Platz entsprechend verteilen. Wie das funktioniert, zeige ich Ihnen in meinem Beispiel FragmentDemo3 in Kapitel 4. Auch in linearlayout_demo.xml kommt android:layout_weight vor, aber nur einmal. Was kann denn da verteilt werden? Die Komponente EditText soll möglichst hoch werden, aber noch genügend Platz für das untergeordnete LinearLayout und seine zwei Schaltflächen lassen. Das geht einfach mit android:layout_height="0dp" und android:layout_weight="1.0".

Oberflächen mit dem Layout Inspector analysieren

Wie Sie gesehen haben, können Sie durch das Schachteln von LinearLayouts mit geringem Aufwand die Benutzeroberfläche Ihrer App gestalten. Allerdings belegen jede View und jede ViewGroup Speicher. Zudem ist das Entfalten von komplexen Komponentengeflechten aufwendig, verbraucht also Rechenleistung. Auch wenn moderne Smartphones und Tablets eine beachtliche Leistungsfähigkeit erreicht haben, ist effiziente Programmierung weiterhin sehr wichtig. Android Studio enthält deshalb den Layout Inspector. Er hilft Ihnen beim Analysieren Ihrer Benutzeroberflächen. Sie starten ihn über Tools • Layout Inspector. Da sich der Layout Inspector mit einem Prozess auf dem Gerät oder Emulator verbinden möchte, sollten Sie die gewünschte App vorher mit Run • Run 'app' öffnen. Alternativ können Sie in der Klappliste am oberen Rand seines Fensters einen Prozess auswählen. Ihr Titel entspricht entweder dem Namen des Prozesses oder Select Process.

Abbildung 5.5 zeigt, aus wie vielen Objekten die Oberfläche einer App zur Laufzeit besteht. Neben den Views aus der Datei linearlayout_demo.xml sind Komponenten der Plattform (zum Beispiel DecorView) sowie die Action Bar zu sehen. Der Layout Inspector besteht aus drei Bereichen. Component Tree zeigt die vollständige Benutzeroberfläche als baumartige Struktur. Klicken Sie ein Element mit der linken Maustaste an, werden dessen Eigenschaften in der Sicht Attributes aufgelistet. Dies funktioniert auch im mittleren, zoom- und verschiebbaren Bereich. Er stellt die Oberfläche dar. Ein Rechtsklick öffnet ein Kontextmenü, mit dem Sie Teile des Komponentenbaums ein- und ausblenden. Mit Load Overlay (inline image) können Sie eine Schablone laden und über die Oberflächenvorschau legen, um den aktuellen Stad mit UI-Mockups zu vergleichen.

Der Layout Inspector

Abbildung 5.5    Der Layout Inspector

Haben Sie bemerkt, dass Abbildung 5.5 die Klassen AppCompatButton und AppCompatEditText enthält, obwohl ich in der Layoutdatei doch Button und EditText verwende? Die Activity des Projekts LeeresProjekt leitet von androidx.appcompat.app.AppCompatActivity ab. Sie gehört zur Jetpack-Komponente Appcompat. Deren Verwendung führt dazu, dass beim Entfalten von Layoutdateien spezialisierte Versionen der Basisklassen instanziiert werden. Auf ihre Apps hat dies normalerweise keinen Einfluss. Achten Sie aber darauf, bei Typprüfungen nicht die abgeleiteten, sondern die Klassen des Frameworks (beispielsweise android.widget.Button) zu verwenden.

RelativeLayout

Das RelativeLayout kann nicht nur nebeneinanderliegende Schaltflächen ohne zusätzlichen Container anordnen. Die vollständige Benutzeroberfläche des Beispiels aus dem vorherigen Abschnitt ist damit intuitiv und effizient umsetzbar. Denken Sie daran, dass die Datei relativelayout_demo.xml im Projekt LeeresProjekt schon vorhanden ist und Sie das Layout nur mit setContentView(R.layout.relativelayout_demo) anzeigen müssen.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">
<Button
android:id="@+id/button_ok"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:text="@string/ok" />
<Button
android:id="@+id/button_cancel"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toStartOf="@id/button_ok"
android:text="@string/cancel" />
<EditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/button_ok"
android:hint="@string/hint"
android:importantForAutofill="no"
android:inputType="text" />
</RelativeLayout>

Listing 5.8    »relativelayout_demo.xml«

Die Grundidee von RelativeLayout ist, die Position von Komponenten in Abhängigkeit zu anderen Elementen zu beschreiben. Beispielsweise sorgt android:layout_toStartOf="@id/button_ok" dafür, dass die Schaltfläche Abbruch in Leserichtung vor (also links neben) OK platziert wird. Ähnliches gilt für android:layout_above="@id/button_ok". Dieser Ausdruck positioniert das Eingabefeld über den beiden Schaltflächen.

android:layout_alignParentBottom="true" legt fest, dass ein Kind am unteren Rand der Elternkomponente auszurichten ist. Um meinen OK-Button in Leserichtung am Ende (also am rechten Rand) zu positionieren, verwende ich android:layout_alignParentEnd="true". Vielleicht fragen Sie sich, warum ich die Benutzeroberfläche von unten nach oben und von rechts nach links beschreibe. Wäre es nicht einfacher, mit EditText zu beginnen und im Anschluss daran die beiden Schaltflächen zu definieren? Grundsätzlich ist dies möglich, allerdings kann Android dann nicht ohne Weiteres berechnen, wie hoch das Eingabefeld werden darf. Oftmals müssen Sie etwas tüfteln, bis Sie das gewünschte Ergebnis mit RelativeLayout erreichen. Meiner Meinung nach ist die geringere Anzahl von Objekten zur Laufzeit diese Mühe aber wert.

 
Zum Seitenanfang

5.1.3    Alternative Layouts Zur vorigen ÜberschriftZur nächsten Überschrift

Die ersten verfügbaren Android-Telefone, das G1 und das Magic, besaßen ein 3,2-Zoll-Display, das aus heutiger Sicht mickrige 320 × 480 Pixel auflöste. Drehte der Benutzer das Gerät oder öffnete er die Hardwaretastatur des G1, wechselte das Seitenverhältnis vom Hochkant- ins Querformat. Diese Funktionalität ist selbstverständlich auch in der aktuellen Android-Version noch enthalten, schließlich profitieren auch Tablets von solchen Orientierungswechseln.

Ihre Apps können mit alternativen Ressourcen darauf reagieren. Ich habe sie in Abschnitt 4.3.3, »Mehrspaltenlayouts«, bereits verwendet, bin Ihnen aber eine ausführliche Erklärung schuldig geblieben. Das hole ich nun nach. Die Kernidee ist, Benutzeroberflächen bei Bedarf in unterschiedlichen Ausprägungen zur Verfügung zu stellen. Hierzu ein Beispiel: Android beinhaltet das Bedienelement DatePicker, mit dem sich sehr schnell eine App bauen lässt, die die Anzahl der Tage zwischen zwei Datumsangaben berechnet. Das Beispielprojekt Datumsdifferenz ist in Abbildung 5.6 dargestellt.

Die Klasse DatumsdifferenzActivity leitet von AppCompatActivity ab. Sie enthält unter anderem zwei mit Calendar.getInstance() erzeugte Objekte des Typs java.util.Calendar. Die Objekte dienen als Modell für zwei android.widget.DatePicker. In onCreate() wird die Benutzeroberfläche geladen und mit setContentView() angezeigt. Die beiden init()-Aufrufe aktualisieren die DatePicker-Anzeigen. Warum ich als letzten Parameter null übergebe, erkläre ich etwas später.

Die App »Datumsdifferenz«

Abbildung 5.6    Die App »Datumsdifferenz«

Klickt der Anwender auf die Schaltfläche Berechnen, überträgt die Methode updateCalendarFromDatePicker() das Datum eines Pickers zurück in das korrespondierende Modell-Objekt. Die Methode berechnen() ermittelt die Datumsdifferenz. Hierzu wird das Startdatum so lange hochgezählt, bis es dem Enddatum entspricht. Damit das klappt, muss das Enddatum dem Startdatum entsprechen oder später sein. Ist das nicht der Fall, vertauscht mein Code die beiden Werte. Aus Performancesicht ist der Algorithmus natürlich verbesserungsfähig, dafür kommt er prima mit Schaltjahren zurecht.

package com.thomaskuenneth.androidbuch.datumsdifferenz

import java.util.Calendar
import android.os.Bundle
import android.widget.Button
import android.widget.DatePicker
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class DatumsdifferenzActivity : AppCompatActivity() {

private var cal1 = Calendar.getInstance()
private var cal2 = Calendar.getInstance()

private lateinit var tv: TextView
private lateinit var dp1: DatePicker
private lateinit var dp2: DatePicker

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.datumsdifferenz)
dp1 = findViewById(R.id.date1)
dp1.init(cal1.get(Calendar.YEAR),
cal1.get(Calendar.MONTH),
cal1.get(Calendar.DAY_OF_MONTH),
null)
dp2 = findViewById(R.id.date2)
dp2.init(cal2.get(Calendar.YEAR),
cal2.get(Calendar.MONTH),
cal2.get(Calendar.DAY_OF_MONTH),
null)
tv = findViewById(R.id.textview_result)
val b = findViewById<Button>(R.id.button_calc)
b.setOnClickListener { berechnen() }
berechnen()
}

private fun berechnen() {
updateCalendarFromDatePicker(cal1, dp1)
updateCalendarFromDatePicker(cal2, dp2)
if (cal2.before(cal1)) {
val temp = cal1
cal1 = cal2
cal2 = temp
}
var days = 0
while (cal1[Calendar.YEAR] != cal2[Calendar.YEAR]
|| cal1[Calendar.MONTH] != cal2[Calendar.MONTH]
|| cal1[Calendar.DAY_OF_MONTH]
!= cal2[Calendar.DAY_OF_MONTH]) {
days += 1
cal1.add(Calendar.DAY_OF_YEAR, 1)
}
tv.text = getString(R.string.template, days)
}

private fun updateCalendarFromDatePicker(cal: Calendar,
dp: DatePicker) {
cal[Calendar.YEAR] = dp.year
cal[Calendar.MONTH] = dp.month
cal[Calendar.DAY_OF_MONTH] = dp.dayOfMonth
}
}

Listing 5.9    Die Klasse »DatumsdifferenzActivity«

Listing 5.10 zeigt die Layoutdatei datumsdifferenz.xml. In einem vertikalen LinearLayout werden zwei DatePicker sowie ein RelativeLayout mit einem Button und einer TextView verteilt. Das Aussehen der Datumsauswahl ist konfigurierbar. Sie schalten mit android:datePickerMode= zwischen einer Kalenderblattdarstellung (calendar) und der Anzeige von Schaltflächen zum Blättern zwischen Tag, Monat und Jahr (spinner) um. android:calendarViewShown="false" ist nötig, um den in dieser Anzeigevariante sonst erscheinenden Monatskalender auszublenden.

<?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:gravity="center"
android:orientation="vertical">
<DatePicker
android:id="@+id/date1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:datePickerMode="spinner" />
<DatePicker
android:id="@+id/date2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:datePickerMode="spinner" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/button_calc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/calc" />
<TextView
android:id="@+id/textview_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/button_calc"
android:layout_marginStart="16dp"
android:layout_toEndOf="@id/button_calc" />
</RelativeLayout>
</LinearLayout>

Listing 5.10    Die Layoutdatei »datumsdifferenz.xml«

Wenn Sie die App im Emulator starten, bringen Sie ihn bitte in den Quermodus, indem Sie eine der beiden Schaltflächen inline image oder inline image der Steuerleiste anklicken. Wie Sie in Abbildung 5.7 sehen, ist das Ergebnis sehr unbefriedigend, da die Benutzeroberfläche nicht mehr vollständig dargestellt wird.

Die App »Datumsdifferenz« im Quermodus

Abbildung 5.7    Die App »Datumsdifferenz« im Quermodus

Um dies zu korrigieren, legen Sie im Werkzeugfenster Project unterhalb von res das Verzeichnis layout-land an. Es nimmt Layoutdateien auf, die angezeigt werden, wenn das Gerät in den Landscape-Modus gebracht wird. Erzeugen Sie in diesem Ordner eine Datei mit dem Namen datumsdifferenz.xml, und übernehmen Sie die folgenden Zeilen. Ein Rechtsklick auf res öffnet ein Kontextmenü, das die beiden Befehle NewFile und NewDirectory enthält. Das Projekt Datumsdifferenz in den Begleitmaterialien enthält das alternative Layout bereits.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<DatePicker
android:id="@+id/date1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:calendarViewShown="false"
android:datePickerMode="spinner" />
<DatePicker
android:id="@+id/date2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="16dp"
android:layout_toEndOf="@id/date1"
android:calendarViewShown="false"
android:datePickerMode="spinner" />
<Button
android:id="@+id/button_calc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/date2"
android:text="@string/calc" />
<TextView
android:id="@+id/textview_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/button_calc"
android:layout_marginStart="16dp"
android:layout_toEndOf="@id/button_calc" />
</RelativeLayout>

Listing 5.11    »datumsdifferenz.xml« für den Landscape-Modus

Haben Sie den Unterschied bemerkt? Die alternative Variante ist kompakter, weil sie nur ein RelativeLayout enthält. Die beiden DatePicker werden durch layout_alignParentStart und layout_toEndOf in Leserichtung nebeneinander angeordnet. Darunter (layout_below) kommen Button und TextView. Der Name der Layoutdatei ist für den Porträt- und Landschaftsmodus gleich (datumsdifferenz.xml). Und das ist auch nötig, weil dieser Name die Grundlage für eine Konstante in der automatisch generierten Klasse R.layout bildet. Diese Konstante wiederum verwenden Sie beispielsweise in setContentView(). Entscheidend ist, ob Sie die Layoutdatei unter layout-land oder layout ablegen. Layouts, die ausdrücklich für den Hochkantmodus entworfen wurden, deponieren Sie in layout-port. Android sucht im Ordner layout, wenn das angeforderte Layout in keinem der genannten Spezialverzeichnisse gefunden wird.

Die Komponente DatePicker kann Ihre App bei einer Änderung des Datums über einen Callback informieren. Das ist praktisch, wenn Sie die Datumsdifferenz nicht nur beim Anklicken der Schaltfläche Berechnen ermitteln möchten. Bitte sehen Sie sich Listing 5.12 an.

val cb = DatePicker.OnDateChangedListener() { _, _, _, _ -> berechnen() }
dp1 = findViewById(R.id.date1)
dp1.init(
cal1.get(Calendar.YEAR),
cal1.get(Calendar.MONTH),
cal1.get(Calendar.DAY_OF_MONTH),
cb
)
dp2 = findViewById(R.id.date2)
dp2.init(
cal2.get(Calendar.YEAR),
cal2.get(Calendar.MONTH),
cal2.get(Calendar.DAY_OF_MONTH),
cb
)

Listing 5.12    Auf Datumsänderungen reagieren

Sie müssen nur das Interface DatePicker.OnDateChangedListener implementieren und als letzten Parameter der DatePicker-Methode init() übergeben. Die ersten drei Werte repräsentieren das vom Picker anzuzeigende Datum. Das Interface enthält die Methode onDateChanged(). Ihr wird der Picker, der den Callback ausgelöst hat, sowie das eingestellte Datum bestehend aus Jahr, Monat (Achtung: wie bei Calendar von 0 bis 11) und Tag übergeben. Da ich in meiner berechnen()-Methode die Werte aus den DatePicker-Objekten auslese, kann ich die Methodenparameter mit _ kennzeichnen. Sie werden nicht verwendet.

Layouts auf Basis der Bildschirmgröße

Neben den layout-Suffixen -land und -port kennt Android eine Reihe weiterer. Einige davon basieren auf der Idee, die App definieren zu lassen, wie viel Platz sie in horizontaler oder vertikaler Richtung mindestens benötigt. Die Größenangaben beziehen sich dabei nicht auf den physikalischen Bildschirm, sondern auf den Bereich, der der Activity tatsächlich zur Verfügung steht. Ob ein Layout für eine bestimmte Bildschirmkonfiguration verwendet wird, ergibt sich aus dem Namen des Verzeichnisses, in dem sich die Layoutdatei befindet. Der Name beginnt wie gewohnt mit layout-, gefolgt von einem der Suffixe aus Tabelle 5.1.

Bildschirmkonfiguration

Suffix

Bedeutung

kleinstmögliche Breite

sw…dp

Breite in geräteunabhängigen Punkten, die mindestens zur Verfügung stehen muss; das Drehen des Bildschirms hat keine Auswirkung.

zur Verfügung stehende Breite

w…dp

Breite in geräteunabhängigen Punkten, die mindestens vorhanden sein muss, damit diese Ressource verwendet wird; dieser Qualifier bietet sich bei der Definition von Mehrspaltenlayouts an.

zur Verfügung stehende Höhe

h…dp

Höhe in geräteunabhängigen Pixeln, die mindestens vorhanden sein muss, damit diese Ressource verwendet wird.

Tabelle 5.1    Zuordnung von Bildschirmkonfigurationen und Layoutdateien

Beispielsweise wird ein Layout, das Sie unter res/layout-w200dp abgelegt haben, immer dann verwendet, wenn einer Activity horizontal mindestens 200 geräteunabhängige Punkte für die Darstellung zur Verfügung stehen. Werte für gängige Gerätegrößen sind:

  • 320 dp für gängige Smartphones

  • 480 dp für kleine Tablets

  • 600 dp für 7-Zoll-Tablets

  • 720 dp für 10-Zoll-Tablets

Mit dem Attribut android:requiresSmallestWidthDp des Tags <supports-screens /> können Sie in der Manifestdatei die kleinstmögliche Breite in geräteunabhängigen Pixeln eintragen, die einer Activity zur Verfügung stehen muss. Ihre App wird dann nur auf Geräten zum Download angeboten, die diese Anforderung erfüllen. Besser ist es natürlich, wenn Ihr Programm ohne solche Einschränkungen funktioniert.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
Zur Rheinwerk-Konferenz für Kotlin
 Buchempfehlungen
Zum Rheinwerk-Shop: Kotlin

Kotlin


Zum Rheinwerk-Shop: Praxisbuch Usability und UX

Praxisbuch Usability und UX


Zum Rheinwerk-Shop: Flutter und Dart

Flutter und Dart


Zum Rheinwerk-Shop: App-Design

App-Design


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2023
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern