Arrastar e soltar no Compose

1. Antes de começar

Este codelab oferece instruções práticas sobre os conceitos básicos da implementação da operação de arrastar e soltar no Compose. Você aprenderá como ativar o recurso de arrastar e soltar visualizações no seu app e em diferentes apps. Você aprenderá como implementar a operação de arrastar e soltar no seu app e até em diferentes apps.

Pré-requisitos

Para concluir este codelab, você precisa ter:

O que você vai fazer

Criar um app simples e:

  • Configurar o elemento combinável para ser arrastável usando o modificador dragAndDropSource
  • Configurar o combinável para ser o destino de soltar usando o modificador dragAndDropTarget
  • Receber conteúdo avançado usando o Compose

O que é necessário

2. Um evento de arrastar e soltar

Uma operação de arrastar e soltar pode ser considerada como um evento de quatro fases, sendo elas

  1. Iniciado: o sistema inicia a operação de arrastar e soltar em resposta ao gesto de arrastar do usuário.
  2. Em andamento: o usuário continua arrastando.
  3. Finalizado: o usuário solta o item arrastado no elemento combinável de destino
  4. Existiu: o sistema envia o sinal para finalizar a operação de arrastar e soltar.

O sistema envia o evento de arrastar no objeto DragEvent. O objeto DragEvent pode conter estes dados

  1. ActionType: valor da ação do evento com base no ciclo de vida de arrastar e soltar. Por exemplo, ACTION_DRAG_STARTED, ACTION_DROP etc.
  2. ClipData: dados sendo arrastados, encapsulados no objeto ClipData.
  3. ClipDescription: metainformações sobre o objeto ClipData.
  4. Result: resultado da operação de arrastar e soltar.
  5. X: coordenada x da localização atual do objeto arrastado.
  6. Y: coordenada y do local atual do objeto arrastado.

3. Configurar

Crie um novo projeto e selecione o modelo "Empty Activity":

19da275afd995463.png

Deixe todos os parâmetros como padrão.

Neste codelab, vamos usar a ImageView para demonstrar a funcionalidade de arrastar e soltar. Vamos adicionar uma dependência do Gradle à biblioteca Glide para compor e sincronizar o projeto.

implementation("com.github.bumptech.glide:compose:1.0.0-beta01")

Agora, em MainActivity.kt, crie um composable para a imagem, que vai funcionar como uma origem de arrastar para nosso objetivo.

@Composable
fun DragImage(url: String) {
   GlideImage(model = url, contentDescription = "Dragged Image")
}

Da mesma forma, crie a imagem de destino da ação de soltar.

@Composable
fun DropTargetImage(url: String) {
   val urlState = remember {mutableStateOf(url)}
   GlideImage(model = urlState.value, contentDescription = "Dropped Image")
}

Adicione uma coluna combinável ao elemento combinável para incluir essas duas imagens.

Column {
   DragImage(url = getString(R.string.source_url))
   DropTargetImage(url = getString(R.string.target_url))
}

Neste estágio, temos MainActivity, que mostra duas imagens verticais. Esta tela vai aparecer.

5e12c26cb2ad1068.png

4. Como configurar a origem da ação de arrastar

Vamos adicionar um modificador para a origem de arrastar e soltar do elemento combinável DragImage.

modifier = Modifier.dragAndDropSource {
   detectTapGestures(
       onLongPress = {
           startTransfer(
               DragAndDropTransferData(
                   ClipData.newPlainText("image uri", url)
               )
           )
       }
   )
}

Aqui, adicionamos um modificador dragAndDropSource. O modificador dragAndDropSource permite arrastar e soltar qualquer elemento a que ele é aplicado. Ele representa visualmente o elemento arrastado como uma sombra da ação de arrastar.

O modificador dragAndDropSource fornece o PointerInputScope para detectar o gesto de arrastar. Usamos o PointerInputScope detectTapGesture para detectar longPress, que é nosso gesto de arrastar.

O método onLongPress inicia a transferência dos dados que estão sendo arrastados.

startTransfer inicia uma sessão de arrastar e soltar com o transferData como os dados a serem transferidos na conclusão do gesto. Ele coleta dados encapsulados no DragAndDropTransferData, que tem três campos.

  1. Clipdata:: dados reais a serem transferidos.
  2. flags: sinalizações para controlar a operação de arrastar e soltar.
  3. localState: estado local da sessão ao arrastar na mesma atividade.

ClipData é um objeto complexo que contém itens de diferentes tipos, incluindo texto, marcação, áudio, vídeo etc. Para os fins deste codelab, estamos usando imageurl como um item em ClipData.

Agora nossa visualização pode ser arrastada.

415dcef002492e61.gif

5. Configurar para soltar

Para que a visualização aceite o item que foi solto, ela precisa adicionar dragAndDropTarget modifier

Modifier.dragAndDropTarget(
   shouldStartDragAndDrop = {
       // condition to accept dragged item
   },
   target = // DragAndDropTarget
   )
)

dragAndDropTarget é o modificador que permite que os dados sejam arrastados no elemento combinável. Esse modificador tem dois parâmetros:

  1. shouldStartDragAndDrop: permite que o elemento combinável decida se quer receber de uma determinada sessão de arrastar e soltar, inspecionando o DragAndDropEvent que iniciou a sessão.
  2. target: o DragAndDropTarget que vai receber eventos para uma determinada sessão de arrastar e soltar.

Vamos adicionar uma condição quando quisermos transmitir um evento de arrastar para o DragAndDropTarget.

shouldStartDragAndDrop = { event ->
   event.mimeTypes()
       .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
}

