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.

Utilizzare 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 sull'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 Column, Row e Box.

Ciascuno di questi composable consente di definire gli allineamenti verticali e orizzontali dei contenuti e i vincoli di larghezza, altezza, peso o padding utilizzando i modificatori. Inoltre, ogni elemento secondario può definire il proprio modificatore per modificare lo spazio e il posizionamento all'interno dell'elemento principale.

L'esempio seguente mostra come creare una 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)
}

La Row riempie la larghezza massima disponibile e, poiché ogni elemento secondario ha lo stesso peso, condivide uniformemente lo spazio disponibile. Puoi definire pesi, dimensioni, padding o allineamenti diversi per adattare i layout alle tue esigenze.

Utilizzare layout scorrevoli

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

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

Puoi fornire 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 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 aggiungi o rimuovi 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 con aggancio

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

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


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

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

Se utilizzi lo scorrimento con aggancio con le immagini, consulta il layout canonico dell'immagine a pagina intera.

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

Definire SizeMode

AppWidget sizes may differ depending on the device, user choice, or launcher, so it is important to provide flexible layouts as described in the Provide flexible widget layouts page. Glance semplifica questa operazione con la definizione di 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, 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 delle dimensioni minime e massime valori dei metadati siano definiti correttamente in base alle dimensioni del contenuto.
  • Il contenuto sia sufficientemente flessibile 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 contenuti quando viene ridimensionato.

SizeMode.Responsive

Questa modalità è l'equivalente della fornitura di layout reattivi, che consente a GlanceAppWidget di definire un insieme di layout reattivi delimitati da dimensioni specifiche. Per ogni dimensione definita, il contenuto viene creato e mappato alla dimensione specifica quando AppWidget viene creato o aggiornato. Il sistema seleziona quindi quello più adatto in base alle dimensioni disponibili.

Ad esempio, nel nostro AppWidget di destinazione, 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 è 100x100. Il contenuto non include il pulsante aggiuntivo né i testi in alto e in basso.
  • Nella seconda chiamata, la dimensione è 250x100. Il contenuto include il pulsante aggiuntivo, ma non i testi in alto e in basso.
  • Nella terza chiamata, la dimensione è 250x250. Il contenuto include il pulsante aggiuntivo e entrambi i testi.

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

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

Dimensione disponibile 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 è l'equivalente della fornitura di layout esatti, che richiede il contenuto di GlanceAppWidget ogni volta che la dimensione disponibile di AppWidget cambia (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 alcune limitazioni:

  • AppWidget deve essere ricreato completamente ogni volta che la dimensione cambia. Questo può causare problemi di prestazioni e salti dell'UI quando il contenuto è complesso.
  • La dimensione disponibile potrebbe 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 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 reattivi non è fattibile).

Accedere 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 finale RemoteViews e per abilitare le risorse dinamiche, come i colori dinamici.

I composable e i metodi accettano le risorse utilizzando un "provider", ad esempio ImageProvider, o utilizzando 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",
)

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

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 attivato il lambda fornito. 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 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 Primario / 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 le progettazioni dei componenti in questo kit di progettazione su Figma.

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