Arrastar elementos combináveis

Permitir que os usuários arrastem fisicamente componentes em uma tela é fundamental para apps Android modernos. Se você estiver criando 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 abstraem 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, você pode usar modificadores integrados que se vinculam diretamente aos sistemas de gerenciamento de estado e animação do Compose.

Esta página descreve 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 (como SwipeToDismissBox ou 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.draggable quando: você só precisar 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 arrastável.
    • Escolha Modifier.draggable2D quando: você precisar de um componente para se mover continuamente nos eixos X e Y simultaneamente (por exemplo, fazer uma panorâmica em um mapa ou criar um botão de ação flutuante que o usuário possa arrastar para qualquer lugar na tela). Para mais informações, consulte Arrastar em duas dimensões com Modifier.draggable2D.
  • Escolha Modifier.anchoredDraggable quando: 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 ancoragem com anchoredDraggable.

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 pelo offset modificador:

@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, considere usar o detector de gestos de arrastar em vez disso, pelo pointerInput modificador.

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

INSIRA O TEXTO ALTERNATIVO AQUI
Figura 1. Um elemento da interface sendo arrastado por um dedo

Arrastar componentes entre pontos de ancoragem com anchoredDraggable

O modificador anchoredDraggable permite arrastar um componente entre um conjunto de posições predefinidas chamadas de pontos de ancoragem. Quando o usuário libera o gesto de arrastar, o componente se instala 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 enum para Start, 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á instalado e o progresso de arrastar. Ele precisa ser criado usando rememberSaveable e AnchoredDraggableState.Saver<T> para preservar o estado em mudanças de configuração
  • Modificador de gesto (Modifier.anchoredDraggable): um modificador que detecta gestos de arrastar ao longo de uma Orientation, atualiza o estado com o delta de arrastar e processa o gesto de deslizar para animar o componente até a âncora mais próxima após a liberação.

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.

  1. Definir valores de âncora. Defina um enum que represente as posições discretas em que o conteúdo arrastável pode ser instalado:

    private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }

  2. Instanciar e salvar AnchoredDraggableState. Use rememberSaveable com AnchoredDraggableState.Saver() para persistir a posição atual do componente em toda a recriação de atividades e 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)
        }

  3. Aplicar os modificadores layout, offset e draggable. Para tornar o componente arrastável e garantir que os pontos de ancoragem se adaptem ao tamanho dele, aplique os seguintes modificadores ao conteúdo que precisa se mover visualmente:

    • Modifier.layout: as posições de ancoragem geralmente dependem do tamanho de um componente, que não é conhecido durante a composição inicial. Use o modificador layout para medir o componente, calcular o mapa DraggableAnchors dinamicamente com base na largura dele e atualizar o estado usando state.updateAnchors().

    • Modifier.anchoredDraggable: conecta-se ao estado, envia deltas de arrastar na orientação especificada e processa o gesto de deslizar para animar o componente até a âncora mais próxima quando liberado. Adicione esse modificador ao componente que precisa responder a gestos de arrastar e transmita o state e a orientation de arrastar (e, opcionalmente, overscrollEffect).
    • Modifier.offset: lê o deslocamento de state.requireOffset() dentro da lambda de deslocamento. A leitura do deslocamento nessa lambda garante que o valor seja lido apenas durante a fase de posicionamento, evitando recomposições desnecessárias durante o arrastar. 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()
    }

  4. [Opcional] Integrar a rolagem esticada. A API overscrollEffect implementa um efeito visual que aparece quando as bordas de um contêiner de rolagem são alcançadas com uma rolagem ou deslize. Você pode fornecer feedback visual quando um usuário arrasta além dos limites de ancoragem definidos com um efeito de rolagem esticada (normalmente um alongamento ou brilho).

    1. Enviar: transmita uma instância overscrollEffect para o anchoredDraggable modificador. Isso envia deltas de arrastar para o estado AnchoredDraggable ou para o efeito de rolagem esticada, dependendo se o gesto está dentro ou fora dos limites da âncora.
    2. Renderizar: aplique o Modifier.overscroll com a mesma OverscrollEffect instância ao componente que precisa renderizar o efeito visualmente.

O snippet a seguir 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 "Drag me to dismiss!", que o usuário pode deslizar para a esquerda para dispensar (ou arrastar para a direita para ver o efeito de alongamento de rolagem):

Vídeo 2. Uma implementação de componente de deslizar para dispensar personalizado.

Exemplo: criar uma gaveta de navegação personalizada orientada por gestos

Este exemplo demonstra como criar uma gaveta de navegação personalizada orientada por gestos. À medida que o usuário arrasta horizontalmente, a tela de primeiro plano (ScreenContents) desliza para a direita, é reduzida de 1.0f para 0.8f e arredonda os cantos para revelar a gaveta de navegação subjacente.

  1. Definir estados e posições de ancoragem. Neste exemplo, as âncoras são os estados aberto e fechado da gaveta. Defina um enum para os estados Open e Closed e mapeie-os para deslocamentos de pixel usando DraggableAnchors:

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

  2. Animar anchoredDraggableState ao clicar em um item. Coloque HomeScreenDrawer na parte de baixo da Box e ScreenContents na parte de cima. A gaveta permanece estacionária, enquanto a camada superior é traduzida e dimensionada sobre ela.

    Quando um usuário seleciona um item de gaveta, atualize o estado da tela ativa e dispense automaticamente a gaveta 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(...)
        }
    }

  3. Use state.requireOffset() para mudar várias propriedades visuais. Em vez de apenas deslocar o conteúdo, você pode mudar várias propriedades com Modifier.graphicsLayer para animar simultaneamente a tradução, a escala e o raio do canto com base no progresso de arrastar:

    • Use state.requireOffset() para receber o deslocamento de arrastar atual.
    • Use lerp() (interpolação linear) para calcular a escala proporcional (1f para 0.8f) e o raio do canto (0.dp para 32.dp) em relação ao deslocamento e à largura da gaveta.
    • Modifier.graphicsLayer executa mudanças na fase de desenho, garantindo animações de alto desempenho sem acionar recomposições durante o arrastar.

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

  4. Acionar animações programáticas com animateTo. Controle a gaveta de maneira programática (por exemplo, ao tocar no ícone de menu ou selecionar um item de gaveta) iniciando state.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

Vídeo 3. A tela de primeiro plano (Sleep) é arrastada para a direita, mostrando a gaveta de navegação subjacente.

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

Vídeo 4. Uma pequena caixa roxa sendo reposicionada em um plano de fundo cinza, demonstrando o arrastar 2D direto em que o elemento para de se mover no instante em que o dedo do usuário é levantado.

O snippet de código anterior inclui o seguinte:

  • Rastreia a posição da caixa usando um estado offset.
  • Usa o modificador offset para mudar a posição do componente com base nos deltas de arrastar.
  • Como não há suporte para deslizar, a caixa para de se mover no instante em que o usuário levanta o dedo.

Arrastar um 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 componente em si, essa implementação usa os deltas 2D para mover um "seletor" combinável 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)
                    }
                )
        )
    }
}

Vídeo 5. Um gradiente de cores com um botão seletor circular branco que pode ser arrastado em qualquer direção, demonstrando como os deltas 2D são fixados aos limites do contêiner para atualizar os valores de cores selecionados.

O snippet anterior inclui o seguinte:

  • Ele usa o modificador onSizeChanged para capturar as dimensões reais do contêiner de gradiente. O seletor sabe exatamente onde estão as bordas.
  • Dentro da graphicsLayer, ele ajusta translationX e translationY para garantir que o seletor permaneça centralizado durante o arrastar.