Grundlagen zum Verfassen von Layouts

Mit Jetpack Compose lässt sich die Benutzeroberfläche Ihrer App viel einfacher gestalten und entwickeln. Compose wandelt den Status in UI-Elemente um. Dazu werden folgende Schritte ausgeführt:

  1. Zusammensetzung von Elementen
  2. Layout von Elementen
  3. Zeichnen von Elementen

Zustand über Komposition, Layout und Zeichnen in UI umwandeln

In diesem Dokument geht es um das Layout von Elementen. Es werden einige der Bausteine erläutert, die Compose für das Layout von UI-Elementen bietet.

Ziele von Layouts in Compose

Die Jetpack Compose-Implementierung des Layoutsystems hat zwei Hauptziele:

Grundlagen von komponierbaren Funktionen

Composable-Funktionen sind der grundlegende Baustein von Compose. Eine zusammensetzbare Funktion ist eine Funktion, die Unit ausgibt, das einen Teil Ihrer Benutzeroberfläche beschreibt. Die Funktion nimmt einige Eingaben entgegen und generiert, was auf dem Bildschirm angezeigt wird. Weitere Informationen zu Composables finden Sie in der Dokumentation zum Compose-Konzeptmodell.

Eine zusammensetzbare Funktion kann mehrere UI-Elemente ausgeben. Wenn Sie jedoch keine Anleitung dazu geben, wie sie angeordnet werden sollen, kann es sein, dass Compose die Elemente auf eine Weise anordnet, die Ihnen nicht gefällt. Mit diesem Code werden beispielsweise zwei Textelemente generiert:

@Composable
fun ArtistCard() {
    Text("Alfred Sisley")
    Text("3 minutes ago")
}

Ohne Angaben dazu, wie die Elemente angeordnet werden sollen, stapelt Compose die Textelemente übereinander, sodass sie nicht lesbar sind:

Zwei übereinander gezeichnete Textelemente, die den Text unleserlich machen

Compose bietet eine Sammlung von gebrauchsfertigen Layouts, mit denen Sie Ihre UI-Elemente anordnen können. Außerdem lassen sich damit ganz einfach eigene, speziellere Layouts definieren.

Standardmäßige Layoutkomponenten

In vielen Fällen können Sie einfach die Standard-Layoutelemente von Compose verwenden.

Mit Column können Sie Elemente vertikal auf dem Bildschirm platzieren.

@Composable
fun ArtistCardColumn() {
    Column {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }
}

Zwei Textelemente in einem Spaltenlayout, damit der Text lesbar ist

Verwenden Sie Row, um Elemente horizontal auf dem Bildschirm zu platzieren. Sowohl Column als auch Row unterstützen die Konfiguration der Ausrichtung der darin enthaltenen Elemente.

@Composable
fun ArtistCardRow(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

Zeigt ein komplexeres Layout mit einer kleinen Grafik neben einer Spalte mit Textelementen

Mit Box können Sie Elemente übereinander platzieren. In Box kann auch die Ausrichtung der darin enthaltenen Elemente konfiguriert werden.

@Composable
fun ArtistAvatar(artist: Artist) {
    Box {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Icon(Icons.Filled.Check, contentDescription = "Check mark")
    }
}

Zwei übereinander gestapelte Elemente

Oft sind diese Bausteine alles, was Sie brauchen. Sie können eine eigene zusammensetzbare Funktion schreiben, um diese Layouts in einem komplexeren Layout zu kombinieren, das zu Ihrer App passt.

Vergleicht drei einfache Layout-Composables: „column“, „row“ und „box“

Um die Position von untergeordneten Elementen in einem Row festzulegen, verwenden Sie die Argumente horizontalArrangement und verticalAlignment. Legen Sie für ein Column die Argumente verticalArrangement und horizontalAlignment fest:

@Composable
fun ArtistCardArrangement(artist: Artist) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.End
    ) {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Column { /*...*/ }
    }
}

Elemente sind rechtsbündig ausgerichtet

Das Layoutmodell

