Créer une UI avec Glance

Cette page explique comment gérer les tailles et fournir des mises en page flexibles et responsives avec Glance.

Utiliser Box, Column et Row

Glance propose trois mises en page modulables principales:

  • Box: place des éléments sur les autres. Elle se traduit par RelativeLayout.

  • Column: place les éléments les uns après les autres sur l'axe vertical. Elle se traduit par une LinearLayout à orientation verticale.

  • Row: place les éléments les uns après les autres sur l'axe horizontal. Elle se traduit par une LinearLayout à orientation horizontale.

Image d'une mise en page à colonnes, lignes et cases.
Figure 1. Exemples de mises en page avec "Column", "Row" et "Box".

Chacun de ces composables vous permet de définir les alignements vertical et horizontal de son contenu, ainsi que les contraintes de largeur, de hauteur, de poids ou de marge intérieure à l'aide de modificateurs. En outre, chaque enfant peut définir son modificateur pour modifier l'espace et l'emplacement à l'intérieur du parent.

L'exemple suivant montre comment créer un Row qui répartit uniformément ses enfants horizontalement, comme illustré dans la figure 1:

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

Row remplit la largeur maximale disponible et, comme chaque enfant ayant le même poids, partage uniformément l'espace disponible. Vous pouvez définir différentes épaisseurs, tailles, marges intérieures ou alignements pour adapter les mises en page à vos besoins.

Utiliser des mises en page déroulantes

Une autre façon de fournir du contenu responsif consiste à le faire défiler. Cela est possible avec le composable LazyColumn. Ce composable vous permet de définir un ensemble d'éléments à afficher dans un conteneur à faire défiler dans le widget d'application.

Les extraits de code suivants montrent différentes façons de définir des éléments dans LazyColumn.

Vous pouvez indiquer le nombre d'éléments:

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

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

Fournissez des éléments individuels:

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

Fournissez une liste ou un tableau d'éléments:

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

Vous pouvez également combiner les exemples précédents:

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")
    }
}

Notez que l'extrait précédent ne spécifie pas l'itemId. La spécification de itemId permet d'améliorer les performances et de maintenir la position de défilement de la liste, ainsi que les mises à jour de appWidget à partir d'Android 12 (par exemple, lors de l'ajout ou de la suppression d'éléments dans la liste). L'exemple suivant montre comment spécifier un itemId:

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

Définir SizeMode

La taille des AppWidget peut varier selon l'appareil, le choix de l'utilisateur ou le lanceur d'applications. Il est donc important de fournir des mises en page flexibles, comme décrit sur la page Fournir des mises en page de widget flexibles. Glance simplifie ce processus avec la définition SizeMode et la valeur LocalSize. Les sections suivantes décrivent les trois modes.

SizeMode.Single

SizeMode.Single est le mode par défaut. Elle indique qu'un seul type de contenu est fourni. Autrement dit, même si la taille disponible de AppWidget change, la taille du contenu n'est pas modifiée.

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
        // ...
    }
}

Lorsque vous utilisez ce mode, vérifiez les points suivants:

  • Les valeurs de métadonnées de taille minimale et maximale sont correctement définies en fonction de la taille du contenu.
  • Le contenu est suffisamment flexible dans la plage de tailles attendue.

En règle générale, vous devez utiliser ce mode dans les cas suivants:

a) le AppWidget a une taille fixe, ou b) il ne modifie pas son contenu lorsqu'il est redimensionné.

SizeMode.Responsive

Ce mode équivaut à fournir des mises en page responsives, ce qui permet à GlanceAppWidget de définir un ensemble de mises en page responsives limitées par des tailles spécifiques. Pour chaque taille définie, le contenu est créé et mappé à la taille spécifique lors de la création ou de la mise à jour de AppWidget. Le système sélectionne ensuite celui qui est le mieux adapté en fonction de la taille disponible.

Par exemple, dans notre destination AppWidget, vous pouvez définir trois tailles et son contenu:

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")
            }
        }
    }
}

Dans l'exemple précédent, la méthode provideContent est appelée trois fois et mappée sur la taille définie.

  • Dans le premier appel, la taille est évaluée à 100x100. Le contenu n'inclut pas le bouton supplémentaire, ni les textes en haut et en bas.
  • Dans le deuxième appel, la taille est évaluée à 250x100. Le contenu inclut le bouton supplémentaire, mais pas les textes en haut et en bas.
  • Dans le troisième appel, la taille est évaluée à 250x250. Le contenu comprend le bouton supplémentaire et les deux textes.

SizeMode.Responsive est une combinaison des deux autres modes et vous permet de définir du contenu responsif dans des limites prédéfinies. En général, ce mode fonctionne mieux et permet des transitions plus fluides lorsque AppWidget est redimensionné.

Le tableau suivant indique la valeur de la taille en fonction de la taille disponible pour SizeMode et pour AppWidget:

Taille disponible 105 x 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 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Responsive 80 x 100 80 x 100 80 x 100 150 x 120
* Les valeurs exactes ne sont fournies qu'à titre de démonstration.

SizeMode.Exact

SizeMode.Exact équivaut à fournir des mises en page exactes, qui demande le contenu GlanceAppWidget chaque fois que la taille de AppWidget disponible change (par exemple, lorsque l'utilisateur redimensionne la AppWidget sur l'écran d'accueil).

Par exemple, dans le widget de destination, un bouton supplémentaire peut être ajouté si la largeur disponible est supérieure à une certaine valeur.

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")
                }
            }
        }
    }
}

Ce mode offre plus de flexibilité que les autres, mais il comporte quelques mises en garde:

  • La AppWidget doit être complètement recréée chaque fois que la taille change. Cela peut entraîner des problèmes de performances et des sauts d'interface utilisateur lorsque le contenu est complexe.
  • La taille disponible peut varier selon l'implémentation du lanceur d'applications. Par exemple, si le lanceur ne fournit pas la liste des tailles, la taille minimale possible est utilisée.
  • Sur les appareils antérieurs à Android 12, la logique de calcul de la taille peut ne pas fonctionner dans toutes les situations.

En général, vous devez utiliser ce mode si SizeMode.Responsive ne peut pas être utilisé (c'est-à-dire s'il n'est pas possible d'utiliser un petit ensemble de mises en page responsives).

Accéder aux ressources

Utilisez LocalContext.current pour accéder à n'importe quelle ressource Android, comme illustré dans l'exemple suivant:

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

Nous vous recommandons de fournir directement les ID de ressource afin de réduire la taille de l'objet RemoteViews final et d'activer les ressources dynamiques, telles que les couleurs dynamiques.

Les composables et les méthodes acceptent les ressources à l'aide d'un "fournisseur", tel que ImageProvider, ou d'une méthode de surcharge telle que GlanceModifier.background(R.color.blue). Par exemple :

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

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

Ajouter des boutons composés

Les boutons composés ont été introduits sur Android 12. Glance est compatible avec la rétrocompatibilité pour les types de boutons composés suivants:

Ces boutons composés affichent chacun une vue cliquable qui représente l'état "coché".

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"
)

Lorsque l'état change, le lambda fourni est déclenché. Vous pouvez stocker l'état de la vérification, comme illustré dans l'exemple suivant:

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) }
        )
    }
}

Vous pouvez également fournir l'attribut colors à CheckBox, Switch et RadioButton pour personnaliser leurs couleurs:

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)
    ),

)