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. Inserisci 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 figlio può definire il proprio modificatore per modificare lo spazio e il posizionamento all'interno del padre.

L'esempio seguente mostra come creare una Row che distribuisce uniformemente i figli 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)
}

La Row riempie la larghezza massima disponibile e, poiché ogni figlio 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. Ciò è possibile con il composable LazyColumn. Questo composable consente di definire un insieme di elementi da visualizzare all'interno di un contenitore scorrevole nel widget per 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 il rendimento 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 ancoraggio

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

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


Per implementare lo scorrimento con ancoraggio, 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 ancoraggio è supportato sui dispositivi con Android 17 e versioni successive.
  • Configura LazyColumn con VerticalScrollMode. Se il dispositivo supporta lo scorrimento con ancoraggio, utilizza SnapScrollMatchHeight. In caso contrario, utilizza Normal.

Se utilizzi lo scorrimento con ancoraggio 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 dei contenuti 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 siano definiti correttamente in base alle dimensioni dei contenuti.
  • I contenuti siano 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 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, i contenuti vengono creati e mappati 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. I contenuti non includono il pulsante aggiuntivo né i testi in alto e in basso.
  • Nella seconda chiamata, la dimensione è 250x100. I contenuti includono il pulsante aggiuntivo, ma non i testi in alto e in basso.
  • Nella terza chiamata, la dimensione è 250x250. I contenuti includono il pulsante aggiuntivo e entrambi i testi.

SizeMode.Responsive è una combinazione delle altre due modalità e consente di definire contenuti reattivi all'interno di 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 i contenuti di GlanceAppWidget ogni volta che le dimensioni disponibili di AppWidget 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 alcune limitazioni:

  • AppWidget deve essere ricreato completamente ogni volta che le dimensioni cambiano. Questo può causare problemi di prestazioni e salti dell'UI 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 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).

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 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 Layout canonici dei widget.