Im Layoutmodell wird der UI-Baum in einem einzigen Durchgang angeordnet. Jeder Knoten wird zuerst aufgefordert, sich selbst zu messen, und dann alle untergeordneten Elemente rekursiv zu messen. Dabei werden Größenbeschränkungen im Baum an die untergeordneten Elemente weitergegeben. Anschließend werden die Blattknoten in der Größe angepasst und platziert. Die aufgelösten Größen und Platzierungsanweisungen werden dann wieder nach oben im Baum weitergegeben.

Kurz gesagt: Eltern werden vor ihren Kindern gemessen, aber nach ihnen in Größe und Position angepasst.

Betrachten Sie die folgende SearchResult-Funktion.

@Composable
fun SearchResult() {
    Row {
        Image(
            // ...
        )
        Column {
            Text(
                // ...
            )
            Text(
                // ...
            )
        }
    }
}

Diese Funktion ergibt den folgenden UI-Baum.

SearchResult
  Row
    Image
    Column
      Text
      Text

Im Beispiel SearchResult folgt das Layout des UI-Baums dieser Reihenfolge:

  1. Der Stammknoten Row wird aufgefordert, eine Messung vorzunehmen.
  2. Der Stammknoten Row fordert sein erstes untergeordnetes Element Image auf, die Messung durchzuführen.
  3. Image ist ein Blattknoten (d. h. er hat keine untergeordneten Elemente). Daher wird eine Größe gemeldet und es werden Placement-Anweisungen zurückgegeben.
  4. Der Stammknoten Row fordert seinen zweiten untergeordneten Knoten Column auf, die Messung durchzuführen.
  5. Der Column-Knoten fordert sein erstes Text-untergeordnetes Element auf, eine Messung durchzuführen.
  6. Der erste Text-Knoten ist ein Blattknoten. Er gibt also eine Größe an und gibt Platzierungsanweisungen zurück.
  7. Der Knoten Column fordert seinen zweiten untergeordneten Knoten Text auf, die Messung durchzuführen.
  8. Der zweite Text-Knoten ist ein Blattknoten. Er gibt also eine Größe an und gibt Platzierungsanweisungen zurück.
  9. Nachdem der Column-Knoten seine untergeordneten Elemente gemessen, in der Größe angepasst und platziert hat, kann er seine eigene Größe und Platzierung bestimmen.
  10. Nachdem der Stammknoten Row seine untergeordneten Elemente gemessen, in der Größe angepasst und platziert hat, kann er seine eigene Größe und Platzierung festlegen.

Reihenfolge von Messung, Größenanpassung und Platzierung im UI-Baum der Suchergebnisse

Leistung

Compose erzielt eine hohe Leistung, indem Kinder nur einmal gemessen werden. Die Single-Pass-Messung ist gut für die Leistung, da Compose tiefe UI-Bäume effizient verarbeiten kann. Wenn ein Element sein untergeordnetes Element zweimal gemessen hat und dieses untergeordnete Element jedes seiner untergeordneten Elemente zweimal gemessen hat usw., müsste bei einem einzelnen Versuch, das gesamte UI zu gestalten, viel Arbeit geleistet werden, was es schwierig macht, die Leistung Ihrer App aufrechtzuerhalten.

Wenn für Ihr Layout aus irgendeinem Grund mehrere Messungen erforderlich sind, bietet Compose ein spezielles System, intrinsische Messungen. Weitere Informationen zu dieser Funktion finden Sie hier.

Da Analyse und Platzierung separate Unterphasen des Layout-Durchlaufs sind, können Änderungen, die sich nur auf die Platzierung von Elementen und nicht auf die Analyse auswirken, separat ausgeführt werden.

Modifizierer in Layouts verwenden

Wie im Abschnitt Compose-Modifikatoren beschrieben, können Sie Modifikatoren verwenden, um Ihre Composables zu dekorieren oder zu erweitern. Modifikatoren sind für die Anpassung des Layouts unerlässlich. Hier werden beispielsweise mehrere Modifizierer verkettet, um die ArtistCard anzupassen:

@Composable
fun ArtistCardModifiers(
    artist: Artist,
    onClick: () -> Unit
) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
        Spacer(Modifier.size(padding))
        Card(
            elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
        ) { /*...*/ }
    }
}

