Cómo arrastrar y soltar en Compose

1. Antes de comenzar

En este codelab, se proporcionan instrucciones prácticas sobre los aspectos básicos de la implementación de la operación de arrastrar y soltar para Compose. Aprenderás a habilitar la función para arrastrar y soltar vistas tanto dentro de tu app como entre diferentes apps. Aprenderás a implementar una operación de arrastrar y soltar dentro de tu app, además de poder hacerlo en diferentes apps.

Requisitos previos

Para completar este codelab, necesitas lo siguiente:

Actividades

Crea una app simple con las siguientes características:

  • Configurar el elemento componible para que sea arrastrable con el modificador dragAndDropSource
  • Configurar el elemento componible para que sea destino para soltar con el modificador dragAndDropTarget

Requisitos

2. Un evento de arrastrar y soltar

Una operación de arrastrar y soltar puede considerarse como los 4 eventos en etapas, donde las etapas son las siguientes:

  1. Iniciada: El sistema inicia la operación de arrastrar y soltar en respuesta al gesto de arrastre del usuario.
  2. En curso: El usuario continúa arrastrando.
  3. Finalizada: El usuario suelta la función de arrastrar en el elemento componible de destino para soltar.
  4. En existencia: El sistema envía la señal para finalizar la operación de arrastrar y soltar.

