Codelab sobre arrastrar y soltar

1. Antes de comenzar

En este codelab, se proporcionan instrucciones prácticas sobre los aspectos básicos de la implementación de la funcionalidad de arrastrar y soltar para vistas. 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 interacciones de arrastrar y soltar dentro de tu app e incluso en diferentes apps. El codelab te servirá como guía en el uso de DropHelper para habilitar la función de arrastrar y soltar, personalizar la respuesta visual durante el arrastre con ShadowBuilder, agregar permisos para el arrastre entre apps e implementar un receptor de contenido que funcione de manera universal.

Requisitos previos

Para completar este codelab, necesitas lo siguiente:

Actividades

Crea una app simple con las siguientes características:

  • Implementa la funcionalidad de arrastrar y soltar con DragStartHelper y DropHelper.
  • Cambia el ShadowBuilder.
  • Agrega permiso para arrastrar entre apps.
  • Implementa el receptor de contenido de alcance para la implementación universal.

Requisitos

2. Un evento de arrastrar y soltar

Un proceso de arrastrar y soltar puede verse como el evento de 4 etapas, donde las etapas son las siguientes:

  1. Inicio: El sistema inicia la operación de arrastrar y soltar en respuesta al gesto de arrastre del usuario.
  2. Continuación: El usuario continúa arrastrando y se activa el compilador de sombras de arrastre cuando se ingresa a la vista de destino.
  3. Finalización: El usuario libera el arrastre dentro del cuadro delimitador de un destino de soltar en el área de destino para soltar.
  4. 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. El objeto DragEvent puede contener los siguientes datos:

  1. ActionType: Es el valor de la acción del evento en función del evento de ciclo de vida del evento de arrastrar y soltar; p. ej., ACTION_DRAG_STARTED, ACTION_DROP, etcétera.
  2. ClipData: Son datos que se arrastran. Se encapsulan en el objeto ClipData.
  3. ClipDescription: Son metadatos sobre el objeto ClipData.
  4. Result: Es el resultado de la operación de arrastrar y soltar.
  5. X: Es la coordenada X de la ubicación actual del objeto arrastrado.
  6. Y: Es la coordenada Y de la ubicación actual del objeto arrastrado.

3. Configuración

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

2fbd2bca1483033f.png

Deja todos los parámetros de configuración predeterminados. Permite que el proyecto se sincronice y se indexe. Verás que se creó MainActivity.kt junto con la vista activity_main.xml.

4. Arrastrar y soltar con vistas

En string.xml, agreguemos algunos valores de cadena

<resources>
    <string name="app_name">DragAndDropCodelab</string>
    <string name="drag_image">Drag Image</string>
    <string name="drop_image">drop image</string>
 </resources>

Abre el archivo fuente activity_main.xml y modifica el diseño para incluir dos ImageViews: uno actuará como fuente de arrastrar y el otro como destino de soltar.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_greeting"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/iv_source"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_source"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="@string/drag_image"
        app:layout_constraintBottom_toTopOf="@id/iv_target"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_greeting" />

    <ImageView
        android:id="@+id/iv_target"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="@string/drop_image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iv_source" />
</androidx.constraintlayout.widget.ConstraintLayout>

En build.gradle.kts, habilita la vinculación de vista.

buildFeatures{
   viewBinding = true
}

En build.gradle.kts, agrega una dependencia para Glide.

dependencies {
    implementation("com.github.bumptech.glide:glide:4.16.0")
    annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
    
    //other dependencies
}

Agrega URLs de imágenes y texto de saludo en string.xml

<string name="greeting">Drag and Drop</string>
<string name="target_url">https://services.google.com/fh/files/misc/qq2.jpeg</string>
<string name="source_url">https://services.google.com/fh/files/misc/qq10.jpeg</string>

En MainActivity.kt, inicializaremos las vistas.

class MainActivity : AppCompatActivity() {
   val binding by lazy(LazyThreadSafetyMode.NONE) {
       ActivityMainBinding.inflate(layoutInflater)
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(binding.root)
       binding.tvGreeting.text = getString(R.string.greeting)
       Glide.with(this).asBitmap()
           .load(getString(R.string.source_url))
           .into(binding.ivSource)
       Glide.with(this).asBitmap()
           .load(getString(R.string.target_url))
           .into(binding.ivTarget)
   }
}

