Creazione di una UI in sintesi

Questa pagina descrive come gestire le dimensioni e fornire layout flessibili e adattabili con Glance, utilizzando i componenti Glance esistenti.

Utilizza Box, Column e Row

Glance ha tre layout componibili principali:

  • Box: posiziona gli elementi uno sopra l'altro. Si traduce in RelativeLayout.

  • Column: posiziona gli elementi uno dopo l'altro sull'asse verticale. Si traduce in un LinearLayout con orientamento verticale.

  • Row: posiziona gli elementi uno dopo l'altro sull'asse orizzontale. Si traduce in un LinearLayout con orientamento orizzontale.

Glance supporta gli oggetti Scaffold. Posiziona i composabili Column, Row e Box all'interno di un determinato oggetto Scaffold.

Un layout con colonne, righe e riquadri.
Figura 1. Esempi di layout con Column, Row e Box.

Ognuno di questi elementi componibili ti consente di definire gli allineamenti verticali e orizzontali dei relativi contenuti e i vincoli di larghezza, altezza, peso o spaziatura interna utilizzando i modificatori. Inoltre, ogni elemento secondario può definire il proprio modificatore per cambiare lo spazio e il posizionamento all'interno dell'elemento principale.

L'esempio seguente mostra come creare un Row che distribuisce uniformemente i suoi elementi secondari in orizzontale, come mostrato nella Figura 1:

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

Row riempie la larghezza massima disponibile e, poiché ogni elemento secondario ha lo stesso peso, lo spazio disponibile viene condiviso in modo uniforme. Puoi definire spessori, dimensioni, spaziatura interna o allineamenti diversi per adattare i layout alle tue esigenze.

Utilizza layout scorrevoli

Un altro modo per fornire contenuti adattabili è renderli scorrevoli. Questo è possibile con il componibile LazyColumn. Questo composable ti consente di definire un insieme di elementi da visualizzare all'interno di un contenitore scorrevole nel widget dell'app.

I seguenti snippet mostrano diversi modi per definire gli elementi all'interno di LazyColumn.

Puoi fornire il numero di articoli:

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

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

Fornisci singoli elementi:

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

Fornisci un elenco o un array di elementi:

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

Puoi anche utilizzare una combinazione degli esempi precedenti:

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

Tieni presente che lo snippet precedente non specifica itemId. Specificare itemId aiuta a migliorare le prestazioni e a mantenere la posizione di scorrimento durante gli aggiornamenti di elenchi e appWidget da Android 12 in poi (ad esempio, quando si aggiungono o rimuovono elementi dall'elenco). L'esempio seguente mostra come specificare un itemId:

items(
    items = peopleList,
    itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

Scorrimento rapido

Lo scorrimento con aggancio è un'animazione che consente ai contenuti scorrevoli di agganciarsi alla parte superiore del contenitore del widget.

Video 1. A sinistra è mostrato un elemento dell'elenco che non si aggancia in posizione durante lo scorrimento, mentre a destra si aggancia in posizione.


Per implementare lo scorrimento a scatto, assicurati di soddisfare le seguenti condizioni:

  • Aggiorna la dipendenza di Glance alla versione 1.3.0-alpha02 o successiva.
  • Imposta compileSdk su 37 o un valore superiore, poiché lo scorrimento a scatto è supportato sui dispositivi con Android 17 e versioni successive.
  • Configura LazyColumn con VerticalScrollMode. Se il dispositivo supporta lo scorrimento a scatto, utilizza SnapScrollMatchHeight. In caso contrario, utilizza Normal.

Se utilizzi lo scorrimento con aggancio con le immagini, consulta il layout canonico Immagine full bleed.

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

Definisci il SizeMode

Le dimensioni di AppWidget possono variare in base al dispositivo, alla scelta dell'utente o all'Avvio app, quindi è importante fornire layout flessibili, come descritto nella pagina Fornisci layout dei widget flessibili. Glance semplifica questa operazione con la definizione SizeMode e il valore LocalSize. Le sezioni seguenti descrivono le tre modalità.

SizeMode.Single

SizeMode.Single è la modalità predefinita. Indica che viene fornito un solo tipo di contenuto, ovvero che anche se le dimensioni disponibili di AppWidget cambiano, le dimensioni dei contenuti non cambiano.

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

Quando utilizzi questa modalità, assicurati che:

  • I valori dei metadati relativi alle dimensioni minime e massime sono definiti correttamente in base alle dimensioni dei contenuti.
  • I contenuti sono abbastanza flessibili da rientrare nell'intervallo di dimensioni previsto.

In generale, devi utilizzare questa modalità quando:

a) AppWidget ha dimensioni fisse o b) non cambia i contenuti quando viene ridimensionato.

SizeMode.Responsive

