Creazione di una UI in sintesi

Questa pagina descrive come gestire le dimensioni e fornire layout flessibili e reattivi 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 un 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 nell'asse orizzontale. Si traduce in un LinearLayout con orientamento orizzontale.

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

Un layout a colonne, righe e caselle.
Figura 1. Esempi di layout con Colonna, Riga e Casella.

Ciascuno di questi composable ti consente di definire gli allineamenti verticale e orizzontale dei contenuti e i vincoli di 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 del genitore.

L'esempio seguente mostra come creare un Row che distribuisce uniformemente i relativi 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 occupa la larghezza massima disponibile e, poiché ogni figlio ha lo stesso peso, lo spazio disponibile viene condiviso in modo uniforme. Puoi definire pesi, dimensioni, spaziatura interna o allineamenti diversi per adattare i layout alle tue esigenze.

Utilizzare layout scorrevoli

Un altro modo per fornire contenuti reattivi è renderli scorrevoli. È possibile con il componente componibile LazyColumn. Questo componente combinabile ti consente di definire un insieme di elementi da visualizzare all'interno di un contenitore a scorrimento 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. La specifica di itemId contribuisce a migliorare le prestazioni e a mantenere la posizione di scorrimento tramite gli aggiornamenti di elenchi e appWidget a partire da Android 12 (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 rapido è un'animazione che consente ai contenuti scorrevoli di posizionarsi nella parte superiore del contenitore del widget.

Video 1. A sinistra è mostrato un elemento di elenco che non si blocca durante lo scorrimento, mentre a destra si blocca.


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

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

Se utilizzi lo scorrimento rapido con le immagini, consulta il layout canonico immagine a tutta pagina.

@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 a seconda del dispositivo, della scelta dell'utente o del launcher, pertanto è 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. Nelle sezioni seguenti vengono descritte le tre modalità.

SizeMode.Single

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

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 sufficientemente flessibili all'interno dell'intervallo di dimensioni previsto.

In generale, devi utilizzare questa modalità quando:

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

SizeMode.Responsive

Questa modalità equivale a fornire layout reattivi, il che consente a GlanceAppWidget di definire un insieme di layout reattivi delimitati da dimensioni specifiche. Per ogni dimensione definita, i contenuti vengono creati e mappati alla dimensione specifica quando viene creato o aggiornato il AppWidget. Il sistema seleziona quindi quella più adatta in base alla taglia disponibile.

Ad esempio, nella nostra destinazione AppWidget, puoi definire tre dimensioni e il relativo contenuto:

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é i testi in alto e in basso.
  • Nella seconda chiamata, la dimensione viene valutata come 250x100. I contenuti includono il pulsante aggiuntivo, ma non i testi in alto e in basso.
  • Nella terza chiamata, la dimensione viene valutata come 250x250. I contenuti includono il pulsante aggiuntivo e entrambi i 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 transizioni più fluide quando viene ridimensionato il AppWidget.

La tabella seguente mostra il valore della dimensione, a seconda di SizeMode e della dimensione disponibile 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 richiede i contenuti GlanceAppWidget ogni volta che le dimensioni 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 è maggiore di 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 presenta alcuni avvertimenti:

  • Il AppWidget deve essere ricreato completamente ogni volta che le dimensioni cambiano. Ciò può causare problemi di prestazioni e salti dell'interfaccia utente quando i contenuti sono complessi.
  • Le dimensioni disponibili potrebbero variare a seconda dell'implementazione del launcher. Ad esempio, se il launcher 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, devi utilizzare questa modalità se non è possibile utilizzare 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 gli ID risorsa direttamente per ridurre le dimensioni dell'oggetto RemoteViews finale e per attivare risorse dinamiche, come i colori dinamici.

I composable 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",
)

Testo dell'handle

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

Aggiungere 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:

Questi pulsanti composti mostrano ciascuno 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 lambda fornita. Puoi memorizzare lo stato di 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)
    ),

)

Componenti aggiuntivi

Glance 1.1.0 include il rilascio di componenti aggiuntivi, come descritto nella tabella seguente:

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

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

Per ulteriori informazioni sui layout canonici, visita la pagina Layout canonici dei widget.