En este estado, tu app debería mostrar texto de saludo y dos imágenes en orientación vertical.

b0e651aaee336750.png

5. Haz que la vista sea arrastrable

Para que una vista en particular sea arrastrable, debe implementar el método startDragAndDrop() en el gesto de arrastre.

Implementemos una devolución de llamada para onLongClickListener mientras el usuario inicia el arrastre en la vista.

draggableView.setOnLongClickListener{ v ->
   //drag logic here
   true
}

Incluso si no se puede hacer clic en la vista durante mucho tiempo, esta devolución de llamada hace que se le pueda hacer clic durante un largo tiempo. Un valor que se muestra es booleano. Si el valor es verdadero, la devolución de llamada consume el arrastre.

Prepara ClipData: datos que se arrastrarán

Definamos los datos que queremos soltar. Los datos pueden ser de cualquier tipo, desde texto simple hasta video. Estos datos se encapsulan en el objeto ClipData. Conservación de objeto ClipData de uno o más ClipItem complejos.

Con diferentes tipos de MIME definidos en ClipDescription.

Arrastraremos la URL de imagen de la vista de origen. Hay 3 componentes principales de ClipData

  1. Etiqueta: Texto simple para mostrar al usuario lo que se arrastra.
  2. Tipo de MIME: Tipo de MIME de los elementos que se arrastran.
  3. ClipItem: Elemento que se arrastrará encapsulado en el objeto ClipData.Item.

Crearemos ClipData.

val label = "Dragged Image Url"
val clipItem = ClipData.Item(v.tag as? CharSequence)
val mimeTypes = arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)
val draggedData = ClipData(
   label, mimeTypes, clipItem
)

Inicia arrastrar y soltar

Ahora que ya estamos listos con los datos para arrastrar, iniciemos el arrastre. Para ello, usaremos startDragAndDrop.

El método startDragAndDrop toma 4 argumentos.

  1. Datos: datos que se arrastran en forma de ClipData..
  2. shadowBuilder: DragShadowBuilder para compilar la sombra de la vista.
  3. myLocalState: Es un objeto que contiene datos locales sobre la operación de arrastrar y soltar. Cuando se despachen eventos de arrastre a vistas en la misma actividad, este objeto estará disponible a través de DragEvent.getLocalState().
  4. Parámetros: parámetros para controlar las operaciones de arrastrar y soltar.

Una vez que se llama a esta función, según la clase View.DragShadowBuilder, se dibuja la sombra de arrastre. Una vez que el sistema tenga la sombra de arrastre, se iniciará la operación de arrastrar y soltar. Para ello, se enviará el evento a la vista visible que implementó la interfaz OnDragListener.

v.startDragAndDrop(
   draggedData,
   View.DragShadowBuilder(v),
   null,
   0
)

Con esto, configuramos nuestra vista para el arrastre y los datos que se arrastrarán. La implementación final tiene el siguiente aspecto:

fun setupDrag(draggableView: View) {
   draggableView.setOnLongClickListener { v ->
       val label = "Dragged Image Url"
       val clipItem = ClipData.Item(v.tag as? CharSequence)
       val mimeTypes = arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)
       val draggedData = ClipData(
           label, mimeTypes, clipItem
       )
       v.startDragAndDrop(
           draggedData,
           View.DragShadowBuilder(v),
           null,
           0
       )
   }
}

En esta etapa, deberías poder arrastrar la vista con un clic largo.

526e9e2a7f3a90ea.gif

Continuemos con la configuración de la vista soltada.

6. Configura la vista para DropTarget

La vista puede actuar como destino de la acción de soltar dado que implementó la interfaz OnDragListener.

Configuremos la segunda vista de imagen para que sea un destino para soltar.

private fun setupDrop(dropTarget: View) {
   dropTarget.setOnDragListener { v, event ->
       // handle drag events here
       true
   }
}

Anularemos el método onDrag de la interfaz OnDragListener. El método onDrag tiene 2 argumentos.

  1. Vista que recibió el evento de arrastre
  2. Objeto de evento para el evento de arrastre

Este método muestra el valor true si el evento de arrastre se controla correctamente; de lo contrario, muestra el valor "false".

DragEvent

Representa un paquete de datos que transmite el sistema en diferentes etapas de una operación de arrastrar y soltar. Este paquete de datos encapsula información vital sobre la operación y los datos implicados.

