Listas permitem que os usuários selecionem um item de um conjunto de opções em dispositivos Wear OS.
Muitos dispositivos Wear OS usam telas redondas, o que dificulta a visualização
de itens de lista que aparecem perto das partes de cima e de baixo da tela. Por esse motivo,
o Compose para Wear OS inclui uma versão da classe LazyColumn chamada
TransformingLazyColumn, que oferece suporte a animações de dimensionamento e transformação.
Quando os itens se movem para as bordas, eles ficam menores e desaparecem.
Para aplicar os efeitos de rolagem e escalonamento recomendados:
- Use
Modifier.transformedHeightpara permitir que o Compose calcule a mudança de altura à medida que o item rola pela tela. - Use
transformation = SurfaceTransformation(transformationSpec)para aplicar os efeitos visuais, incluindo a redução do conteúdo do item. - Use um
TransformationSpecpersonalizado para componentes que não usamtransformationcomo parâmetro, comoText.
A animação a seguir mostra como um elemento de lista é dimensionado e muda de forma ao se aproximar da parte de cima e de baixo da tela:
O snippet de código a seguir mostra como criar uma lista usando o layoutTransformingLazyColumn para criar conteúdo que
fica ótimo em vários tamanhos de tela do Wear OS.
O snippet também demonstra o uso do modificador
minimumVerticalContentPadding, que deve ser definido nos itens da lista
para aplicar o padding correto na parte de cima e de baixo da lista.
Para mostrar o indicador de rolagem, compartilhe o columnState entre
o ScreenScaffold e o TransformingLazyColumn:
val columnState = rememberTransformingLazyColumnState() val transformationSpec = rememberTransformationSpec() ScreenScaffold( scrollState = columnState ) { contentPadding -> TransformingLazyColumn( state = columnState, contentPadding = contentPadding ) { item { ListHeader( modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), transformation = SurfaceTransformation(transformationSpec) ) { Text(text = "Header") } } // ... other items item { Button( modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), transformation = SurfaceTransformation(transformationSpec), onClick = { /* ... */ }, icon = { Icon( imageVector = Icons.Default.Build, contentDescription = "build", ) }, ) { Text( text = "Build", maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } } }
Adicionar um efeito de ajuste e deslize rápido
O ajuste garante que, quando um usuário termina um gesto de rolagem ou deslize, a lista se ajuste com um item posicionado precisamente em um ponto específico, geralmente o centro da tela. Em telas redondas, em que os itens são dimensionados e transformados à medida que se afastam do centro, o ajuste é particularmente útil para garantir que o item mais relevante permaneça totalmente visível e legível na área de visualização ideal.Para adicionar um comportamento de ajuste e movimento rápido, defina o parâmetro flingBehavior como TransformingLazyColumnDefaults.snapFlingBehavior(columnState).
Defina o rotaryScrollableBehavior para corresponder, usando
RotaryScrollableDefaults.snapBehavior(columnState) para uma experiência consistente
ao usar a coroa ou o aro físico.
val columnState = rememberTransformingLazyColumnState() ScreenScaffold(scrollState = columnState) { contentPadding -> TransformingLazyColumn( state = columnState, flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState), rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState) ) { // ... // ... } }
Layout invertido
Por padrão, uma lista rolável é ancorada na borda superior. Se um usuário rolar até a parte de baixo de uma lista padrão e um novo item for adicionado ao final, a lista vai manter a visualização do usuário no item atual. Por exemplo, se o usuário estiver visualizando o item 10 na parte de baixo da tela e o item 11 for adicionado, a visualização vai continuar focada no item 10, e o item 11 vai aparecer fora da tela abaixo da visualização atual.
Para casos de uso como aplicativos de mensagens ou registros ativos, esse comportamento geralmente não é desejado. Quando novos itens chegam, os usuários geralmente querem ver o conteúdo mais recente imediatamente se já estiverem na parte de baixo da lista. Se muitos itens chegarem de uma vez, a lista vai pular para mostrar o item mais recente na parte de baixo. Isso significa que alguns itens intermediários podem não ser exibidos, a menos que o usuário role para cima.
Para oferecer suporte a esses casos de uso, o TransformingLazyColumn permite inverter o
layout definindo reverseLayout = true. Isso muda a âncora da lista da borda superior para a inferior.
Para sua conveniência, definir reverseLayout = true também inverte a ordem visual
dos itens e a direção dos gestos de rolagem:
- Os itens são compostos de baixo para cima, ou seja, o índice 0 aparece na parte de baixo da tela.
- Ao rolar para cima, os itens com índices mais altos aparecem.
Para adicionar um comportamento de ajuste e movimento rápido junto com o layout invertido, combine
flingBehavior e rotaryScrollableBehavior, conforme mostrado no snippet
a seguir:
val columnState = rememberTransformingLazyColumnState() val transformationSpec = rememberTransformationSpec() ScreenScaffold(scrollState = columnState) { contentPadding -> TransformingLazyColumn( state = columnState, contentPadding = contentPadding, reverseLayout = true, modifier = Modifier.fillMaxWidth() ) { items(10) { index -> Button( label = { Text( text = "Item ${index + 1}" ) }, onClick = {}, modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), transformation = SurfaceTransformation(transformationSpec) ) } item { // With reverseLayout = true, the last item declared appears at the top. ListHeader( modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), transformation = SurfaceTransformation(transformationSpec) ) { Text("Header") } } } }
As imagens a seguir mostram a diferença entre uma lista normal e uma invertida:
Botões de borda em listas
No Material 3, você pode adicionar um EdgeButton, que é um botão que fica na borda da parte de baixo das listas. No entanto, tome cuidado para não adicionar isso como um item no TransformingLazyColumn. Em vez disso, use o slot edgeButton no ScreenScaffold.
Usar o slot edgeButton garante que o botão seja posicionado corretamente na
parte de baixo da tela e se comporte de maneira adequada quando a lista for rolada.
O snippet de código a seguir mostra como implementar EdgeButton:
val columnState = rememberTransformingLazyColumnState() ScreenScaffold( scrollState = columnState, edgeButton = { EdgeButton( onClick = { /* TODO */ }, modifier = Modifier.scrollable( columnState, orientation = Orientation.Vertical, reverseDirection = true, // Apply overscroll to the EdgeButton for proper scrolling behavior. overscrollEffect = rememberOverscrollEffect(), ) ) { Text("More") } } ) { contentPadding -> TransformingLazyColumn( contentPadding = contentPadding, state = columnState, ) { // ... // ... } }
Deslizar para revelar em listas
O componente SwipeToReveal permite acessar ações de um item de lista, como
um Card ou Chip, deslizando. Ao deslizar, geralmente aparecem um ou dois botões de ação (como "Excluir" ou "Mais") na lateral.
Ao usar SwipeToReveal em um TransformingLazyColumn, siga estas diretrizes:
- Redefinir ao rolar: quando o usuário rola a lista, redefina todos os itens abertos com um deslize para o estado coberto.
- Alturas consistentes: defina as alturas dos botões de ação para corresponder ao item interno
deslizado (seja um
Buttonou umCard) para garantir uma aparência consistente. - Transformar o contêiner: aplique o modificador
transformedHeightetransformationSpecao próprio componenteSwipeToReveal. - Não faça uma transformação dupla: não aplique modificadores
transformedHeightoutransformationao item interno deslizado (o card ou botão dentro do contêinerSwipeToReveal).
Combináveis personalizados em listas
Ao criar componentes de superfície personalizados para um TransformingLazyColumn, siga
estas práticas recomendadas para que seu elemento combinável seja dimensionado, desapareça e se transforme sem problemas perto
das bordas da tela:
- Expor
SurfaceTransformation: aceitar um parâmetroSurfaceTransformationopcional (padrãonull), correspondente a componentes padrão do Wear Compose Material 3, comoCardeButton. Isso permite que os autores de chamadas em umTransformingLazyColumnpassemSurfaceTransformation(transformationSpec), enquanto o componente funciona normalmente fora de uma lista. - Aplique
Modifier.transformedHeightprimeiro no código de chamada: ao colocar seu elemento combinável personalizado em umTransformingLazyColumn, transmitaModifier.transformedHeight(this, transformationSpec)como o primeiro modificador na cadeia de modificadores do código de chamada. EnquantoSurfaceTransformationaplica os efeitos visuais de escalonamento e fade,transformedHeighté essencial para informar ao layout da lista que recalcule a altura do item à medida que ele diminui. - Aplique camadas de transformação, o chamador
modifiere o pintor em ordem:- Camada de transformação de contêiner: inicie a cadeia de modificadores do contêiner raiz com
Modifier.graphicsLayereapplyContainerTransformation()para que o plano de fundo e o conteúdo sejam desenhados no espaço de coordenadas dimensionado e inclinado. - Chamador
modifier: aplique o parâmetromodifiertransmitido pelo chamador (que incluiModifier.transformedHeight) em seguida, antes de qualquer dimensionamento ou padding interno. - Corte de forma quando não há transformação: se
transformationfornull, apliqueModifier.clip(shape)antes de desenhar o plano de fundo. O pintor retornado porcreateContainerPainter()se ajusta à forma, mas um pintor simples não faz isso. Portanto, sem isso, o plano de fundo é desenhado com cantos quadrados fora de uma lista. - Pintor de plano de fundo com transformação: desenhe o plano de fundo dentro da
camada de contêiner usando
Modifier.drawBehinde um pintor criado comcreateContainerPainter(). - Camada de transformação de conteúdo: aplique um segundo
Modifier.graphicsLayercomapplyContentTransformation()e corte na forma do contêiner para que o conteúdo interno desapareça mais cedo à medida que se aproxima da moldura.
- Camada de transformação de contêiner: inicie a cadeia de modificadores do contêiner raiz com
O snippet a seguir mostra como implementar um elemento combinável BoardingPassCard
personalizado que aplica essas transformações em ordem:
@Composable fun BoardingPassCard( flightNumber: String, origin: String, destination: String, gate: String, seat: String, departureTime: String, modifier: Modifier = Modifier, transformation: SurfaceTransformation? = null, shape: Shape = RoundedCornerShape(18.dp), statusBadge: @Composable () -> Unit = {} ) { // 1. Create morphing container painter val backgroundPainter = ColorPainter(MaterialTheme.colorScheme.surfaceContainer) val finalPainter = if (transformation != null) { remember(transformation, backgroundPainter, shape) { transformation.createContainerPainter(backgroundPainter, shape, border = null) } } else { backgroundPainter } Column( modifier = Modifier // 2a. Container layer: Scales, fades, and tilts the whole card surface .then( if (transformation != null) { Modifier.graphicsLayer { transformation.run { applyContainerTransformation() } } } else Modifier ) // 2b. Caller modifier: Includes Modifier.transformedHeight in a list .then(modifier) .fillMaxWidth() // 2c. Shape clip: Only needed without a transformation, because the // painter from createContainerPainter clips itself to the shape .then(if (transformation == null) Modifier.clip(shape) else Modifier) // 2d. Morphing background: Drawn inside the transformed container layer .drawBehind { with(finalPainter) { draw(size) } } // 2e. Content layer: Fades content earlier and clips children to shape .then( if (transformation != null) { Modifier.graphicsLayer { this.shape = shape this.clip = true transformation.run { applyContentTransformation() } } } else Modifier ) .padding(horizontal = 14.dp, vertical = 10.dp) ) { // Card content goes here } }
Em seguida, use BoardingPassCard em um TransformingLazyColumn transmitindo
Modifier.transformedHeight como o primeiro modificador, junto com
SurfaceTransformation(transformationSpec):
@Composable fun BoardingPassListSample(flights: List<FlightInfo>) { val listState = rememberTransformingLazyColumnState() val transformationSpec = rememberTransformationSpec() ScreenScaffold(scrollState = listState) { contentPadding -> TransformingLazyColumn( state = listState, contentPadding = contentPadding, modifier = Modifier.fillMaxSize() ) { items(flights.size) { index -> val flight = flights[index] BoardingPassCard( flightNumber = flight.number, origin = flight.origin, destination = flight.destination, gate = flight.gate, seat = flight.seat, departureTime = flight.time, modifier = Modifier .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding( CardDefaults.minimumVerticalListContentPadding ), transformation = SurfaceTransformation(transformationSpec) ) } } } }
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Codelab sobre o Compose para Wear OS
- Listas e grades
- Como usar visualizações no Compose