Criar uma interface com o Glance

Esta página descreve como processar tamanhos e fornecer layouts flexíveis e responsivos com o Glance.

Usar Box, Column e Row

O Glance tem três layouts principais que podem ser compostos:

  • Box: posiciona elementos sobre outros. Ele é convertido em um RelativeLayout.

  • Column: posiciona os elementos depois do outro no eixo vertical. Ela se traduz em um LinearLayout com orientação vertical.

  • Row: posiciona os elementos depois do outro no eixo horizontal. Ela é convertida em um LinearLayout com orientação horizontal.

Imagem de um layout de coluna, linha e caixa.
Figura 1. Exemplos de layouts com Column, Row e Box.

Cada um desses elementos combináveis permite definir os alinhamentos vertical e horizontal do conteúdo e as restrições de largura, altura, peso ou padding usando modificadores. Além disso, cada filho pode definir o modificador para mudar o espaço e a posição dentro do pai.

O exemplo a seguir mostra como criar um Row que distribua os filhos de maneira uniforme na horizontal, conforme mostrado na Figura 1:

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

O Row preenche a largura máxima disponível e, como cada filho tem o mesmo peso, eles compartilham de maneira uniforme o espaço disponível. É possível definir pesos, tamanhos, paddings ou alinhamentos diferentes para adaptar os layouts às suas necessidades.

Usar layouts roláveis

Outra maneira de fornecer conteúdo responsivo é torná-lo rolável. Isso é possível com o elemento combinável LazyColumn. Esse elemento combinável permite definir um conjunto de itens que vão ser mostrados dentro de um contêiner rolável no widget de app.

Os snippets a seguir mostram maneiras diferentes de definir itens dentro do LazyColumn.

Você pode fornecer o número de itens:

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

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

Forneça itens individuais:

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

Forneça uma lista ou matriz de itens:

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

Também é possível usar uma combinação dos exemplos anteriores:

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

O snippet anterior não especifica o itemId. Especificar o itemId ajuda a melhorar a performance e manter a posição de rolagem na lista e nas atualizações de appWidget do Android 12 em diante, por exemplo, ao adicionar ou remover itens da lista. O exemplo a seguir mostra como especificar um itemId:

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

Definir o SizeMode

Os tamanhos de AppWidget podem variar de acordo com o dispositivo, a escolha do usuário ou a tela de início. Por isso, é importante fornecer layouts flexíveis, conforme descrito na página Fornecer layouts flexíveis de widget. O Glance simplifica isso com a definição SizeMode e o valor LocalSize. As seções abaixo descrevem os três modos.

SizeMode.Single

SizeMode.Single é o modo padrão. Isso indica que apenas um tipo de conteúdo é fornecido, ou seja, mesmo que o tamanho disponível de AppWidget mude, o tamanho do conteúdo não vai mudar.

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

Ao usar esse modo, verifique se:

  • Os valores de metadados de tamanho mínimo e máximo são definidos adequadamente com base no tamanho do conteúdo.
  • O conteúdo é flexível o suficiente dentro do intervalo de tamanho esperado.

Em geral, use esse modo quando:

a) AppWidget tiver um tamanho fixo ou b) não mudar o conteúdo quando for redimensionado.

SizeMode.Responsive

Esse modo é o equivalente a fornecer layouts responsivos, que permite que o GlanceAppWidget defina um conjunto de layouts responsivos limitados por tamanhos específicos. Para cada tamanho definido, o conteúdo é criado e mapeado para o tamanho específico quando o AppWidget é criado ou atualizado. Em seguida, o sistema seleciona o melhor ajuste com base no tamanho disponível.

Por exemplo, no nosso AppWidget de destino, você pode definir três tamanhos e o conteúdo deles:

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

No exemplo anterior, o método provideContent é chamado três vezes e mapeado para o tamanho definido.

  • Na primeira chamada, o tamanho é avaliado como 100x100. O conteúdo não inclui o botão extra nem os textos de cima e de baixo.
  • Na segunda chamada, o tamanho é avaliado como 250x100. O conteúdo inclui o botão extra, mas não os textos de cima e de baixo.
  • Na terceira chamada, o tamanho é avaliado como 250x250. O conteúdo inclui o botão extra e os dois textos.

O SizeMode.Responsive é uma combinação dos outros dois modos e permite definir conteúdo responsivo dentro de limites predefinidos. Em geral, esse modo funciona melhor e permite transições mais suaves quando a AppWidget é redimensionada.

A tabela a seguir mostra o valor do tamanho, dependendo do tamanho disponível de SizeMode e AppWidget:

Tamanho disponível 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
* Os valores exatos são apenas para fins de demonstração.

SizeMode.Exact

SizeMode.Exact é equivalente a fornecer layouts exatos, que solicita o conteúdo GlanceAppWidget sempre que o tamanho de AppWidget disponível muda (por exemplo, quando o usuário redimensiona a AppWidget na tela inicial).

Por exemplo, no widget de destino, um botão extra poderá ser adicionado se a largura disponível for maior que um determinado valor.

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

Esse modo oferece mais flexibilidade que os outros, mas traz algumas ressalvas:

  • O AppWidget precisa ser completamente recriado sempre que o tamanho mudar. Isso pode levar a problemas de desempenho e a saltos na interface quando o conteúdo é complexo.
  • O tamanho disponível pode variar dependendo da implementação da tela de início. Por exemplo, se a tela de início não fornecer a lista de tamanhos, o tamanho mínimo possível será usado.
  • Em dispositivos anteriores ao Android 12, a lógica de cálculo de tamanho pode não funcionar em todas as situações.

Em geral, use esse modo se SizeMode.Responsive não puder ser usado, ou seja, não é viável ter um pequeno conjunto de layouts responsivos.

Acessar recursos

Use LocalContext.current para acessar qualquer recurso do Android, conforme mostrado no exemplo abaixo:

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

Recomendamos fornecer IDs de recursos diretamente para reduzir o tamanho do objeto RemoteViews final e ativar recursos dinâmicos, como cores dinâmicas.

Os elementos combináveis e métodos aceitam recursos usando um "provedor", como ImageProvider, ou um método de sobrecarga como GlanceModifier.background(R.color.blue). Por exemplo:

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

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

Adicionar botões compostos

Botões compostos foram lançados no Android 12. O Glance oferece suporte a versões anteriores para os tipos de botões compostos abaixo:

Cada botão composto mostra uma visualização clicável que representa o estado "verificado".

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 o estado muda, a lambda fornecida é acionada. É possível armazenar o estado da verificação, conforme mostrado no exemplo a seguir:

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

Você também pode fornecer o atributo colors a CheckBox, Switch e RadioButton para personalizar as cores:

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

)