DragEvent tiene diferentes acciones de arrastre según la etapa de la operación de arrastrar y soltar.

  1. ACTION_DRAG_STARTED: Indica el inicio de la operación de arrastrar y soltar.
  2. ACTION _DRAG_LOCATION: Indica que el usuario soltó el arrastre en el estado ingresado; es decir, no en el límite del área de destino para soltar.
  3. ACTION_DRAG_ENTERED: Indica que la vista arrastrada está dentro de los límites de la vista para soltar de destino.
  4. ACTION_DROP: Indica que el usuario liberó el arrastre en el área de soltar de destino.
  5. ACTION_DRAG_ENDED: Indica que finalizó la operación de arrastrar y soltar.
  6. ACTION_DRAG_EXITED: Indica el final de la operación de arrastrar y soltar.

Valida DragEvent

Puedes continuar con la operación de arrastrar y soltar si se cumplen todas tus restricciones en el evento ACTION_DRAG_STARTED. Por ejemplo, en este ejemplo, podemos verificar si los datos entrantes son del tipo correcto o no.

DragEvent.ACTION_DRAG_STARTED -> {
   Log.d(TAG, "ON DRAG STARTED")
   if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
       (v as? ImageView)?.alpha = 0.5F
       v.invalidate()
       true
   } else {
       false
   }
}

En este ejemplo, verificamos si el elemento ClipDescription del evento tiene el tipo de MIME aceptable o no. Si es así, se proporciona la señal visual para indicar lo mismo y mostrar un valor verdadero, lo que indica que se están controlando los datos arrastrados. De lo contrario, se muestra el valor "false" para indicar que la vista de destino para soltar descarta el arrastre.

Maneja datos de soltar

En el evento ACTION_DROP, podemos elegir qué hacer con los datos que se soltaron. En este ejemplo, extraeremos la URL que agregamos a ClipData como texto. Colocaremos esta imagen de la URL en la vista de imagen de destino.

DragEvent.ACTION_DROP -> {
   Log.d(TAG, "On DROP")
   val item: ClipData.Item = event.clipData.getItemAt(0)
   val dragData = item.text
   Glide.with(this).load(item.text).into(v as ImageView)
   (v as? ImageView)?.alpha = 1.0F
   true
}

Además de controlar la acción de soltar, podemos configurar lo que sucede cuando un usuario arrastra la vista en el cuadro de límite de la vista para soltar de destino y qué sucede cuando arrastra la vista fuera del área de destino.

Agregaremos algunas marcas visuales cuando el elemento arrastrado ingrese al área de destino

DragEvent.ACTION_DRAG_ENTERED -> {
   Log.d(TAG, "ON DRAG ENTERED")
   (v as? ImageView)?.alpha = 0.3F
   v.invalidate()
   true
}

Además, agrega más indicadores visuales cuando el usuario arrastre la vista fuera del cuadro de límite de la vista de destino para soltar.

DragEvent.ACTION_DRAG_EXITED -> {
   Log.d(TAG, "ON DRAG EXISTED")
   (v as? ImageView)?.alpha = 0.5F
   v.invalidate()
   true
}

Agrega algunas señales visuales más para indicar el final de la operación de arrastrar y soltar.

DragEvent.ACTION_DRAG_ENDED -> {
   Log.d(TAG, "ON DRAG ENDED")
   (v as? ImageView)?.alpha = 1.0F
   true
}

En esta etapa, deberías poder arrastrar una imagen a la vista de imagen de destino; una vez que se suelte la imagen del objeto ImageView de destino, se reflejará el cambio.

114238f666d84c6f.gif

7. Arrastrar y soltar en el modo multiventana

Los elementos se pueden arrastrar de una app a otra, ya que las apps comparten la pantalla a través del modo multiventana. La implementación para habilitar la función de arrastrar y soltar entre apps es la misma, con la excepción de que se deben agregar parámetros durante la acción de arrastrar y soltar.

Configura parámetros durante el arrastre

Como recordamos, startDragAndDrop tiene un argumento para especificar los parámetros, lo que controla la operación de arrastrar y soltar.

v.startDragAndDrop(
   draggedData,
   View.DragShadowBuilder(v),
   null,
   View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
)

View.DRAG_FLAG_GLOBAL indica que el arrastre puede cruzar los límites de la ventana y View.DRAG_FLAG_GLOBAL_URI_READ indica que el destinatario de la función de arrastrar puede leer los URI de contenido.

