Criar uma interface com o Glance

Nesta página, descrevemos como lidar com tamanhos e fornecer layouts flexíveis e responsivos com o Glance, usando os componentes atuais do Glance.

Use Box, Column e Row.

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

  • Box: coloca elementos uns sobre os outros. Ele é traduzido para um RelativeLayout.

  • Column: posiciona os elementos um após o outro no eixo vertical. Ele é convertido em um LinearLayout com orientação vertical.

  • Row: posiciona os elementos um após o outro no eixo horizontal. Ele é convertido em um LinearLayout com orientação horizontal.

O Resumo é compatível com objetos Scaffold. Coloque os elementos combináveis Column, Row e Box em um determinado objeto Scaffold.

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 próprio modificador para mudar o espaço e o posicionamento dentro do pai.

O exemplo a seguir mostra como criar um Row que distribui os filhos de maneira uniforme na horizontal, como mostrado 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 o mesmo peso, eles compartilham o espaço disponível de maneira uniforme. É possível definir diferentes pesos, tamanhos, preenchimentos 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 em um contêiner rolável no widget do app.

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

Você pode informar 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 o desempenho e manter a posição de rolagem pela lista e pelas 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,
    itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

Rolagem rápida

A rolagem rápida é uma animação que permite que o conteúdo rolável se ajuste ao topo do contêiner do widget.

Vídeo 1. À esquerda, um item de lista que não se ajusta ao rolar a tela. À direita, um que se ajusta.


Para implementar a rolagem rápida, verifique se você atende às seguintes condições:

  • Atualize a dependência do Resumo para a versão 1.3.0-alpha02 ou mais recente.
  • Defina o compileSdk como 37 ou mais recente, já que a rolagem rápida é compatível com dispositivos com o Android 17 e versões mais recentes.
  • Configure o LazyColumn com VerticalScrollMode. Se o dispositivo for compatível com rolagem rápida, use SnapScrollMatchHeight. Do contrário, use Normal.

Se você estiver usando a rolagem de ajuste com imagens, consulte o layout canônico de imagem com sangria total.

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

Defina o SizeMode

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

SizeMode.Single

SizeMode.Single é o modo padrão. Ele 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 muda.

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 corretamente 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) o AppWidget tem um tamanho fixo ou b) ele não muda o conteúdo quando é redimensionado.

SizeMode.Responsive

Esse modo é o equivalente a fornecer layouts responsivos, o 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 mais adequado com base no tamanho disponível.

Por exemplo, no destino AppWidget, é possível 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.

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

A tabela a seguir mostra o valor do tamanho, dependendo de SizeMode e do 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 solicitam o conteúdo de GlanceAppWidget sempre que o tamanho de 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 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 do que os outros, mas tem algumas ressalvas:

  • A AppWidget precisa ser completamente recriada sempre que o tamanho mudar. Isso pode causar problemas de desempenho e saltos na interface quando o conteúdo é complexo.
  • O tamanho disponível pode variar de acordo com a implementação do acesso rápido. Por exemplo, se o acesso rápido não fornecer a lista de tamanhos, o menor tamanho 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, se um pequeno conjunto de layouts responsivos não for viável.

Acessar recursos

Use LocalContext.current para acessar qualquer recurso do Android, como 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 que podem ser compostos e os métodos aceitam recursos usando um "provedor", como ImageProvider, ou 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",
)

Texto do identificador

O Glance 1.1.0 inclui uma API para definir seus estilos de texto. Defina estilos de texto usando os atributos fontSize, fontWeight ou fontFamily da classe TextStyle.

O fontFamily é compatível com todas as fontes do sistema, como mostrado no exemplo a seguir, mas não com fontes personalizadas em apps:

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

Adicionar botões compostos

Os botões compostos foram introduzidos no Android 12. O Resumo é compatível com versões anteriores dos seguintes tipos de botões compostos:

Cada um desses botões compostos mostra uma visualização clicável que representa o estado "marcado".

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 o estado muda, a lambda fornecida é acionada. É possível armazenar o estado marcado, 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) }
        )
    }
}

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

    )

Componentes adicionais

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

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

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

Para mais informações sobre layouts canônicos, acesse Layouts de widgets canônicos.