Questa modalità equivale a fornire layout adattabili, il che consente a GlanceAppWidget di definire un insieme di layout adattabili delimitati da dimensioni specifiche. Per ogni dimensione definita, i contenuti vengono creati e mappati alla dimensione specifica quando l'oggetto AppWidget viene creato o aggiornato. Il sistema seleziona quindi l'asset più adatto in base alle dimensioni disponibili.

Ad esempio, nella nostra destinazione AppWidget, puoi definire tre dimensioni e i relativi contenuti:

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

Nell'esempio precedente, il metodo provideContent viene chiamato tre volte e mappato alle dimensioni definite.

  • Nella prima chiamata, la dimensione restituisce 100x100. I contenuti non includono il pulsante aggiuntivo, né i testi in alto e in basso.
  • Nella seconda chiamata, la dimensione restituisce 250x100. I contenuti includono il pulsante aggiuntivo, ma non i testi in alto e in basso.
  • Nella terza chiamata, la dimensione restituisce 250x250. I contenuti includono il pulsante aggiuntivo e i due testi.

SizeMode.Responsive è una combinazione delle altre due modalità e ti consente di definire contenuti adattabili entro limiti predefiniti. In generale, questa modalità offre prestazioni migliori e consente transizioni più fluide quando la AppWidget viene ridimensionata.

La tabella seguente mostra il valore della dimensione, a seconda di SizeMode e della dimensione disponibile di AppWidget:

Dimensioni disponibili 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
* I valori esatti sono solo a scopo dimostrativo.

SizeMode.Exact

SizeMode.Exact equivale a fornire layout esatti, che richiedono i contenuti di GlanceAppWidget ogni volta che le dimensioni di AppWidget disponibili cambiano (ad esempio, quando l'utente ridimensiona AppWidget nella schermata Home).

Ad esempio, nel widget di destinazione, è possibile aggiungere un pulsante aggiuntivo se la larghezza disponibile è superiore a un determinato valore.

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

Questa modalità offre una maggiore flessibilità rispetto alle altre, ma presenta alcuni aspetti da considerare:

  • AppWidget deve essere ricreato completamente ogni volta che le dimensioni cambiano. Questo può causare problemi di prestazioni e salti della UI quando i contenuti sono complessi.
  • Le dimensioni disponibili potrebbero variare a seconda dell'implementazione del programma di avvio. Ad esempio, se Avvio app non fornisce l'elenco delle dimensioni, viene utilizzata la dimensione minima possibile.
  • Nei dispositivi con versioni precedenti ad Android 12, la logica di calcolo delle dimensioni potrebbe non funzionare in tutte le situazioni.

In generale, dovresti utilizzare questa modalità se non è possibile utilizzare SizeMode.Responsive (ovvero, non è possibile utilizzare un piccolo insieme di layout adattabili).

Accedi alle risorse

Utilizza LocalContext.current per accedere a qualsiasi risorsa Android, come mostrato nell'esempio seguente:

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

Ti consigliamo di fornire direttamente gli ID risorsa per ridurre le dimensioni dell'oggetto RemoteViews finale e attivare risorse dinamiche, come i colori dinamici.

Gli elementi componibili e i metodi accettano le risorse utilizzando un "provider", ad esempio ImageProvider, o un metodo di overload come GlanceModifier.background(R.color.blue). Ad esempio:

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

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

Gestisci il testo

Glance 1.1.0 include un'API per impostare gli stili di testo. Imposta gli stili di testo utilizzando gli attributi fontSize, fontWeight o fontFamily della classe TextStyle.

fontFamily supporta tutti i caratteri di sistema, come mostrato nell'esempio seguente, ma i caratteri personalizzati nelle app non sono supportati:

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

Aggiungi pulsanti composti

I pulsanti composti sono stati introdotti in Android 12. Glance supporta la compatibilità con le versioni precedenti per i seguenti tipi di pulsanti composti:

Ognuno di questi pulsanti composti mostra una visualizzazione cliccabile che rappresenta lo stato "selezionato".

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

Quando lo stato cambia, viene attivata la funzione lambda fornita. Puoi memorizzare lo stato selezionato, come mostrato nell'esempio seguente:

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

Puoi anche fornire l'attributo colors a CheckBox, Switch e RadioButton per personalizzarne i colori:

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

    )

Componenti aggiuntivi

Glance 1.1.0 include la release di componenti aggiuntivi, come descritto nella tabella seguente:

Nome Immagine Link di riferimento Note aggiuntive
Pulsante pieno alt_text Componente
Pulsanti con contorno alt_text Componente
Pulsanti icona alt_text Componente Principale / Secondario / Solo icona
Barra del titolo alt_text Componente
Scaffold Scaffold e barra del titolo si trovano nella stessa demo.

Per ulteriori informazioni sulle specifiche di progettazione, consulta i design dei componenti in questo kit di progettazione su Figma.

Per saperne di più sui layout canonici, consulta Layout dei widget canonici.