Para que el destino de la función Drop Target lea los datos arrastrados de otras apps, la vista de destino de la función debe declarar el permiso de lectura.

val dropPermission = requestDragAndDropPermissions(event)

Además, libera el permiso una vez que se hayan controlado los datos arrastrados.

dropPermission.release()

El manejo final del elemento arrastrado tiene el siguiente aspecto:

DragEvent.ACTION_DROP -> {
   Log.d(TAG, "On DROP")
   val dropPermission = requestDragAndDropPermissions(event)
   val item: ClipData.Item = event.clipData.getItemAt(0)
   val dragData = item.text
   Glide.with(this).load(item.text).into(v as ImageView)
   (v as? ImageView)?.alpha = 1.0F
   dropPermission.release()
   true
}

En esta etapa, deberías poder arrastrar esta imagen a otra app; también los datos arrastrados desde otra app se pueden controlar correctamente.

8. Biblioteca de arrastrar y soltar

Jetpack proporciona una biblioteca DragAndDrop para simplificar la implementación de la operación de arrastrar y soltar.

Agregaremos una dependencia en build.gradle.kts para usar la biblioteca DragAndDrop.

implementation("androidx.draganddrop:draganddrop:1.0.0")

Para este ejercicio, crea un objeto Activity independiente llamado DndHelperActivity.kt que tenga 2 elementos ImageView en forma vertical, uno de ellos funcionará como fuente de arrastre y otro será el destino para soltar.

Modifica strings.xml para agregar recursos de cadenas.

<string name="greeting_1">DragStartHelper and DropHelper</string>
<string name="target_url_1">https://services.google.com/fh/files/misc/qq9.jpeg</string>
<string name="source_url_1">https://services.google.com/fh/files/misc/qq8.jpeg</string>

Actualiza activity_dnd_helper.xml para incluir ImageViews.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:padding="24dp"
   tools:context=".DnDHelperActivity">

   <TextView
       android:id="@+id/tv_greeting"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toTopOf="@id/iv_source"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <ImageView
       android:id="@+id/iv_source"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drag_image"
       app:layout_constraintBottom_toTopOf="@id/iv_target"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/tv_greeting" />

   <ImageView
       android:id="@+id/iv_target"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drop_image"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/iv_source" />
</androidx.constraintlayout.widget.ConstraintLayout>

Finalmente, inicializa vistas en DnDHelperActivity.kt.

class DnDHelperActivity : AppCompatActivity() {
   private val binding by lazy(LazyThreadSafetyMode.NONE) {
       ActivityMainBinding.inflate(layoutInflater)
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(binding.root)
       binding.tvGreeting.text = getString(R.string.greeting)
       Glide.with(this).asBitmap()
           .load(getString(R.string.source_url_1))
           .into(binding.ivSource)
       Glide.with(this).asBitmap()
           .load(getString(R.string.target_url_1))
           .into(binding.ivTarget)
       binding.ivSource.tag = getString(R.string.source_url_1)
   }
}

Asegúrate de actualizar AndroidManifest.xml para que DndHelperActivity sea Launcher Activity.

<activity
   android:name=".DnDHelperActivity"
   android:exported="true">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>

DragStartHelper

Anteriormente, configuramos la vista para que sea arrastrable implementando onLongClickListener y llamando a startDragAndDrop. DragStartHelper simplifica la implementación al proporcionar métodos de utilidades

DragStartHelper(draggableView)
{ view: View, _: DragStartHelper ->
   // prepare clipData

   // startDrag and Drop
}.attach()

DragStartHelper toma la vista que se arrastrará como argumento. Aquí, implementamos el método OnDragStartListener, en el que prepararemos los datos del clip e iniciaremos la operación de arrastrar y soltar.

La implementación final tiene el siguiente aspecto:

DragStartHelper(draggableView)
{ view: View, _: DragStartHelper ->
   val item = ClipData.Item(view.tag as? CharSequence)
   val dragData = ClipData(
       view.tag as? CharSequence,
       arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
       item
   )
   view.startDragAndDrop(
       dragData,
       View.DragShadowBuilder(view),
       null,
       0
   )
}.attach()

DropHelper

DropHelper simplifica la configuración de la vista drop de destino proporcionando un método de utilidad llamado configureView.

