UI mit Glance erstellen

Auf dieser Seite wird beschrieben, wie Sie mit Glance Größen handhaben und flexible und responsive Layouts bereitstellen.

Box, Column und Row verwenden

Glance hat drei zusammensetzbare Hauptlayouts:

  • Box: Setzt Elemente übereinander. Er wird mit RelativeLayout übersetzt.

  • Column: Platziert Elemente in der vertikalen Achse hintereinander. Es wird in ein LinearLayout-Objekt mit vertikaler Ausrichtung umgewandelt.

  • Row: Platziert Elemente in der horizontalen Achse hintereinander. Es wird in ein LinearLayout-Objekt mit horizontaler Ausrichtung umgewandelt.

Bild eines Spalten-, Zeilen- und Feldlayouts.
Abbildung 1. Beispiele für Layouts mit „Column“ (Spalte), „Row“ (Zeile) und „Box“ (Feld).

Mit jeder dieser zusammensetzbaren Funktionen können Sie die vertikale und horizontale Ausrichtung des Inhalts sowie die Einschränkungen für Breite, Höhe, Gewicht und Abstände mithilfe von Modifizierern definieren. Darüber hinaus kann jedes untergeordnete Element seinen Modifikator definieren, um den Abstand und die Platzierung im übergeordneten Element zu ändern.

Das folgende Beispiel zeigt, wie Sie einen Row erstellen, der seine untergeordneten Elemente gleichmäßig horizontal verteilt, wie in Abbildung 1 dargestellt:

Row(modifier = GlanceModifier.fillMaxWidth().padding(16.dp)) {
    val modifier = GlanceModifier.defaultWeight()
    Text("first", modifier)
    Text("second", modifier)
    Text("third", modifier)
}

Der Row füllt die maximal verfügbare Breite aus. Da alle untergeordneten Elemente dieselbe Gewichtung haben, teilen sie sich den verfügbaren Platz gleichmäßig auf. Sie können verschiedene Stärken, Größen, Abstände oder Ausrichtungen definieren, um Layouts an Ihre Anforderungen anzupassen.

Scrollbare Layouts verwenden

Eine weitere Möglichkeit, responsive Inhalte bereitzustellen, besteht darin, sie scrollbar zu machen. Mit der zusammensetzbaren Funktion LazyColumn ist dies möglich. Mit dieser zusammensetzbaren Funktion können Sie eine Reihe von Elementen definieren, die in einem scrollbaren Container im App-Widget angezeigt werden sollen.

Die folgenden Snippets zeigen verschiedene Möglichkeiten zum Definieren von Elementen in LazyColumn.

Sie können die Anzahl der Elemente angeben:

// Remember to import Glance Composables
// import androidx.glance.appwidget.layout.LazyColumn

LazyColumn {
    items(10) { index: Int ->
        Text(
            text = "Item $index",
            modifier = GlanceModifier.fillMaxWidth()
        )
    }
}

Einzelne Elemente bereitstellen:

LazyColumn {
    item {
        Text("First Item")
    }
    item {
        Text("Second Item")
    }
}

Geben Sie eine Liste oder ein Array mit Elementen an:

LazyColumn {
    items(peopleNameList) { name ->
        Text(name)
    }
}

Sie können auch eine Kombination der vorherigen Beispiele verwenden:

LazyColumn {
    item {
        Text("Names:")
    }
    items(peopleNameList) { name ->
        Text(name)
    }

    // or in case you need the index:
    itemsIndexed(peopleNameList) { index, person ->
        Text("$person at index $index")
    }
}

itemId ist im vorherigen Snippet nicht angegeben. Durch Angabe von itemId wird die Leistung verbessert und die Scrollposition durch die Liste und die appWidget-Updates ab Android 12 beibehalten (z. B. beim Hinzufügen oder Entfernen von Elementen in der Liste). Das folgende Beispiel zeigt, wie ein itemId angegeben wird:

items(items = peopleList, key = { person -> person.id }) { person ->
    Text(person.name)
}

SizeMode definieren

Die Größen von AppWidget können sich je nach Gerät, Auswahl des Nutzers oder Launcher unterscheiden. Daher ist es wichtig, flexible Layouts wie auf der Seite Flexible Widget-Layouts bereitstellen beschrieben bereitzustellen. Glance vereinfacht dies mit der SizeMode-Definition und dem LocalSize-Wert. In den folgenden Abschnitten werden die drei Modi beschrieben.

