Criar uma interface com o Glance

Esta página descreve como lidar com tamanhos e fornecer recursos flexíveis e layouts com o Glance, usando componentes atuais do Glance.

Usar Box, Column e Row

O Glance tem três layouts principais combináveis:

  • Box: posiciona elementos sobre outros. Ele é traduzido como um RelativeLayout.

  • Column: posiciona os elementos um após o outro no eixo vertical. Ela traduz para uma LinearLayout com orientação vertical.

  • Row: posiciona os elementos um após o outro no eixo horizontal. Ela traduz para uma LinearLayout com orientação horizontal.

O Glance oferece suporte a objetos Scaffold. Coloque seus Column, Row e Elementos combináveis Box em um determinado objeto Scaffold.

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 das restrições de largura, altura, peso ou padding usando modificadores. Além disso, cada filho pode definir seu próprio modificador para mudar o espaço e posicionamento dentro do principal.

O exemplo a seguir mostra como criar um Row que distribui de forma uniforme os filhos horizontalmente, como visto na Figura 1:

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

A Row preenche a largura máxima disponível e, como cada filho tem a mesma peso, eles compartilham igualmente o espaço disponível. É possível definir pesos diferentes, tamanhos, paddings ou alinhamentos 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 a serem mostrados dentro de um contêiner rolável no widget do app.

Os snippets a seguir mostram maneiras diferentes de definir itens dentro da 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 O itemId ajuda a melhorar o desempenho e manter a rolagem. posição em listas e 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 dependendo do dispositivo, da escolha do usuário ou da tela de início. por isso, é importante fornecer layouts flexíveis, como descrito em Fornecer com layouts de widget flexíveis. O Glance simplifica isso com SizeMode e o valor LocalSize. As seções a seguir descrevem os três dois modos.

SizeMode.Single

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

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, você deve usar esse modo quando:

a) o AppWidget tiver um tamanho fixo; b) o conteúdo não seja alterado quando redimensionado.

SizeMode.Responsive

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

Por exemplo, no AppWidget de destino, é possível definir três tamanhos e conteúdo:

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 incluem o botão extra nem os textos das partes de cima e de baixo.
  • Na segunda chamada, o tamanho é avaliado como 250x100. O conteúdo inclui 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 botão extra e ambos os textos.

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

A tabela a seguir mostra o valor do tamanho, dependendo do SizeMode e o tamanho disponível de 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 é o equivalente a fornecer layouts exatos, que solicita o conteúdo GlanceAppWidget sempre que o tamanho AppWidget disponível muda (por exemplo, quando o usuário redimensiona o AppWidget na tela inicial).

Por exemplo, no widget de destino, um botão extra pode ser adicionado se o largura disponível é maior do 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 vem com alguns ressalvas:

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

Em geral, use esse modo se não for possível usar SizeMode.Responsive. (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 a seguir:

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

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

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

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

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

Processar texto

O Glance 1.1.0 inclui uma API para definir estilos de texto. Definir estilos de texto usando Atributos fontSize, fontWeight ou fontFamily da classe TextStyle.

fontFamily oferece suporte a todas as fontes do sistema, como mostrado no exemplo abaixo, mas fontes personalizadas em apps não são compatíveis:

Text(
    style = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
        fontFamily = FontFamily.Monospace
    ),
    text = "Example Text"
)

Adicionar botões compostos

Os botões compostos foram lançados no Android 12. O Glance oferece suporte para versões anteriores compatibilidade para os seguintes tipos de botões compostos:

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

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, o lambda fornecido é acionado. É possível armazenar verificar o estado, como 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) }
        )
    }
}

Também é possível fornecer o atributo colors para 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)
    ),

)

Outros componentes

O Glance 1.1.0 inclui o lançamento de outros componentes, conforme descrito no tabela a seguir:

Nome Imagem Link de referência Outras observações
Botão preenchido alt_text Componente
Botões de contorno alt_text Componente
Botões de ícone alt_text Componente Principal / Secundário / Apenas ícones
Barra de título alt_text Componente
Scaffold Scaffold e barra de título estão na mesma demonstração.

Para mais informações sobre detalhes de design, consulte os designs de componentes neste kit de design no Figma.