configureView toma 4 argumentos

  1. Actividad: la actividad actual
  2. dropTarget: la vista que se configura
  3. mimeTypes: los tipos de MIME de los elementos de datos que se sueltan
  4. Interfaz de OnReceiveContentListener para controlar los datos soltados

Personaliza lo más destacado del destino para soltar.

DropHelper.configureView(
   This, // Current Activity
   dropTarget,
   arrayOf("text/*"),
   DropHelper.Options.Builder().build()
) {
   // handle the dropped data
}

OnRecieveContentListener recibe el contenido que se soltó. Esto tiene dos parámetros:

  1. Vista: donde se suelta el contenido
  2. Carga útil: el contenido real que se soltará
private fun setupDrop(dropTarget: View) {
   DropHelper.configureView(
       this,
       dropTarget,
       arrayOf("text/*"),
   ) { _, payload: ContentInfoCompat ->
       // TODO: step through clips if one cannot be loaded
       val item = payload.clip.getItemAt(0)
       val dragData = item.text
       Glide.with(this)
           .load(dragData)
           .centerCrop().into(dropTarget as ImageView)
       // Consume payload by only returning remaining items
       val (_, remaining) = payload.partition { it == item }
       remaining
   }
}

En esta etapa, deberías poder arrastrar y soltar datos con DragStartHelper y DropHelper.

2e32d6cd80e19dcb.gif

Configura los aspectos destacados del área de soltar

Como ya pudiste observar, cuando un elemento arrastrado entra en el área de soltar, esta área se destaca. Con DropHelper.Options, podemos personalizar la manera en que se destaca el área de la función de soltar cuando un elemento arrastrado ingresa al límite de la vista.

DropHelper.Options se puede usar para configurar el color de resaltado y el radio de las esquinas del área de destino para soltar.

DropHelper.Options.Builder()
   .setHighlightColor(getColor(R.color.green))
   .setHighlightCornerRadiusPx(16)
   .build()

Estas opciones se deben pasar como argumentos al método configureView de DropHelper.

private fun setupDrop(dropTarget: View) {
   DropHelper.configureView(
       this,
       dropTarget,
       arrayOf("text/*"),
       DropHelper.Options.Builder()
           .setHighlightColor(getColor(R.color.green))
           .setHighlightCornerRadiusPx(16)
           .build(),
   ) { _, payload: ContentInfoCompat ->
       // TODO: step through clips if one cannot be loaded
       val item = payload.clip.getItemAt(0)
       val dragData = item.text
       Glide.with(this)
           .load(dragData)
           .centerCrop().into(dropTarget as ImageView)
       // Consume payload by only returning remaining items
       val (_, remaining) = payload.partition { it == item }
       remaining
   }
}

Deberías poder ver el color y el radio de resaltado mientras arrastras y sueltas.

9d5c1c78ecf8575f.gif

9. Recibe contenido enriquecido

OnReceiveContentListener es la API unificada para recibir contenido enriquecido, como texto, HTML, imágenes y videos, entre otros. El contenido se puede insertar en las vistas desde el teclado, con arrastre o desde el portapapeles. Mantener la devolución de llamada para cada mecanismo de entrada puede ser molesto. Se puede usar OnReceiveContentListener para recibir contenido como texto, marca, audio, video, imágenes y otros a través de una sola API. La API de OnReceiveContentListener consolida estas instrucciones de código diferentes creando una sola API para que puedas enfocarte en la lógica específica de tu app y permitir que la plataforma controle el resto.

Para este ejercicio, crea un objeto Activity independiente llamado ReceiveRichContentActivity.kt que tenga 2 elementos ImageView en forma vertical, uno de ellos funcionará como fuente de arrastre y otro será el destino para soltar.

Modifica strings.xml para agregar recursos de cadenas.

<string name="greeting_2">Rich Content Receiver</string>
<string name="target_url_2">https://services.google.com/fh/files/misc/qq1.jpeg</string>
<string name="source_url_2">https://services.google.com/fh/files/misc/qq3.jpeg</string>

Actualiza activity_receive_rich_content.xml para incluir ImageViews.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".ReceiveRichContentActivity">

   <TextView
       android:id="@+id/tv_greeting"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toTopOf="@id/iv_source"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <ImageView
       android:id="@+id/iv_source"
       android:layout_width="320dp"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drag_image"
       app:layout_constraintBottom_toTopOf="@id/iv_target"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/tv_greeting" />

   <ImageView
       android:id="@+id/iv_target"
       android:layout_width="320dp"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drop_image"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/iv_source" />