SizeMode.Single

SizeMode.Single ist der Standardmodus. Damit wird angegeben, dass nur ein Inhaltstyp angegeben ist. Das heißt, selbst wenn sich die verfügbare Größe für AppWidget ändert, ändert sich die Größe des Inhalts nicht.

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Single

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the minimum size or resizable
        // size defined in the App Widget metadata
        val size = LocalSize.current
        // ...
    }
}

Stellen Sie bei der Verwendung dieses Modus Folgendes sicher:

  • Die Metadatenwerte mit Mindest- und Höchstgröße werden anhand der Inhaltsgröße korrekt definiert.
  • Der Inhalt ist innerhalb des erwarteten Größenbereichs flexibel genug.

Sie sollten diesen Modus in der Regel in folgenden Fällen verwenden:

a) das AppWidget eine feste Größe hat oder b) es bei der Größenanpassung seinen Inhalt nicht ändert.

SizeMode.Responsive

Dieser Modus entspricht der Bereitstellung von responsiven Layouts, bei denen mit dem GlanceAppWidget eine Reihe von responsiven Layouts definiert werden kann, die durch bestimmte Größen begrenzt sind. Für jede definierte Größe wird der Inhalt erstellt und der jeweiligen Größe zugeordnet, wenn das AppWidget erstellt oder aktualisiert wird. Das System wählt dann anhand der verfügbaren Größe die passendste aus.

In unserem Ziel AppWidget können Sie beispielsweise drei Größen und deren Inhalt definieren:

class MyAppWidget : GlanceAppWidget() {

    companion object {
        private val SMALL_SQUARE = DpSize(100.dp, 100.dp)
        private val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
        private val BIG_SQUARE = DpSize(250.dp, 250.dp)
    }

    override val sizeMode = SizeMode.Responsive(
        setOf(
            SMALL_SQUARE,
            HORIZONTAL_RECTANGLE,
            BIG_SQUARE
        )
    )

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be one of the sizes defined above.
        val size = LocalSize.current
        Column {
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            }
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width >= HORIZONTAL_RECTANGLE.width) {
                    Button("School")
                }
            }
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "provided by X")
            }
        }
    }
}

Im vorherigen Beispiel wird die Methode provideContent dreimal aufgerufen und der definierten Größe zugeordnet.

  • Im ersten Aufruf wird als Größe 100x100 ausgewertet. Zu den Inhalten gehören weder die zusätzliche Schaltfläche noch der obere und untere Text.
  • Im zweiten Aufruf wird als Größe 250x100 ausgewertet. Der Inhalt enthält die zusätzliche Schaltfläche, aber nicht den oberen und unteren Text.
  • Im dritten Aufruf wird die Größe 250x250 ausgewertet. Der Inhalt enthält die zusätzliche Schaltfläche und beide Texte.

SizeMode.Responsive ist eine Kombination der beiden anderen Modi. Damit können Sie responsive Inhalte innerhalb vordefinierter Grenzen definieren. Dieser Modus ist in der Regel besser und ermöglicht flüssigere Übergänge, wenn die Größe von AppWidget angepasst wird.

Die folgende Tabelle zeigt den Wert der Größe abhängig von den verfügbaren Größen für SizeMode und AppWidget:

Verfügbare Größe 105 × 110 203 x 112 72 x 72 203 x 150
SizeMode.Single 110 x 110 110 x 110 110 x 110 110 x 110
SizeMode.Exact 105 × 110 203 x 112 72 x 72 203 x 150
SizeMode.Responsive 80 x 100 80 x 100 80 x 100 150 x 120
* Die genauen Werte dienen nur zu Demozwecken.

SizeMode.Exact

SizeMode.Exact entspricht der Bereitstellung exakter Layouts, bei denen der GlanceAppWidget-Inhalt jedes Mal angefordert wird, wenn sich die verfügbare AppWidget-Größe ändert (z. B. wenn der Nutzer die Größe von AppWidget auf dem Startbildschirm ändert).

Im Ziel-Widget kann beispielsweise eine zusätzliche Schaltfläche hinzugefügt werden, wenn die verfügbare Breite größer als ein bestimmter Wert ist.

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Exact

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the size of the AppWidget
        val size = LocalSize.current
        Column {
            Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width > 250.dp) {
                    Button("School")
                }
            }
        }
    }
}

