Creazione di una UI in sintesi

Questa pagina descrive come gestire le dimensioni e fornire layout flessibili e adattabili con Glance.

Utilizza Box, Column e Row

Glance ha tre layout componibili principali:

  • Box: posiziona gli elementi sopra un altro. Si traduce in RelativeLayout.

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

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

Immagine di layout a colonne, righe e riquadri.
Figura 1. Esempi di layout con Colonna, Riga e Casella.

Ognuno di questi elementi componibili consente di definire l'allineamento verticale e orizzontale dei contenuti e i vincoli relativi a larghezza, altezza, peso o spaziatura interna utilizzando i modificatori. Inoltre, ogni figlio può definire il proprio modificatore per cambiare lo spazio e il posizionamento all'interno dell'elemento principale.

L'esempio seguente mostra come creare un elemento Row che distribuisca uniformemente gli elementi secondari orizzontalmente, 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 asset secondario ha la stessa peso, condivide in modo uniforme lo spazio disponibile. Puoi definire ponderazioni, dimensioni, spaziatura interna o allineamenti diversi per adattare i layout alle tue esigenze.

Utilizzare layout scorrevoli

Un altro modo per fornire contenuti adattabili consiste nel renderli scorrevoli. Questo è possibile con il componibile LazyColumn. Questo componibile 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 specificare il numero di elementi:

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

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

Fornisci i 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 il valore itemId. Se specifichi itemId, puoi migliorare le prestazioni e mantenere la posizione di scorrimento tramite l'elenco e gli aggiornamenti appWidget a partire da Android 12 in poi (ad esempio, durante l'aggiunta o la rimozione di elementi dall'elenco). L'esempio seguente mostra come specificare un itemId:

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

Definisci SizeMode

Le dimensioni di AppWidget possono variare a seconda del dispositivo, della scelta dell'utente o dell'Avvio app, perciò è importante fornire layout flessibili come descritto nella pagina Fornire layout flessibili dei widget. Glance semplifica questa operazione con la definizione SizeMode e il valore LocalSize. Le tre sezioni seguenti descrivono le tre modalità.

SizeMode.Single

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

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 minimi e massimi sono definiti correttamente in base alle dimensioni dei contenuti.
  • I contenuti sono sufficientemente flessibili all'interno dell'intervallo di dimensioni previsto.

In generale, dovresti utilizzare questa modalità quando:

a) AppWidget ha una dimensione fissa oppure b) non modifica i suoi contenuti quando viene ridimensionato.

SizeMode.Responsive

Questa modalità equivale a fornire layout adattabili, che consente all'GlanceAppWidget di definire un insieme di layout adattabili limitati da dimensioni specifiche. Per ogni dimensione definita, i contenuti vengono creati e mappati alle dimensioni specifiche quando AppWidget viene creato o aggiornato. Il sistema seleziona quindi quella più adatta 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 alla dimensione definita.

  • Nella prima chiamata, la dimensione restituisce 100x100. I contenuti non includono il pulsante aggiuntivo, né il testo superiore e inferiore.
  • Nella seconda chiamata, la dimensione restituisce 250x100. I contenuti includono il pulsante aggiuntivo, ma non il testo superiore e inferiore.
  • Nella terza chiamata, la dimensione restituisce 250x250. I contenuti includono il pulsante aggiuntivo ed entrambi i testi.

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

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

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 richiede i contenuti GlanceAppWidget ogni volta che cambia la dimensione AppWidget disponibile (ad esempio, quando l'utente ridimensiona AppWidget nella schermata Home).

Ad esempio, nel widget di destinazione, è possibile aggiungere un pulsante extra 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 maggiore flessibilità rispetto alle altre, ma prevede alcune avvertenze:

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

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

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 per abilitare risorse dinamiche, ad esempio i colori dinamici.

I componibili e i metodi accettano le risorse utilizzando un "provider", come ImageProvider, o un metodo di overload come GlanceModifier.background(R.color.blue). Ecco alcuni esempi:

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

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

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:

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

Quando lo stato cambia, viene attivata la funzione lambda fornita. Puoi archiviare lo stato del controllo, 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)
    ),

)