El sistema envía el evento de arrastre en el objeto DragEvent. DragEvent puede contener los siguientes datos:

  1. ActionType: Es el valor de la acción del evento según el evento de ciclo de vida del evento de arrastrar y soltar (por ejemplo, ACTION_DRAG_STARTED ,ACTION_DROP, etcétera(
  2. ClipData: Datos que se arrastran y se encapsulan en el objeto ClipData
  3. ClipDescription: Metadatos sobre el objeto ClipData
  4. Result: Resultado de la operación de arrastrar y soltar
  5. X: Coordenada X de la ubicación actual del objeto arrastrado
  6. Y: Coordenada Y de la ubicación actual del objeto arrastrado

3. Configuración

Crea un proyecto nuevo y selecciona la plantilla "Empty Activity".

19da275afd995463.png

Deja todos los parámetros de configuración predeterminados.

En este codelab, usaremos ImageView para demostrar la funcionalidad de arrastrar y soltar. Agreguemos una dependencia de Gradle para la biblioteca Glide para Compose y sincronicemos el proyecto.

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

Ahora, en MainActivity.kt, crea un composable para Image, que actuará como fuente de arrastre para nuestro propósito.

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

Del mismo modo, crea la imagen de destino para soltar.

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

Agrega un elemento componible de columna al elemento componible para incluir estas dos imágenes.

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

En esta etapa, tenemos MainActivity, que muestra dos imágenes de forma vertical. Deberías poder ver esta pantalla.

5e12c26cb2ad1068.png

4. Configura la fuente de arrastre

Agreguemos un modificador para la fuente de arrastrar y soltar para nuestro elemento componible DragImage

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

Aquí, agregamos un modificador dragAndDropSource. El modificador dragAndDropSource habilita la funcionalidad de arrastrar y soltar para cualquier elemento al que se aplique. Representa visualmente el elemento arrastrado como una sombra de arrastre.

El modificador dragAndDropSource proporciona el elemento PointerInputScope para detectar el gesto de arrastre. Usamos detectTapGesture PointerInputScope para detectar acciones de longPress, que es nuestro gesto de arrastre.

Método onLongPress arrastrado, de cuyos datos estamos iniciando la transferencia.

startTransfer inicia una sesión de arrastrar y soltar con transferData como los datos que se transferirán cuando se complete un gesto. Toma datos encapsulados en DragAndDropTransferData, que tiene 3 campos.

  1. Clipdata: datos reales que se transferirán.
  2. flags: Parámetros para controlar la operación de arrastrar y soltar
  3. localState: Estado local de la sesión cuando se arrastra la misma actividad

ClipData es un objeto complejo que contiene elementos de diferentes tipos, como texto, marcas, audio y video, entre otros. Para los fines de este codelab, usamos imageurl como un elemento en ClipData.

Excelente, ahora se puede arrastrar nuestra vista.

415dcef002492e61.gif

5. Configura la acción de soltar

Para que la vista acepte el elemento soltado, se debe agregar dragAndDropTarget modifier.

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

dragAndDropTarget es el modificador que permite arrastrar datos en el elemento componible. Este modificador tiene dos parámetros:

  1. shouldStartDragAndDrop: Permite que el elemento componible decida si desea recibir de una sesión de arrastrar y soltar determinada con la inspección del evento DragAndDropEvent que inició la sesión.
  2. target: Se trata del DragAndDropTarget que recibirá eventos para una sesión determinada de arrastrar y soltar.

Agreguemos una condición cuando queramos pasar un evento de arrastre a DragAndDropTarget.

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

La condición que se agrega aquí es solo para permitir una acción de soltar cuando al menos uno de los elementos que se arrastran es texto sin formato. Si ninguno de los elementos es texto sin formato, el destino para soltar no se activará.

Para los parámetros de destino, permite crear un objeto de DragAndDropTarget que controla la sesión de la acción de soltar.

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

DragAndDropTarget tiene una devolución de llamada que se debe anular para cada etapa de la sesión de arrastrar y soltar.

  1. onDrop : Se soltó un elemento dentro de este DragAndDropTarget, muestra el valor "true" para indicar que se consumió DragAndDropEvent. El valor "false" indica que se rechazó.
  2. onStarted : Se acaba de iniciar una sesión de arrastrar y soltar, y este DragAndDropTarget es apto para recibirla. Esto brinda la oportunidad de establecer el estado de un DragAndDropTarget en preparación para consumir una sesión de arrastrar y soltar.
  3. onEntered : Un elemento en proceso de soltarse ingresó a los límites de este DragAndDropTarget.
  4. onMoved : Un elemento en proceso de soltarse se movió dentro de los límites de este DragAndDropTarget.
  5. onExited : Un elemento en proceso de soltarse se movió fuera de los límites de este DragAndDropTarget.
  6. onChanged : Se modificó un evento en la sesión actual de arrastrar y soltar dentro de los límites de DragAndDropTarget. Es posible que se haya presionado o soltado una tecla modificadora.
  7. onEnded : Se completó la sesión de arrastrar y soltar. Todas las instancias de DragAndDropTarget de la jerarquía que antes recibieron un evento onStarted recibirán este evento. Esto permite restablecer el estado de un DragAndDropTarget.

Definamos qué sucede cuando se suelta un elemento en un elemento componible de destino.

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

En la función onDrop, extraeremos el elemento ClipData y lo asignaremos a la URL de la imagen. También se muestra el valor "true" para indicar que la acción de soltar se controló correctamente.

No se debe asignar directamente esta instancia de DragAndDropTarget al parámetro de destino del modificador dragAndDropTarget.

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

¡Excelente! Ahora podemos realizar con éxito la operación de arrastrar y soltar.

277ed56f80460dec.gif

Si bien agregamos la función de arrastrar y soltar, es difícil comprender visualmente lo que sucede. Cambiemos eso.

Para el elemento componible de destino de soltar, apliquemos un ColorFilter a nuestra imagen.

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

Después de definir el color de tono, agreguemos ColorFilter a nuestra imagen.

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

Queremos aplicar un tono al color de la imagen cuando un elemento arrastrado ingresa al área de destino para soltar. Para ello, podemos anular la devolución de llamada onEntered.

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

Además, cuando el usuario realiza un arrastre fuera del área de destino, deberíamos recurrir al filtro de color original. Para ello, tenemos que anular la devolución de llamada onExited.

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

Cuando se completa correctamente la acción de arrastrar y soltar, también podemos volver al ColorFilter original.

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

Por último, el elemento componible de soltar tendrá el siguiente aspecto:

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

¡Genial! Podemos agregar indicadores visuales para la operación de arrastrar y soltar.

6be7e749d53d3e7e.gif

6. ¡Felicitaciones!

Compose for Drag and Drop proporciona una interfaz fácil para implementar la funcionalidad de arrastrar y soltar en Compose con modificadores para la vista.

En resumen, aprendiste a implementar la función de arrastrar y soltar con Compose. Explora más la documentación.

Más información