Ein noch komplexeres Layout, bei dem Modifikatoren verwendet werden, um die Anordnung der Grafiken zu ändern und festzulegen, welche Bereiche auf Nutzereingaben reagieren

Im obigen Code werden verschiedene Modifikatorfunktionen zusammen verwendet.

  • clickable reagiert auf Nutzereingaben und zeigt einen Ripple-Effekt an.
  • Mit padding wird um ein Element herum Platz geschaffen.
  • Mit fillMaxWidth wird festgelegt, dass das Composable die maximale Breite einnimmt, die ihm vom übergeordneten Element zugewiesen wird.
  • Mit size() wird die bevorzugte Breite und Höhe eines Elements angegeben.

Scrollbare Layouts

Weitere Informationen zu scrollbaren Layouts finden Sie in der Dokumentation zu Compose-Gesten.

Informationen zu Listen und Lazy Lists finden Sie in der Dokumentation zu Compose-Listen.

Responsive Layouts

Ein Layout sollte unter Berücksichtigung verschiedener Bildschirmausrichtungen und Formfaktoren entworfen werden. Compose bietet standardmäßig einige Mechanismen, mit denen Sie Ihre zusammensetzbaren Layouts an verschiedene Bildschirmkonfigurationen anpassen können.

Einschränkungen

Um die Einschränkungen des übergeordneten Elements zu kennen und das Layout entsprechend zu gestalten, können Sie ein BoxWithConstraints verwenden. Die Messbeschränkungen finden Sie im Umfang der Content-Lambda. Mit diesen Messbeschränkungen können Sie verschiedene Layouts für unterschiedliche Bildschirmkonfigurationen erstellen:

@Composable
fun WithConstraintsComposable() {
    BoxWithConstraints {
        Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
    }
}

Slotbasierte Layouts

Compose bietet eine Vielzahl von Composables, die auf Material Design basieren. Die androidx.compose.material:material-Abhängigkeit (die beim Erstellen eines Compose-Projekts in Android Studio enthalten ist) erleichtert die Erstellung von Benutzeroberflächen. Elemente wie Drawer, FloatingActionButton und TopAppBar sind alle vorhanden.

Material-Komponenten nutzen Slot-APIs, ein Muster, das in Compose eingeführt wurde, um Composables eine zusätzliche Anpassungsebene zu ermöglichen. Dieser Ansatz macht Komponenten flexibler, da sie ein untergeordnetes Element akzeptieren, das sich selbst konfigurieren kann, anstatt jeden Konfigurationsparameter des untergeordneten Elements verfügbar zu machen. Durch Slots wird in der Benutzeroberfläche ein leerer Bereich geschaffen, den der Entwickler nach Belieben füllen kann. Hier sind die Slots, die Sie in einem TopAppBar anpassen können:

Ein Diagramm, das die verfügbaren Slots in einer App-Leiste für Material-Komponenten zeigt

Composables verwenden in der Regel eine content-Composable-Lambda-Funktion ( content: @Composable () -> Unit). Slot-APIs stellen mehrere content-Parameter für bestimmte Zwecke zur Verfügung. Mit TopAppBar können Sie beispielsweise die Inhalte für title, navigationIcon und actions bereitstellen.

Mit Scaffold können Sie beispielsweise eine Benutzeroberfläche mit der grundlegenden Material Design-Layoutstruktur implementieren. Scaffold bietet Slots für die gängigsten Material-Komponenten der obersten Ebene, z. B. TopAppBar, BottomAppBar, FloatingActionButton und Drawer. Mit Scaffold lässt sich ganz einfach prüfen, ob diese Komponenten richtig positioniert sind und korrekt zusammenarbeiten.

Die JetNews-Beispiel-App, in der mit „Scaffold“ mehrere Elemente positioniert werden

@Composable
fun HomeScreen(/*...*/) {
    ModalNavigationDrawer(drawerContent = { /* ... */ }) {
        Scaffold(
            topBar = { /*...*/ }
        ) { contentPadding ->
            // ...
        }
    }
}