Dieser Modus bietet mehr Flexibilität als die anderen, birgt jedoch auch einige Nachteile:

  • Der AppWidget muss bei jeder Größenänderung vollständig neu erstellt werden. Dies kann bei komplexen Inhalten zu Leistungsproblemen und Sprüngen in der Benutzeroberfläche führen.
  • Die verfügbare Größe kann je nach Implementierung des Launchers variieren. Wenn im Launcher beispielsweise keine Liste der Größen enthalten ist, wird die geringstmögliche Größe verwendet.
  • Auf Geräten, die älter als Android 12 sind, funktioniert die Logik zur Größenberechnung möglicherweise nicht in allen Situationen.

Sie sollten diesen Modus im Allgemeinen verwenden, wenn SizeMode.Responsive nicht verwendet werden kann (d. h. eine kleine Gruppe responsiver Layouts nicht möglich ist).

Auf Ressourcen zugreifen

Verwenden Sie LocalContext.current, um auf eine Android-Ressource zuzugreifen, wie im folgenden Beispiel gezeigt:

LocalContext.current.getString(R.string.glance_title)

Wir empfehlen, Ressourcen-IDs direkt anzugeben, um die Größe des endgültigen RemoteViews-Objekts zu verringern und dynamische Ressourcen wie dynamische Farben zu aktivieren.

Zusammensetzbare Funktionen und Methoden akzeptieren Ressourcen, die einen „Anbieter“ wie ImageProvider oder eine Überlastmethode wie GlanceModifier.background(R.color.blue) verwenden. Beispiele:

Column(
    modifier = GlanceModifier.background(R.color.default_widget_background)
) { /**...*/ }

Image(
    provider = ImageProvider(R.drawable.ic_logo),
    contentDescription = "My image",
)

Verschiedene Schaltflächen hinzufügen

Zusammenfassende Schaltflächen wurden in Android 12 eingeführt. Glance unterstützt die Abwärtskompatibilität für die folgenden Arten von zusammengesetzten Schaltflächen:

Diese zusammengesetzten Schaltflächen zeigen jeweils eine anklickbare Ansicht für den Status "Aktiviert" an.

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableStateOf(0) }

CheckBox(
    checked = isApplesChecked,
    onCheckedChange = { isApplesChecked = !isApplesChecked },
    text = "Apples"
)

Switch(
    checked = isEnabledSwitched,
    onCheckedChange = { isEnabledSwitched = !isEnabledSwitched },
    text = "Enabled"
)

RadioButton(
    checked = isRadioChecked == 1,
    onClick = { isRadioChecked = 1 },
    text = "Checked"
)

Wenn sich der Status ändert, wird das angegebene Lambda ausgelöst. Sie können den Prüfstatus wie im folgenden Beispiel speichern:

class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val myRepository = MyRepository.getInstance()

        provideContent {
            val scope = rememberCoroutineScope()

            val saveApple: (Boolean) -> Unit =
                { scope.launch { myRepository.saveApple(it) } }
            MyContent(saveApple)
        }
    }

    @Composable
    private fun MyContent(saveApple: (Boolean) -> Unit) {

        var isAppleChecked by remember { mutableStateOf(false) }

        Button(
            text = "Save",
            onClick = { saveApple(isAppleChecked) }
        )
    }
}

Du kannst das Attribut colors auch für CheckBox, Switch und RadioButton angeben, um deren Farben anzupassen:

CheckBox(
    // ...
    colors = CheckboxDefaults.colors(
        checkedColor = ColorProvider(day = colorAccentDay, night = colorAccentNight),
        uncheckedColor = ColorProvider(day = Color.DarkGray, night = Color.LightGray)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked }
)

Switch(
    // ...
    colors = SwitchDefaults.colors(
        checkedThumbColor = ColorProvider(day = Color.Red, night = Color.Cyan),
        uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Magenta),
        checkedTrackColor = ColorProvider(day = Color.Blue, night = Color.Yellow),
        uncheckedTrackColor = ColorProvider(day = Color.Magenta, night = Color.Green)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked },
    text = "Enabled"
)

RadioButton(
    // ...
    colors = RadioButtonDefaults.colors(
        checkedColor = ColorProvider(day = Color.Cyan, night = Color.Yellow),
        uncheckedColor = ColorProvider(day = Color.Red, night = Color.Blue)
    ),

)