A condição adicionada aqui permite uma ação de soltar apenas quando pelo menos um dos itens arrastados é texto simples. O destino de soltar não será ativado se nenhum dos itens for texto simples.

Para parâmetros de destino, vamos criar um objeto de DragAndDropTarget que gerencia a sessão de soltar.

val dndTarget = remember{
   object : DragAndDropTarget{
       // handle Drag event
   }
}

DragAndDropTarget tem um callback que precisa ser substituído em cada etapa da sessão de arrastar e soltar.

  1. onDrop: um item foi solto dentro desse DragAndDropTarget e retorna "true" (verdadeiro) para indicar que o DragAndDropEvent foi consumido. "false" (falso) indica que foi rejeitado.
  2. onStarted: uma sessão de arrastar e soltar acabou de ser iniciada, e o DragAndDropTarget está qualificado para recebê-la. Isso dá a oportunidade de definir o estado de um DragAndDropTarget em preparação para consumir uma sessão de arrastar e soltar.
  3. onEntered: um item que está sendo solto entrou nos limites desse DragAndDropTarget.
  4. onMoved: um item que está sendo solto foi movido dentro dos limites do DragAndDropTarget.
  5. onExited: um item que está sendo descartado foi movido para fora dos limites de DragAndDropTarget.
  6. onChanged: um evento na sessão atual de arrastar e soltar mudou nos limites de DragAndDropTarget. Talvez uma tecla modificadora tenha sido pressionada ou solta.
  7. onEnded: a sessão de arrastar e soltar foi concluída. Todas as instâncias de DragAndDropTarget na hierarquia que receberam um evento onStarted antes vão receber esse evento. Isso dá uma oportunidade de redefinir o estado de um DragAndDropTarget.

Vamos definir o que acontece quando um item é solto no elemento combinável de destino.

override fun onDrop(event: DragAndDropEvent): Boolean {
   val draggedData = event.toAndroidDragEvent().clipData.getItemAt(0).text
   urlState.value = draggedData.toString()
   return true
}

Na função onDrop, extraímos o item ClipData e atribuímos ao URL da imagem, além de retornar o valor "true" para indicar que a ação de soltar foi processada corretamente.

Não atribua esta instância DragAndDropTarget ao parâmetro de destino do modificador dragAndDropTarget.

Modifier.dragAndDropTarget(
   shouldStartDragAndDrop = { event ->
       event.mimeTypes()
           .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
   },
   target = dndTarget
)

Ótimo! Já podemos realizar a operação de arrastar e soltar.

277ed56f80460dec.gif

Embora tenhamos adicionado a funcionalidade de arrastar e soltar, visualmente é difícil entender o que está acontecendo. Vamos mudar isso.

Para o elemento combinável de destino de soltar, vamos aplicar um ColorFilter à nossa imagem.

var tintColor by remember {
   mutableStateOf(Color(0xffE5E4E2))
}

Depois de definir a cor, vamos adicionar ColorFilter à nossa imagem.

GlideImage(
   colorFilter = ColorFilter.tint(color = backgroundColor,
       blendMode = BlendMode.Modulate),
   // other params
)

Queremos aplicar uma tonalidade à cor da imagem quando um item arrastado entra na área de destino de soltar. Para isso, podemos substituir o callback onEntered.

override fun onEntered(event: DragAndDropEvent) {
   super.onEntered(event)
   tintColor = Color(0xff00ff00)
}

Além disso, quando o usuário arrasta para fora da área de destino, precisamos usar o filtro de cor original. Para isso, temos que substituir o callback onExited.

override fun onExited(event: DragAndDropEvent) {
   super.onEntered(event)
   tintColor = Color(0xffE5E4E2)
}

Após a conclusão do processo de arrastar e soltar, também podemos voltar ao ColorFilter original.

override fun onEnded(event: DragAndDropEvent) {
   super.onEntered(event)
   tintColor = Color(0xffE5E4E2)
}

Por fim, o elemento combinável de soltar vai ficar assim:

@Composable
fun DropTargetImage(url: String) {
   val urlState = remember {
       mutableStateOf(url)
   }
   var tintColor by remember {
       mutableStateOf(Color(0xffE5E4E2))
   }
   val dndTarget = remember {
       object : DragAndDropTarget {
           override fun onDrop(event: DragAndDropEvent): Boolean {
               val draggedData = event.toAndroidDragEvent()
                   .clipData.getItemAt(0).text
               urlState.value = draggedData.toString()
               return true
           }

           override fun onEntered(event: DragAndDropEvent) {
               super.onEntered(event)
               tintColor = Color(0xff00ff00)
           }
           override fun onEnded(event: DragAndDropEvent) {
               super.onEntered(event)
               tintColor = Color(0xffE5E4E2)
           }
           override fun onExited(event: DragAndDropEvent) {
               super.onEntered(event)
               tintColor = Color(0xffE5E4E2)
           }

       }
   }
   GlideImage(
       model = urlState.value,
       contentDescription = "Dropped Image",
       colorFilter = ColorFilter.tint(color = tintColor,
           blendMode = BlendMode.Modulate),
       modifier = Modifier
           .dragAndDropTarget(
               shouldStartDragAndDrop = { event ->
                   event
                       .mimeTypes()
                       .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
               },
               target = dndTarget
           )
   )
}

Agora podemos adicionar indicações visuais para a operação de arrastar e soltar.

6be7e749d53d3e7e.gif

6. Parabéns!

O Compose para arrastar e soltar oferece uma interface fácil para implementar a funcionalidade usando modificadores na visualização.

Em resumo, você aprendeu a implementar o recurso de arrastar e soltar usando o Compose. Consulte a documentação.

Saiba mais