Permitir que os usuários arrastem componentes fisicamente pela tela é fundamental para apps Android modernos. Seja para criar um controle deslizante personalizado, um componente de deslizar para dispensar ou uma tela interativa, o Jetpack Compose oferece um conjunto abrangente de APIs para processar eventos de arrastar com facilidade.
As APIs de arrastar do Compose eliminam a complexidade dos eventos de toque brutos.
Em vez de calcular manualmente as coordenadas de toque e gerenciar os ciclos de vida de gestos
com pointerInput, use modificadores integrados que se conectam
diretamente aos sistemas de gerenciamento de estado e animação do Compose.
Nesta página, descrevemos como e quando usar cada API de arrastar.
Escolher a API de arrastar correta
Há várias APIs que podem ser usadas para implementar conteúdo arrastável.
- Escolha componentes padrão do Material Design (como
SwipeToDismissBoxou gavetas de navegação) quando: você precisar do comportamento padrão de deslizar ou arrastar do Material 3 (por exemplo, excluir um e-mail, arquivar uma tarefa ou abrir um menu lateral / páginas inferiores). Eles oferecem comportamentos integrados sem exigir gerenciamento de estado ou layout personalizado. - Escolha
Modifier.draggablequando: você só precisa de deltas de arrastar brutos (por exemplo, arrastar um controle deslizante personalizado). Para mais informações, consulte Arrastar ao longo de um único eixo com o modificador draggable.- Escolha
Modifier.draggable2Dquando: você precisa de um componente para se mover continuamente pelos eixos X e Y ao mesmo tempo (por exemplo, ao deslocar um mapa ou criar um botão de ação flutuante que o usuário pode arrastar para qualquer lugar na tela). Para mais informações, consulte Arrastar em duas dimensões comModifier.draggable2D.
- Escolha
- Escolha
Modifier.anchoredDraggablequando: você estiver criando componentes interativos personalizados que precisam se ajustar entre duas ou mais posições discretas (por exemplo, uma página inferior, uma gaveta de navegação ou uma ação de deslizar para revelar). Para mais informações, consulte Arrastar componentes entre pontos de fixação comanchoredDraggable.
Arrastar ao longo de um único eixo com draggable
O modificador draggable é o ponto de entrada de alto nível para gestos de arrastar em
uma única orientação e informa a distância da ação de arrastar em pixels.
É importante observar que esse modificador é semelhante a scrollable, porque
ele detecta apenas o gesto. Você precisa manter o estado e representá-lo na tela, por exemplo, movendo o elemento usando o modificador offset:
@Composable private fun DraggableText() { var offsetX by remember { mutableFloatStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ), text = "Drag me!" ) }
Se você precisar controlar todo o gesto de arrastar, use o detector de gestos de arrastar
com o modificador pointerInput.
@Composable private fun DraggableTextLowLevel() { Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) } Box( Modifier .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() offsetX += dragAmount.x offsetY += dragAmount.y } } ) } }
Arrastar componentes entre pontos de fixação com anchoredDraggable
O modificador anchoredDraggable permite arrastar um componente entre um conjunto de
posições predefinidas chamadas de pontos de fixação. Quando o usuário solta o gesto de arrastar, o componente se fixa na âncora mais próxima. anchoredDraggable é
útil para criar componentes como páginas inferiores personalizadas, controles deslizantes complexos ou
painéis de gaveta.
Superfície da API
A API AnchoredDraggable consiste nas seguintes partes principais:
- Âncoras (
DraggableAnchors): um builder que define o mapeamento entre seus estados personalizados (por exemplo, uma classe de tipo enumerado paraStart, Center, End) e os valores de deslocamento de pixel correspondentes. - Estado (
AnchoredDraggableState<T>): um detentor de estado que gerencia a posição atual do componente (deslocamento), a âncora em que ele está fixado e o progresso de arrastar. Ele precisa ser criado usandorememberSaveableeAnchoredDraggableState.Saver<T>para preservar o estado em todas as mudanças de configuração. - Modificador de gesto (
Modifier.anchoredDraggable): um modificador que detecta gestos de arrastar ao longo de umOrientation, atualiza o estado com o delta de arrastar e processa o gesto de deslizar para animar o componente até a âncora mais próxima ao soltar.
Exemplo: criar um componente SwipeToDismiss personalizado
Este exemplo demonstra como criar um componente SwipeToDismiss personalizado (um
banner branco) que o usuário pode deslizar para a esquerda para dispensar ou arrastar para a direita para
ver um efeito de rolagem esticada.
Defina valores de âncora. Defina um tipo enumerado que represente as posições discretas em que o conteúdo arrastável pode se fixar:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
Instancie e salve
AnchoredDraggableState. UserememberSaveablecomAnchoredDraggableState.Saver()para persistir a posição atual do componente durante a recriação da Activity e as mudanças de configuração (por exemplo, rotação da tela ou mudanças de tema):val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
Aplique os modificadores
layout,offsetedraggable. Para tornar o componente arrastável e garantir que os pontos de fixação se adaptem ao tamanho, aplique os seguintes modificadores ao conteúdo que precisa se mover visualmente:Modifier.layout: as posições de âncora geralmente dependem do tamanho de um componente, que não é conhecido durante a composição inicial. Use o modificadorlayoutpara medir o componente, calcular o mapaDraggableAnchorsdinamicamente com base na largura dele e atualizar o estado usandostate.updateAnchors().
Modifier.anchoredDraggable: conecta-se ao estado, envia deltas de arrasto na orientação especificada e processa o gesto de deslizar para animar o componente até a fixação mais próxima quando solto. Adicione esse modificador ao componente que precisa responder aos gestos de arrastar e transmita a ele ostatee oorientationde arrastar (e, opcionalmente,overscrollEffect).Modifier.offset: lê o deslocamento destate.requireOffset()dentro do lambda de deslocamento. A leitura do deslocamento dentro dessa lambda garante que o valor seja lido apenas durante a fase de posicionamento, evitando recomposições desnecessárias durante o arrasto. Aplique esse modificador ao conteúdo que precisa se mover visualmente.
Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() }
[Opcional] Integrar a rolagem esticada. A API
overscrollEffectimplementa um efeito visual que aparece quando as bordas de um contêiner de rolagem são alcançadas com uma rolagem ou um deslize. Você pode fornecer feedback visual quando um usuário arrasta além dos limites de fixação definidos com um efeito de rolagem esticada (geralmente um alongamento ou brilho).- Envio: transmita uma instância de
overscrollEffectpara o modificadoranchoredDraggable. Isso envia deltas de arrastar para o estadoAnchoredDraggableou para o efeito de rolagem esticada, dependendo se o gesto está dentro ou fora dos limites da âncora. - Renderização: aplique o
Modifier.overscrollcom a mesma instânciaOverscrollEffectao componente que renderiza visualmente o efeito.
- Envio: transmita uma instância de
O snippet abaixo mostra como adicionar um efeito de alongamento padrão:
val overscrollEffect = rememberOverscrollEffect() Box( modifier.fillMaxWidth() ) { Box(Modifier.fillMaxWidth()) { background() } Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() } }
Resultado
O snippet de código anterior resulta em um banner branco com o texto "Arraste-me para dispensar!", que o usuário pode deslizar para a esquerda para dispensar (ou arrastar para a direita para ver o efeito de rolagem esticada):
Exemplo: criar uma gaveta de navegação personalizada orientada por gestos
Este exemplo demonstra como criar um painel de navegação personalizado
acionado por gestos. Conforme o usuário arrasta horizontalmente, a tela em primeiro plano (ScreenContents)
desliza para a direita, reduz a escala vertical de 1.0f para 0.8f e arredonda os cantos
para revelar o painel de navegação abaixo.
Defina estados e posições de âncora. Neste exemplo, as âncoras são os estados aberto e fechado do painel. Defina um tipo enumerado para os estados
OpeneClosede mapeie-os para deslocamentos de pixel usandoDraggableAnchors:enum class DrawerState { Open, Closed } val anchors = DraggableAnchors { DrawerState.Open at drawerWidth DrawerState.Closed at 0f } val state = remember { AnchoredDraggableState( initialValue = DrawerState.Closed, anchors = anchors, ) }
Animação de
anchoredDraggableStateao clicar em um item. ColoqueHomeScreenDrawerna parte de baixo doBoxeScreenContentsna parte de cima. A gaveta permanece parada, enquanto a camada superior se move e é dimensionada sobre ela.Quando um usuário selecionar um item do painel, atualize o estado da tela ativa e dispense o painel automaticamente usando
state.animateTo(DrawerState.Closed):@Composable private fun AnchoredDraggableDrawerOnClickSample( modifier: Modifier = Modifier, state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { var screenState by remember { mutableStateOf(ScreenState.Home) } Box(modifier = modifier.fillMaxSize()) { HomeScreenDrawer( selectedScreen = screenState, onScreenSelected = { screen -> screenState = screen // Dismiss drawer when a destination is selected coroutineScope.launch { state.animateTo(DrawerState.Closed) } } ) // ScreenContents(...) } }
Use
state.requireOffset()para mudar várias propriedades visuais. Em vez de apenas deslocar o conteúdo, é possível mudar várias propriedades comModifier.graphicsLayerpara animar simultaneamente a translação, a escala e o raio do canto com base no progresso do arrasto:- Use
state.requireOffset()para ver o deslocamento de arrasto atual. - Use
lerp()(interpolação linear) para calcular a escala proporcional (1fa0.8f) e o raio do canto (0.dpa32.dp) em relação ao deslocamento e à largura do painel. Modifier.graphicsLayerexecuta mudanças na fase de desenho, garantindo animações de alta performance sem acionar recomposições durante o arrasto.
@Composable private fun AnchoredDraggableDrawerGraphicsLayerSample( state: AnchoredDraggableState<DrawerState>, drawerWidth: Float, selectedScreen: ScreenState, onDrawerClicked: () -> Unit ) { ScreenContents( selectedScreen = selectedScreen, onDrawerClicked = onDrawerClicked, modifier = Modifier .graphicsLayer { this.translationX = state.requireOffset() val scale = lerp(1f, 0.8f, state.requireOffset() / drawerWidth) this.scaleX = scale this.scaleY = scale val cornerRadius = lerp(0f, 32f, state.requireOffset() / drawerWidth) this.shape = RoundedCornerShape(cornerRadius.dp) this.clip = true } .anchoredDraggable(state, Orientation.Horizontal) ) }
- Use
Acione animações programáticas com
animateTo. Controle o painel de forma programática (por exemplo, ao tocar no ícone de menu ou selecionar um item do painel) iniciandostate.animateTo()em um escopo de corrotina:fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
Resultado
Arraste em duas dimensões com Modifier.draggable2D
Use o modificador draggable2D para mover elementos individuais da interface nos eixos X e Y simultaneamente.
Arrastar um elemento combinável
Este exemplo mostra o caso de uso mais comum para draggable2D: permitir que um usuário
selecione um elemento da interface e o reposicione em qualquer lugar dentro de um contêiner pai.
@Composable private fun DraggableComposableElement() { // 1. Track the position of the floating window var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) { Box( modifier = Modifier // 2. Apply the offset to the box's position .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } // ... // 3. Attach the 2D drag logic .draggable2D( state = rememberDraggable2DState { delta -> // 4. Update the position based on the movement delta offset += delta } ), contentAlignment = Alignment.Center ) { Text("Video Preview", color = Color.White, fontSize = 12.sp) } } }
O snippet de código anterior inclui o seguinte:
- Rastreia a posição da caixa usando um estado
offset. - Usa o modificador
offsetpara mudar a posição do componente com base nos deltas de arrastar. - Como não há suporte para deslizamento, a caixa para de se mover no instante em que o usuário levanta o dedo.
Arrastar um elemento combinável filho com base na área de arrastar do pai
Este exemplo demonstra como usar draggable2D para criar uma área de entrada 2D
em que um botão seletor é restrito a uma superfície específica. Ao contrário do
exemplo de elemento arrastável, que move o próprio componente, essa implementação
usa os deltas 2D para mover um "seletor" de composição filho em um seletor de cores:
@Composable private fun ExampleColorSelector( // ... ) { // 1. Maintain the 2D position of the selector in state. var selectorOffset by remember { mutableStateOf(Offset.Zero) } // 2. Track the size of the background container. var containerSize by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .size(300.dp, 200.dp) // Capture the actual pixel dimensions of the container when it's laid out. .onSizeChanged { containerSize = it } .clip(RoundedCornerShape(12.dp)) .background( brush = remember(hue) { // Create a simple gradient representing Saturation and Value for the given Hue. Brush.linearGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f))) } ) ) { Box( modifier = Modifier .size(24.dp) .graphicsLayer { // Center the selector on the finger by subtracting half its size. translationX = selectorOffset.x - (24.dp.toPx() / 2) translationY = selectorOffset.y - (24.dp.toPx() / 2) } // ... // 3. Configure 2D touch dragging. .draggable2D( state = rememberDraggable2DState { delta -> // 4. Calculate the new position and clamp it to the container bounds val newX = (selectorOffset.x + delta.x) .coerceIn(0f, containerSize.width.toFloat()) val newY = (selectorOffset.y + delta.y) .coerceIn(0f, containerSize.height.toFloat()) selectorOffset = Offset(newX, newY) } ) ) } }
O snippet anterior inclui o seguinte:
- Ele usa o modificador
onSizeChangedpara capturar as dimensões reais do contêiner de gradiente. O seletor sabe exatamente onde estão as bordas. - Dentro do
graphicsLayer, ele ajusta otranslationXe otranslationYpara garantir que o seletor permaneça centralizado ao ser arrastado.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Noções básicas sobre gestos
- Exemplo de animação avançada: gestos {:#gesture-and-animation}
- Animações com base no valor