</androidx.constraintlayout.widget.ConstraintLayout>

Finalmente, inicializa vistas en ReceiveRichContentActivity.kt.

class ReceiveRichContentActivity : AppCompatActivity() {
   private val binding by lazy(LazyThreadSafetyMode.NONE) {
       ActivityReceiveRichContentBinding.inflate(layoutInflater)
   }
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(binding.root)
       binding.tvGreeting.text = getString(R.string.greeting_2)
       Glide.with(this).asBitmap()
           .load(getString(R.string.source_url_2))
           .into(binding.ivSource)
       Glide.with(this).asBitmap()
           .load(getString(R.string.target_url_2))
           .into(binding.ivTarget)
       binding.ivSource.tag = getString(R.string.source_url_2)
   }
}

Asegúrate de actualizar AndroidManifest.xml para que DndHelperActivity sea Launcher Activity

<activity
   android:name=".ReceiveRichContentActivity"
   android:exported="true">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>

Primero, crearemos una devolución de llamada que implemente OnReceiveContentListener.

val listener = OnReceiveContentListener { view, payload ->
   val (textContent, remaining) =
       payload.partition { item: ClipData.Item -> item.text != null }
   if (textContent != null) {
       val clip = textContent.clip
       for (i in 0 until clip.itemCount) {
           val currentText = clip.getItemAt(i).text
           Glide.with(this)
               .load(currentText)
               .centerCrop().into(view as ImageView)
       }
   }
   remaining
}

Aquí, implementamos la interfaz OnRecieveContentListener. El método onRecieveContent tiene 2 argumentos.

  1. Vista actual que recibe los datos
  2. Carga útil de datos desde el teclado, el arrastre o el portapapeles en forma de ContentInfoCompat

Este método muestra la carga útil que no se controla.

Aquí, usamos el método de partición para separar la carga útil en contenido de texto y otro contenido. Manejamos los datos de texto según nuestras necesidades y devolvemos la carga útil restante.

Manejamos lo que queremos hacer con los datos arrastrados.

val listener = OnReceiveContentListener { view, payload ->
   val (textContent, remaining) =
       payload.partition { item: ClipData.Item -> item.text != null }
   if (textContent != null) {
       val clip = textContent.clip
       for (i in 0 until clip.itemCount) {
           val currentText = clip.getItemAt(i).text
           Glide.with(this)
               .load(currentText)
               .centerCrop().into(view as ImageView)
       }
   }
   remaining
}

Ahora nuestro objeto de escucha está listo. Agregaremos este objeto de escucha a la vista de destino.

ViewCompat.setOnReceiveContentListener(
   binding.ivTarget,
   arrayOf("text/*"),
   listener
)

En esta etapa, deberías poder arrastrar una imagen y soltarla en el área objetivo. Una vez que se suelte, la imagen arrastrada debe reemplazar la imagen original en la vista de destino para soltar.

e4c3a3163c51135d.gif

10. ¡Felicitaciones!

Ahora sabes implementar el método de arrastrar y soltar en tu app para Android. A través de este codelab, aprendiste a crear interacciones interactivas de arrastrar y soltar dentro de tu app para Android y en diferentes apps, lo que mejora la experiencia del usuario y la funcionalidad. Aprendiste lo siguiente:

  • Aspectos básicos de arrastrar y soltar: Comprender las 4 etapas de un evento de arrastrar y soltar (inicio, continuación, finalización y cierre) y los datos clave del objeto DragEvent
  • Habilitación de la función de arrastrar y soltar: Hacer que una vista sea arrastrable y controlar la operación de soltar en la vista de destino controlando DragEvent
  • Operación de arrastrar y soltar en el modo multiventana: Habilitar la operación de arrastrar y soltar entre apps configurando parámetros y permisos adecuados
  • Uso de la biblioteca DragAndDrop: Simplificar la implementación de la función de arrastrar y soltar con la biblioteca de Jetpack.
  • Recepción de contenido enriquecido: Implementar el control de diversos tipos de contenido (texto, imágenes, videos, etc.) de varios métodos de entrada a través de una API unificada.

Más información