Créer une UI avec Glance

Cette page explique comment gérer les tailles et fournir des mises en page flexibles et adaptatives avec Glance, à l'aide des composants Glance existants.

Utiliser Box, Column et Row

Glance comporte trois mises en page composables principales :

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

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

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

Glance est compatible avec les objets Scaffold. Placez vos composables Column, Row et Box dans un objet Scaffold donné.

Disposition en colonnes, en lignes et en boîtes.
Figure 1. Exemples de mises en page avec Column, Row et Box.

Chacun de ces composables vous permet de définir les alignements verticaux et horizontaux de son contenu, ainsi que les contraintes de largeur, de hauteur, de pondération ou de marge intérieure à l'aide de modificateurs. De plus, chaque enfant peut définir son modificateur pour modifier l'espace et le placement à l'intérieur du parent.

L'exemple suivant montre comment créer une Row qui distribue ses enfants de manière uniforme sur l'axe horizontal, 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)
}

La Row remplit la largeur maximale disponible et, comme chaque enfant a la même pondération, ils partagent l'espace disponible de manière uniforme. Vous pouvez définir différentes pondérations, tailles, marges intérieures ou alignements pour adapter les mises en page à vos besoins.

Utiliser des mises en page avec défilement

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

Les extraits de code suivants montrent différentes façons de définir des éléments dans le 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()
        )
    }
}

Indiquez des éléments individuels :

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

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

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

Vous pouvez également utiliser une combinaison des 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 de code précédent ne spécifie pas l'itemId. Spécifier l'itemId permet d'améliorer les performances et de conserver la position de défilement lors des mises à jour de la liste et de l'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, itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

Défilement instantané

Le défilement instantané est une animation qui permet au contenu défilable de s'aligner en haut du conteneur de widget.

Vidéo 1. À gauche, un élément de liste ne s'aligne pas lors du défilement , tandis qu'à droite, il s'aligne.


Pour implémenter le défilement instantané, assurez-vous de remplir les conditions suivantes :

  • Mettez à jour votre dépendance Glance vers la version 1.3.0-alpha02 ou une version ultérieure.
  • Définissez votre compileSdk sur 37 ou une version ultérieure, car le défilement instantané est compatible avec les appareils équipés d'Android 17 et versions ultérieures.
  • Configurez votre LazyColumn avec VerticalScrollMode. Si l'appareil est compatible avec le défilement instantané, utilisez SnapScrollMatchHeight. Sinon, utilisez Normal.

Si vous utilisez le défilement instantané avec des images, consultez la mise en page canonique des images à fond perdu full bleed image.

@Composable
fun SnapScrollLayout() {
    val height = LocalSize.current.height
    val items = listOf(
        ColorItem(Color.Red, "Red"),
        ColorItem(Color.Yellow, "Yellow"),
        ColorItem(Color.Blue, "Blue")
    )

    val scrollMode = if (Build.VERSION.SDK_INT >= 37) {
        VerticalScrollMode.SnapScrollMatchHeight(height)
    } else {
        VerticalScrollMode.Normal
    }

    LazyColumn(
        verticalScrollMode = scrollMode
    ) {
        items(items) { item ->
            ColorCard(item, height)
        }
    }
}

@Composable
private fun ColorCard(item: ColorItem, height: Dp) {
    Box(
        modifier = GlanceModifier
            .background(item.color)
            .fillMaxWidth()
            .height(height),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = item.name,
            modifier = GlanceModifier.background(Color.White)
        )
    }
}

Définir le SizeMode

Les tailles de AppWidget peuvent varier en fonction de l'appareil, du choix utilisateur ou du 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 cette opération 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. Il indique qu'un seul type de contenu est fourni. Autrement dit, même si la taille disponible de l'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, assurez-vous que :

En général, vous devez utiliser ce mode lorsque :

a) l'AppWidget a une taille fixe ou b) son contenu ne change pas lorsqu'il est redimensionné.

SizeMode.Responsive

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

Par exemple, dans notre AppWidget de destination, vous pouvez définir trois tailles et leur 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 à la taille définie.

  • Lors du 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.
  • Lors du 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.
  • Lors du troisième appel, la taille est évaluée à 250x250. Le contenu inclut 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 adaptatif dans des limites prédéfinies. En général, ce mode est plus performant et permet des transitions plus fluides lorsque l'AppWidget est redimensionné.

Le tableau suivant indique la valeur de la taille, en fonction du SizeMode et de la taille disponible de l'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 sont fournies à des fins de démonstration uniquement.

SizeMode.Exact

SizeMode.Exact équivaut à fournir des mises en page exactes, ce qui demande le contenu GlanceAppWidget chaque fois que la taille disponible de l'AppWidget change (par exemple, lorsque l'utilisateur redimensionne l'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 présente quelques inconvénients :

  • L'AppWidget doit être entièrement recréé chaque fois que la taille change. Cela peut entraîner des problèmes de performances et des sauts d'UI lorsque le contenu est complexe.
  • La taille disponible peut varier en fonction de l'implémentation du lanceur. 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 qu'un petit ensemble de mises en page adaptatives n'est pas réalisable).

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 des ID de ressource pour réduire la taille de l'objet final RemoteViews et 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 à l'aide d'une méthode de surcharge comme GlanceModifier.background(R.color.blue). Exemple :

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

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

Gérer le texte

Glance 1.1.0 inclut une API permettant de définir vos styles de texte. Définissez les styles de texte à l'aide des attributs fontSize, fontWeight ou fontFamily de la classe TextStyle.

fontFamily est compatible avec toutes les polices système, comme illustré dans l'exemple suivant, mais les polices personnalisées dans les applications ne sont pas compatibles :

Text(
    style = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
        fontFamily = FontFamily.Monospace
    ),
    text = "Example Text"
)

Ajouter des boutons composés

Les boutons composés ont été introduits dans Android 12. Glance est rétrocompatible avec les types de boutons composés suivants :

Chacun de ces boutons composés affiche 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 { mutableIntStateOf(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 coché, 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)
    ),

)

Composants supplémentaires

Glance 1.1.0 inclut la publication de composants supplémentaires, comme décrit dans le tableau suivant :

Nom Image Lien de référence Remarques supplémentaires
Bouton rempli alt_text Component
Boutons avec contours alt_text Component
Boutons d'icône alt_text Component Principal / Secondaire / Icône uniquement
Barre de titre alt_text Component
Scaffold Scaffold et la barre de titre se trouvent dans la même démo.

Pour en savoir plus sur les spécifications de conception, consultez les conceptions de composants dans ce kit de conception sur Figma.

Pour en savoir plus sur les mises en page canoniques, consultez la page Mises en page canoniques des widgets.