Cómo arrastrar y soltar

Con el framework para arrastrar y soltar de Android, puedes permitir que los usuarios muevan datos con gestos interactivos de arrastrar y soltar. Los usuarios pueden arrastrar texto, imágenes, objetos (cualquier contenido que se pueda representar con un URI) de un View a otro dentro de una app o, en el modo multiventana, entre apps.

Imagen y string de texto que se arrastran y se sueltan en una app Imagen y string de texto que se arrastran y se sueltan entre apps en el modo de pantalla dividida
Figura 1: Arrastra y suelta dentro de una app.
Figura 2: Arrastra y suelta entre apps.

El framework incluye una clase de evento de arrastre, objetos de escucha de arrastre y clases y métodos auxiliares. Aunque está diseñado principalmente para permitir la transferencia de datos, el framework se puede usar en otras acciones de la IU. Por ejemplo, puedes crear una app que combine colores cuando el usuario arrastre un ícono de color sobre otro ícono. En el resto de esta guía, sin embargo, se describe el framework de arrastrar y soltar en el contexto de transferencia de datos.

Resumen

Una operación de arrastrar y soltar comienza cuando el usuario hace un gesto de IU que tu app reconoce como una señal para comenzar a arrastrar datos. En respuesta, la app notifica al sistema que se inicia una operación de arrastrar y soltar. El sistema vuelve a llamar a tu app para obtener una representación de los datos que se arrastran (una sombra de arrastre). A medida que el usuario mueve la sombra de arrastre sobre el diseño de la app, el sistema envía eventos de arrastre a los objetos de escucha de eventos de arrastre y los métodos de devolución de llamada asociados con los objetos View del diseño. Si el usuario suelta la sombra de arrastre sobre una vista que puede aceptar los datos (un destino para soltar), el sistema envía los datos al destino. La operación de arrastrar y soltar finaliza cuando el usuario suelta la sombra de arrastre sin importar si lo hace sobre un destino para soltar.

Para crear un objeto de escucha de evento de arrastre, implementa View.OnDragListener. Configura el objeto de escucha en un destino para soltar con el método setOnDragListener() del objeto View. Cada vista del diseño también tiene un método de devolución de llamada onDragEvent().

Tu aplicación notifica al sistema para que inicie una operación de arrastrar y soltar llamando al método startDragAndDrop(), que le indica al sistema que comience a enviar eventos de arrastre. El método también proporciona al sistema los datos que arrastra el usuario y los metadatos que describen los datos. Puedes llamar a startDragAndDrop() en cualquier View del diseño actual. El sistema usa el objeto View solo para obtener acceso a la configuración global del diseño.

Durante la operación de arrastrar y soltar, el sistema envía eventos de arrastre a los objetos de escucha de eventos de arrastre o métodos de devolución de llamada de los objetos View del diseño. Los objetos de escucha o los métodos de devolución de llamada usan los metadatos para decidir si quieren aceptar los datos cuando se descartan. Si el usuario suelta los datos en un destino para soltar (un objeto View que aceptará los datos), el sistema envía un objeto de evento de arrastre que contiene los datos al objeto de escucha de eventos de arrastre o al método de devolución de llamada del destino para soltar.

Objetos de escucha de eventos de arrastre y métodos de devolución de llamada

Un objeto View recibe eventos de arrastre con un objeto de escucha de eventos de arrastre que implementa View.OnDragListener o con el método de devolución de llamada onDragEvent() de la vista. Cuando el sistema llama al método o al objeto de escucha, proporciona un argumento DragEvent.

En la mayoría de los casos, es preferible usar un objeto de escucha que usar el método de devolución de llamada. Cuando se diseñan IU, por lo general, no se crean subclases de clases View, pero el uso del método de devolución de llamada te obliga a crear subclases para anular el método. En comparación, puedes implementar una clase de objeto de escucha y, luego, usarla con varios objetos View diferentes. También puedes implementarlo como una clase intercalada anónima o una expresión lambda. Para configurar el objeto de escucha de un objeto View, llama a setOnDragListener().

Como alternativa, la implementación predeterminada de onDragEvent() se puede modificar sin anular el método. Si configuras un OnReceiveContentListener en una vista (consulta setOnReceiveContentListener()), el método onDragEvent() hace lo siguiente de forma predeterminada:

  • El resultado es verdadero en respuesta a la llamada a startDragAndDrop().
  • Llama a performReceiveContent() si los datos de la función de arrastrar y soltar aparecen en la vista.

    Los datos se pasan al método como un objeto ContentInfo. El método invoca a OnReceiveContentListener.

  • El resultado es verdadero si los datos de la función de arrastrar y soltar se sueltan en la vista y el objeto OnReceiveContentListener consume el contenido.

Define el objeto OnReceiveContentListener para controlar los datos específicos de tu app. Para obtener una retrocompatibilidad hasta el nivel de API 24, usa la versión de Jetpack de OnReceiveContentListener.

Puedes tener un objeto de escucha de eventos de arrastre y un método de devolución de llamada para un objeto View, en cuyo caso el sistema primero llama al objeto de escucha. El sistema no llama al método de devolución de llamada a menos que el objeto de escucha muestre false.

La combinación del método onDragEvent() y View.OnDragListener es análoga a la combinación de onTouchEvent() y View.OnTouchListener que se usa con los eventos táctiles.

Proceso de arrastrar y soltar

Básicamente, el proceso de arrastrar y soltar tiene cuatro pasos o estados: Iniciado, Continuado, Soltado y Finalizado.

Iniciado

En respuesta al gesto de arrastre de un usuario, tu aplicación llama a startDragAndDrop() para indicarle al sistema que inicie una operación de arrastrar y soltar. Los argumentos del método proporcionan lo siguiente:

  • Los datos que se arrastrarán
  • Una devolución de llamada para dibujar la sombra de arrastre
  • Metadatos que describen los datos arrastrados

Primero, el sistema responde mediante una llamada a la app para obtener una sombra de arrastre. Luego, muestra la sombra de arrastre en el dispositivo.

A continuación, el sistema envía un evento de arrastre con el tipo de acción ACTION_DRAG_STARTED al objeto de escucha de eventos de arrastre de todos los elementos View del diseño actual. Para seguir recibiendo eventos de arrastre, incluido un posible evento de soltar, un objeto de escucha de evento de arrastre debe mostrar true. Esto registra el objeto de escucha con el sistema. Solo los receptores registrados continúan recibiendo eventos de arrastre. En este punto, los objetos de escucha también pueden cambiar el aspecto de su objeto View del destino para soltar a fin de mostrar que el objeto View puede aceptar un evento de soltar.

Si el objeto de escucha de eventos de arrastre muestra false, no recibirá eventos de arrastre para la operación actual hasta que el sistema envíe un evento de arrastre con el tipo de acción ACTION_DRAG_ENDED. Al mostrar false, el objeto de escucha le indica al sistema que no le interesa la operación de arrastrar y soltar, y que no quiere aceptar los datos arrastrados.

Continuado

El usuario continúa el arrastre. A medida que la sombra de arrastre se cruza con el cuadro de límite de un destino para soltar, el sistema envía uno o más eventos de arrastre al objeto de escucha de eventos de arrastre del destino. El objeto de escucha puede optar por modificar el aspecto del destino para soltar View en respuesta al evento. Por ejemplo, si el evento indica que la sombra de arrastre ingresó en el cuadro de límite del destino para soltar (tipo de acción ACTION_DRAG_ENTERED), el objeto de escucha puede reaccionar destacando View.

Soltado

El usuario suelta la sombra de arrastre dentro del cuadro de límite de un destino para soltar. El sistema envía al objeto de escucha del destino para soltar un evento de arrastre con el tipo de acción ACTION_DROP. El objeto del evento de arrastre contiene los datos que se pasaron al sistema en la llamada a startDragAndDrop() que inició la operación. Se espera que el objeto de escucha muestre el valor booleano true al sistema si procesa correctamente los datos soltados.

Ten en cuenta que este paso solo se produce si el usuario suelta la sombra de arrastre dentro del cuadro de límite de un elemento View cuyo objeto de escucha está registrado para recibir eventos de arrastre (un destino para soltar). Si el usuario suelta la sombra de arrastre en cualquier otra situación, no se envía ningún evento de arrastre ACTION_DROP.

Finalizado

Después de que el usuario suelta la sombra de arrastre y el sistema envía (si es necesario) un evento de arrastre con un tipo de acción ACTION_DROP, el sistema envía un evento de arrastre con un tipo de acción ACTION_DRAG_ENDED para indicar que la operación de arrastre terminó. Esto se lleva a cabo sin importar dónde liberó la sombra de arrastre el usuario. El evento se envía a todos los objetos de escucha que están registrados para recibir eventos de arrastre, incluso si también recibieron el evento ACTION_DROP.

Cada uno de los cuatro pasos se describe con más detalle en la sección Una operación de arrastrar y soltar.

Eventos de arrastre

El sistema envía un evento de arrastre en forma de objeto DragEvent, que contiene un tipo de acción que describe lo que sucede en el proceso de arrastrar y soltar. Según el tipo de acción, el objeto también puede contener otros datos.

Los objetos de escucha de eventos de arrastre reciben el objeto DragEvent. Para obtener el tipo de acción, los objetos de escucha llaman a DragEvent#getAction(). Hay seis valores posibles, definidos por constantes en la clase DragEvent.

Tabla 1: Tipos de acción DragEvent

Tipo de acción Significado
ACTION_DRAG_STARTED La aplicación llamó a startDragAndDrop() y obtuvo una sombra de arrastre. Si el objeto de escucha quiere seguir recibiendo eventos de arrastre para esta operación, debe mostrar un valor booleano true en el sistema.
ACTION_DRAG_ENTERED La sombra de arrastre acaba de ingresar al cuadro de límite del elemento View del objeto de escucha de eventos de arrastre. Este es el primer tipo de acción de evento que recibe el objeto de escucha cuando la sombra de arrastre ingresa al cuadro de límite.
ACTION_DRAG_LOCATION Luego de un evento de ACTION_DRAG_ENTERED, la sombra de arrastre permanece dentro del cuadro de límite del elemento View del objeto de escucha de eventos de arrastre.
ACTION_DRAG_EXITED Después de un evento de ACTION_DRAG_ENTERED y al menos un evento de ACTION_DRAG_LOCATION, la sombra de arrastre se movió fuera del cuadro de límite del elemento View del objeto de escucha de eventos de arrastre.
ACTION_DROP La sombra de arrastre se soltó sobre el elemento View del objeto de escucha de eventos de arrastre. Este tipo de acción solo se envía al objeto de escucha de un objeto View si el objeto de escucha mostró un valor booleano true en respuesta al evento de arrastre ACTION_DRAG_STARTED. Este tipo de acción no se envía si el usuario suelta la sombra de arrastre en un elemento View cuyo objeto de escucha no está registrado o si suelta la sombra de arrastre sobre cualquier elemento que no sea parte del diseño actual.

Se espera que el objeto de escucha muestre un valor booleano true si procesa correctamente la acción de soltar. De lo contrario, debería mostrar false.

ACTION_DRAG_ENDED El sistema está finalizando la operación de arrastrar y soltar. Este tipo de acción no necesariamente está precedido por un evento ACTION_DROP. Si el sistema envió un ACTION_DROP, recibir el tipo de acción ACTION_DRAG_ENDED no implica que la operación de soltar se haya realizado correctamente. El objeto de escucha debe llamar a getResult() (consulta la Tabla 2) para obtener el valor que se mostró en respuesta a ACTION_DROP. Si no se envió un evento ACTION_DROP, getResult() muestra false.

El objeto DragEvent también contiene los datos y metadatos que tu aplicación proporcionó al sistema en la llamada a startDragAndDrop(). Algunos de los datos son válidos solo para determinados tipos de acciones, como se resume en la Tabla 2. Para obtener más información sobre los eventos y sus datos asociados, consulta Una operación de arrastrar y soltar.

Tabla 2: Datos válidos de DragEvent por tipo de acción

Valor
getAction()
Valor
getClipDescription()
Valor
getLocalState()
Valor
getX()
Valor
getY()
Valor
getClipData()
Valor
getResult()
ACTION_DRAG_STARTED ✓ ✓ ✓ ✓    
ACTION_DRAG_ENTERED ✓ ✓        
ACTION_DRAG_LOCATION ✓ ✓ ✓ ✓    
ACTION_DRAG_EXITED ✓ ✓        
ACTION_DROP ✓ ✓ ✓ ✓ ✓  
ACTION_DRAG_ENDED   ✓       ✓

Los métodos DragEvent getAction(), describeContents(), writeToParcel() y toString() siempre muestran datos válidos.

Si un método no contiene datos válidos para un tipo de acción específico, muestra null o 0, según su tipo de resultado.

Sombra de arrastre

Durante una operación de arrastrar y soltar, el sistema muestra una imagen, que el usuario arrastra. Para el movimiento de datos, esta imagen representa los datos que se arrastran. Para otras operaciones, la imagen representa algún aspecto de la operación de arrastre.

La imagen se llama sombra de arrastre. Se crea con métodos que declaras para un objeto View.DragShadowBuilder. Para pasar el compilador al sistema cuando inicies una operación de arrastrar y soltar, usa startDragAndDrop(). Como parte de la respuesta a startDragAndDrop(), el sistema invoca los métodos de devolución de llamada que definiste en View.DragShadowBuilder para obtener una sombra de arrastre.

La clase View.DragShadowBuilder tiene dos constructores:

View.DragShadowBuilder(View)

Este constructor acepta cualquiera de los objetos View de la aplicación. El constructor almacena el objeto View en el objeto View.DragShadowBuilder, por lo que las devoluciones de llamada pueden acceder a él para construir la sombra de arrastre. No es necesario que la vista sea el elemento View (si existe) que el usuario seleccionó para iniciar la operación de arrastre.

Si usas este constructor, no necesitas extender View.DragShadowBuilder ni anular sus métodos. De manera predeterminada, obtendrás una sombra de arrastre que tiene el mismo aspecto que el elemento View que pasas como argumento, centrada debajo de la ubicación en la que el usuario toca la pantalla.

View.DragShadowBuilder()

Si usas este constructor, no habrá objetos View disponibles en el objeto View.DragShadowBuilder (el campo se establece en null). Deberás extender View.DragShadowBuilder y anular sus métodos; de lo contrario, obtendrás una sombra de arrastre invisible. El sistema no arroja un error.

La clase View.DragShadowBuilder tiene dos métodos que, en conjunto, crean la sombra de arrastre:

onProvideShadowMetrics()

El sistema llama a este método inmediatamente después de que llamas a startDragAndDrop(). Usa el método para enviar las dimensiones y el punto táctil de la sombra de arrastre al sistema. El método tiene dos parámetros:

outShadowSize
Un objeto Point. El ancho de la sombra de arrastre va en x y su altura va en y.
outShadowTouchPoint
Un objeto Point. El punto táctil es la ubicación dentro de la sombra de arrastre que debe estar debajo del dedo del usuario durante el arrastre. Su posición X va en x y su posición Y va en y.
onDrawShadow()

Inmediatamente después de la llamada a onProvideShadowMetrics(), el sistema llama a onDrawShadow() para crear la sombra de arrastre. El método tiene un solo argumento, un objeto Canvas que el sistema construye a partir de los parámetros que proporcionas en onProvideShadowMetrics(). El método dibuja la sombra de arrastre en el Canvas proporcionado.

Para mejorar el rendimiento, el tamaño de la sombra de arrastre debe ser pequeño. Para un solo elemento, te recomendamos que uses un ícono. Para una selección múltiple, te recomendamos que uses íconos en una pila en lugar de imágenes completas distribuidas en la pantalla.

Una operación de arrastrar y soltar

En esta sección, se muestra paso a paso la manera de iniciar un arrastre, responder a eventos durante la operación de arrastre, responder a un evento de soltar y finalizar la operación de arrastrar y soltar.

Inicia un arrastre

El usuario inicia un arrastre con un gesto de arrastre, que es, por lo general, mantener presionado un elemento View. En respuesta, tu app debería hacer lo siguiente:

  1. Crea un objeto ClipData y un objeto ClipData.Item para los datos que se estén moviendo. Como parte de ClipData, proporciona metadatos que se almacenen en un objeto ClipDescription dentro de ClipData. Para una operación de arrastrar y soltar que no represente movimiento de datos, te recomendamos que uses null en lugar de un objeto real.

    Por ejemplo, en este fragmento de código, se muestra cómo responder a un gesto de mantener presionado en un ImageView creando un objeto ClipData que contiene la etiqueta de un ImageView:

    Kotlin

    // Create a string for the ImageView label.
    val IMAGEVIEW_TAG = "icon bitmap"
    
    ...
    
    val imageView = ImageView(this).apply {
        // Sets the bitmap for the ImageView from an icon bit map (defined elsewhere).
        setImageBitmap(iconBitmap)
        tag = IMAGEVIEW_TAG
        setOnLongClickListener { v ->
            // Create a new ClipData.
            // This is done in two steps to provide clarity. The convenience method
            // ClipData.newPlainText() can create a plain text ClipData in one step.
    
            // Create a new ClipData.Item from the ImageView object's tag.
            val item = ClipData.Item(v.tag as? CharSequence)
    
            // Create a new ClipData using the tag as a label, the plain text MIME type, and
            // the already-created item. This creates a new ClipDescription object within the
            // ClipData and sets its MIME type to "text/plain".
            val dragData = ClipData(
                v.tag as? CharSequence,
                arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
                item)
    
            // Instantiate the drag shadow builder.
            val myShadow = MyDragShadowBuilder(this)
    
            // Start the drag.
            v.startDragAndDrop(dragData,  // The data to be dragged
                               myShadow,  // The drag shadow builder
                               null,      // No need to use local data
                               0          // Flags (not currently used, set to 0)
            )
    
           // Indicate that the long-click was handled.
           true
        }
    }
    

    Java

    // Create a string for the ImageView label.
    private static final String IMAGEVIEW_TAG = "icon bitmap";
    
    ...
    
    // Create a new ImageView.
    ImageView imageView = new ImageView(this);
    
    // Set the bitmap for the ImageView from an icon bit map (defined elsewhere).
    imageView.setImageBitmap(iconBitmap);
    
    // Set the tag.
    imageView.setTag(IMAGEVIEW_TAG);
    
    // Sets a long click listener for the ImageView using an anonymous listener object that
    // implements the OnLongClickListener interface.
    imageView.setOnLongClickListener( v -> {
    
        // Create a new ClipData.
        // This is done in two steps to provide clarity. The convenience method
        // ClipData.newPlainText() can create a plain text ClipData in one step.
    
        // Create a new ClipData.Item from the ImageView object's tag.
        ClipData.Item item = new ClipData.Item((CharSequence) v.getTag());
    
        // Create a new ClipData using the tag as a label, the plain text MIME type, and
        // the already-created item. This creates a new ClipDescription object within the
        // ClipData and sets its MIME type to "text/plain".
        ClipData dragData = new ClipData(
            (CharSequence) v.getTag(),
            new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
            item);
    
        // Instantiate the drag shadow builder.
        View.DragShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
        // Start the drag.
        v.startDragAndDrop(dragData,  // The data to be dragged
                           myShadow,  // The drag shadow builder
                           null,      // No need to use local data
                           0          // Flags (not currently used, set to 0)
        );
    
        // Indicate that the long-click was handled.
        return true;
    });
    
  2. En el siguiente fragmento de código, se define myDragShadowBuilder mediante la anulación de los métodos en View.DragShadowBuilder. El código crea una pequeña sombra de arrastre gris y rectangular para un elemento TextView:

    Kotlin

    private class MyDragShadowBuilder(v: View) : View.DragShadowBuilder(v) {
    
        private val shadow = ColorDrawable(Color.LTGRAY)
    
        // Defines a callback that sends the drag shadow dimensions and touch point
        // back to the system.
        override fun onProvideShadowMetrics(size: Point, touch: Point) {
    
            // Set the width of the shadow to half the width of the original View.
            val width: Int = view.width / 2
    
            // Set the height of the shadow to half the height of the original View.
            val height: Int = view.height / 2
    
            // The drag shadow is a ColorDrawable. This sets its dimensions to be the
            // same as the Canvas that the system provides. As a result, the drag shadow
            // fills the Canvas.
            shadow.setBounds(0, 0, width, height)
    
            // Set the size parameter's width and height values. These get back to
            // the system through the size parameter.
            size.set(width, height)
    
            // Set the touch point's position to be in the middle of the drag shadow.
            touch.set(width / 2, height / 2)
        }
    
        // Defines a callback that draws the drag shadow in a Canvas that the system
        // constructs from the dimensions passed to onProvideShadowMetrics().
        override fun onDrawShadow(canvas: Canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas)
        }
    }
    

    Java

    private static class MyDragShadowBuilder extends View.DragShadowBuilder {
    
        // The drag shadow image, defined as a drawable object.
        private static Drawable shadow;
    
        // Constructor
        public MyDragShadowBuilder(View v) {
    
            // Stores the View parameter.
            super(v);
    
            // Creates a draggable image that fills the Canvas provided by the system.
            shadow = new ColorDrawable(Color.LTGRAY);
        }
    
        // Defines a callback that sends the drag shadow dimensions and touch point
        // back to the system.
        @Override
        public void onProvideShadowMetrics (Point size, Point touch) {
    
            // Defines local variables
            int width, height;
    
            // Set the width of the shadow to half the width of the original View.
            width = getView().getWidth() / 2;
    
            // Set the height of the shadow to half the height of the original View.
            height = getView().getHeight() / 2;
    
            // The drag shadow is a ColorDrawable. This sets its dimensions to be the
            // same as the Canvas that the system provides. As a result, the drag shadow
            // fills the Canvas.
            shadow.setBounds(0, 0, width, height);
    
            // Set the size parameter's width and height values. These get back to the
            // system through the size parameter.
            size.set(width, height);
    
            // Set the touch point's position to be in the middle of the drag shadow.
            touch.set(width / 2, height / 2);
        }
    
        // Defines a callback that draws the drag shadow in a Canvas that the system
        // constructs from the dimensions passed to onProvideShadowMetrics().
        @Override
        public void onDrawShadow(Canvas canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas);
        }
    }
    

Responde a un inicio de arrastre

Durante la operación de arrastre, el sistema despacha eventos de arrastre a los objetos de escucha de eventos de arrastre de los objetos View del diseño actual. Los objetos de escucha deben reaccionar llamando a DragEvent#getAction() para obtener el tipo de acción. Cuando se inicia un arrastre, este método muestra ACTION_DRAG_STARTED.

En respuesta a un evento con el tipo de acción ACTION_DRAG_STARTED, un objeto de escucha de eventos de arrastre debería hacer lo siguiente:

  1. Llama a DragEvent#getClipDescription() y usa los métodos de tipo de MIME en el objeto ClipDescription que se muestra para ver si el objeto de escucha puede aceptar los datos que se arrastran.

    Si la operación de arrastrar y soltar no representa movimiento de datos, es posible que esto no sea necesario.

  2. Si el objeto de escucha de eventos de arrastre puede aceptar la acción de soltar, debe mostrar true para indicarle al sistema que siga enviando eventos de arrastre al objeto de escucha. Si el objeto de escucha no puede aceptar la acción de soltar, debe mostrar false y el sistema dejará de enviar eventos de arrastre hasta que este envíe ACTION_DRAG_ENDED para finalizar la operación de arrastrar y soltar.

Ten en cuenta que para un evento ACTION_DRAG_STARTED no son válidos los siguientes métodos DragEvent: getClipData(), getX(), getY() y getResult().

Controla eventos durante el arrastre

Durante la acción de arrastre, los objetos de escucha de eventos de arrastre que mostraron true en respuesta al evento de arrastre ACTION_DRAG_STARTED continúan recibiendo eventos de arrastre. Los tipos de eventos de arrastre que recibe un objeto de escucha durante el arrastre dependen de la ubicación de la sombra de arrastre y de la visibilidad del elemento View del objeto de escucha. Los objetos de escucha usan los eventos de arrastre principalmente para decidir si deben cambiar el aspecto de su View.

Durante el arrastre, DragEvent#getAction() muestra uno de tres valores:

  • ACTION_DRAG_ENTERED: El objeto de escucha recibe este tipo de acción de evento cuando el punto táctil (el punto en la pantalla debajo del dedo o mouse del usuario) entra en el cuadro de límite del elemento View del objeto de escucha.
  • ACTION_DRAG_LOCATION: Una vez que el objeto de escucha recibe un evento ACTION_DRAG_ENTERED, y antes de recibir un evento ACTION_DRAG_EXITED, recibe un evento ACTION_DRAG_LOCATION nuevo cada vez que se mueve el punto táctil. Los métodos getX() y getY() muestran las coordenadas X e Y del punto táctil.
  • ACTION_DRAG_EXITED: Este tipo de acción de evento se envía a un objeto de escucha que recibió ACTION_DRAG_ENTERED. El evento se envía cuando el punto táctil de la sombra de arrastre se mueve desde el cuadro de límite del elemento View del objeto de escucha hacia afuera del cuadro de límite.

El objeto de escucha de eventos de arrastre no necesita reaccionar a ninguno de estos tipos de acciones. Si el receptor muestra un valor al sistema, dicho valor se ignora.

A continuación, se describen algunas pautas para responder a cada uno de estos tipos de acción:

  • En respuesta a ACTION_DRAG_ENTERED o ACTION_DRAG_LOCATION, el objeto de escucha puede cambiar el aspecto de View a fin de indicar que la vista es un posible destino de la acción de soltar.
  • Un evento con el tipo de acción ACTION_DRAG_LOCATION contiene datos válidos para getX() y getY(), que corresponden a la ubicación del punto táctil. El objeto de escucha puede usar esta información para modificar el aspecto de View en el punto táctil o determinar la posición exacta donde el usuario podría liberar la sombra de arrastre (es decir, descartar los datos).
  • En respuesta a ACTION_DRAG_EXITED, el objeto de escucha debe restablecer los cambios de aspecto que se aplicaron en respuesta a ACTION_DRAG_ENTERED o ACTION_DRAG_LOCATION. Esto le indica al usuario que el elemento View ya no es un destino inminente de la acción de soltar.

Responde a la acción de soltar

Cuando el usuario suelta la sombra de arrastre sobre un View y View informó anteriormente que puede aceptar el contenido que se arrastra, el sistema envía un evento de arrastre al View con el tipo de acción ACTION_DROP.

El objeto de escucha de eventos de arrastre debe hacer lo siguiente:

  1. Llama a getClipData() para obtener el objeto ClipData que se suministró originalmente en la llamada a startDragAndDrop() y procesa los datos.

    Si la operación de arrastrar y soltar no representa movimiento de datos, esta acción no es necesaria.

  2. Se muestra un booleano true para indicar que la acción de soltar se procesó correctamente o un booleano false si no fue así. El valor mostrado se convierte en el valor que getResult() muestra para el evento ACTION_DRAG_ENDED final.

    Ten en cuenta que, si el sistema no envía un evento ACTION_DROP, el valor que muestra getResult() para un evento ACTION_DRAG_ENDED es false.

En el caso de un evento ACTION_DROP, getX() y getY() usan el sistema de coordenadas del View que recibió la acción de soltar a fin de mostrar las posiciones X e Y del punto táctil en el momento del descenso.

El sistema permite que el usuario libere la sombra de arrastre sobre un elemento View cuyo objeto de escucha de eventos de arrastre no reciba eventos de arrastre. También permite al usuario liberar la sombra de arrastre sobre regiones vacías de la IU de la aplicación o sobre áreas que se encuentran fuera de tu aplicación. En cualquier caso, el sistema no enviará un evento con el tipo de acción ACTION_DROP, aunque el sistema envíe un evento ACTION_DRAG_ENDED.

Responde a la finalización de un arrastre

Inmediatamente después de que el usuario suelta la sombra de arrastre, el sistema envía un evento de arrastre con un tipo de acción ACTION_DRAG_ENDED a todos los objetos de escucha de eventos de arrastre de tu aplicación. Esto indica que finalizó la operación de arrastrar y soltar.

Cada objeto de escucha de eventos de arrastre debe hacer lo siguiente:

  1. Si el objeto de escucha cambió el aspecto de su objeto View durante la operación, debe restablecer el objeto View a su aspecto predeterminado. Esta es una indicación visual para el usuario de que terminó la operación.
  2. De manera optativa, el objeto de escucha puede llamar a getResult() para obtener más información sobre la operación. Si un objeto de escucha mostró true en respuesta a un evento del tipo de acción ACTION_DROP, getResult() muestra un valor booleano true. En todos los demás casos, getResult() muestra un valor booleano false, incluido el caso en el que el sistema no envió un evento ACTION_DROP.
  3. Para indicar que la operación de arrastrar y soltar se completó de forma correcta, el objeto de escucha debe mostrar un valor booleano true al sistema.

Cómo responder a eventos de arrastre: ejemplo

Todos los eventos de arrastre son recibidos inicialmente por el objeto de escucha o el método de evento de arrastre. El siguiente fragmento de código es un ejemplo simple de respuesta a eventos de arrastre:

Kotlin

val imageView = ImageView(this)

// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->

    // Handles each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determines if this View can accept the dragged data.
            if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // As an example of what your application might do, applies a blue color tint
                // to the View to indicate that it can accept data.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate()

                // Returns true to indicate that the View can accept the dragged data.
                true
            } else {
                // Returns false to indicate that, during the current drag and drop operation,
                // this View will not receive events again until ACTION_DRAG_ENDED is sent.
                false
            }
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Applies a green tint to the View.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

            // Invalidates the view to force a redraw in the new tint.
            v.invalidate()

            // Returns true; the value is ignored.
            true
        }

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event.
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Resets the color tint to blue.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

            // Invalidates the view to force a redraw in the new tint.
            v.invalidate()

            // Returns true; the value is ignored.
            true
        }
        DragEvent.ACTION_DROP -> {
            // Gets the item containing the dragged data.
            val item: ClipData.Item = e.clipData.getItemAt(0)

            // Gets the text data from the item.
            val dragData = item.text

            // Displays a message containing the dragged data.
            Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()

            // Turns off any color tints.
            (v as? ImageView)?.clearColorFilter()

            // Invalidates the view to force a redraw.
            v.invalidate()

            // Returns true. DragEvent.getResult() will return true.
            true
        }

        DragEvent.ACTION_DRAG_ENDED -> {
            // Turns off any color tinting.
            (v as? ImageView)?.clearColorFilter()

            // Invalidates the view to force a redraw.
            v.invalidate()

            // Does a getResult(), and displays what happened.
            when(e.result) {
                true ->
                    Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
                else ->
                    Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
            }.show()

            // Returns true; the value is ignored.
            true
        }
        else -> {
            // An unknown action type was received.
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            false
        }
    }
}

Java

View imageView = new ImageView(this);

// Set the drag event listener for the View.
imageView.setOnDragListener( (v, e) -> {

    // Handles each of the expected events.
    switch(e.getAction()) {

        case DragEvent.ACTION_DRAG_STARTED:

            // Determines if this View can accept the dragged data.
            if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                // As an example of what your application might do, applies a blue color tint
                // to the View to indicate that it can accept data.
                ((ImageView)v).setColorFilter(Color.BLUE);

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate();

                // Returns true to indicate that the View can accept the dragged data.
                return true;

            }

            // Returns false to indicate that, during the current drag and drop operation,
            // this View will not receive events again until ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

            // Applies a green tint to the View.
            ((ImageView)v).setColorFilter(Color.GREEN);

            // Invalidates the view to force a redraw in the new tint.
            v.invalidate();

            // Returns true; the value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

            // Resets the color tint to blue.
            ((ImageView)v).setColorFilter(Color.BLUE);

            // Invalidates the view to force a redraw in the new tint.
            v.invalidate();

            // Returns true; the value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

            // Gets the item containing the dragged data.
            ClipData.Item item = e.getClipData().getItemAt(0);

            // Gets the text data from the item.
            CharSequence dragData = item.getText();

            // Displays a message containing the dragged data.
            Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show();

            // Turns off any color tints.
            ((ImageView)v).clearColorFilter();

            // Invalidates the view to force a redraw.
            v.invalidate();

            // Returns true. DragEvent.getResult() will return true.
            return true;

        case DragEvent.ACTION_DRAG_ENDED:

            // Turns off any color tinting.
            ((ImageView)v).clearColorFilter();

            // Invalidates the view to force a redraw.
            v.invalidate();

            // Does a getResult(), and displays what happened.
            if (e.getResult()) {
                Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG).show();
            }

            // Returns true; the value is ignored.
            return true;

        // An unknown action type was received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;

});

Cómo arrastrar y soltar en modo multiventana

Los dispositivos que ejecutan Android 7.0 (nivel de API 24) o versiones posteriores admiten el modo multiventana, que permite a los usuarios transferir datos de una app a otra mediante una operación de arrastrar y soltar (consulta Compatibilidad con el modo multiventana).

La app de origen proporciona los datos. La operación de arrastrar y soltar comienza en la app de origen. La app de destino recibe los datos. La operación de arrastrar y soltar finaliza en la app de destino.

Cuando se inicia una operación de arrastrar y soltar, la app de origen debe establecer la marca DRAG_FLAG_GLOBAL para indicar que el usuario puede arrastrar datos a otra app.

Debido a que los datos traspasan los límites de las apps, estas comparten el acceso a los datos mediante un URI de contenido:

  • La app de origen debe establecer la marca DRAG_FLAG_GLOBAL_URI_READ o la marca DRAG_FLAG_GLOBAL_URI_WRITE, o ambas, en función del acceso de lectura/escritura a los datos que tenga la app de destino.
  • La app de destino debe llamar a requestDragAndDropPermissions() inmediatamente antes de manejar los datos que el usuario arrastra a la app. Si la app de destino ya no necesita acceso a los datos de arrastre, puede llamar a release() en el objeto que se mostró desde requestDragAndDropPermissions(). De lo contrario, los permisos se liberan cuando se destruye la actividad que los contiene.

En el siguiente fragmento de código, se muestra cómo liberar el acceso de solo lectura para arrastrar datos inmediatamente después de que se lleva a cabo la operación de arrastrar y soltar. Para ver un ejemplo más completo, consulta el ejemplo de DragAndDrop en GitHub.

Actividad de arrastrar y soltar de origen

Kotlin

// Drag a file stored in internal storage. The file is in an "images/" directory.
val internalImagesDir = File(context.filesDir, "images")
val imageFile = File(internalImagesDir, imageFilename)
val uri = FileProvider.getUriForFile(context, contentAuthority, imageFile)

val listener = OnDragStartListener@{ view: View, _: DragStartHelper ->
    val clipData = ClipData(ClipDescription("Image Description",
                                            arrayOf("image/*")),
                            ClipData.Item(uri))
    // Must include DRAG_FLAG_GLOBAL to allow for dragging data between apps.
    // This example provides read-only access to the data.
    val flags = View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
    return@OnDragStartListener view.startDragAndDrop(clipData,
                                                     View.DragShadowBuilder(view),
                                                     null,
                                                     flags)
}

// Container where the image originally appears in the source app.
val srcImageView = findViewById<ImageView>(R.id.imageView)

// Detect and start the drag event.
DragStartHelper(srcImageView, listener).apply {
    attach()
}

Java

// Drag a file stored under an "images/" directory in internal storage.
File internalImagesDir = new File(context.getFilesDir(), "images");
File imageFile = new File(internalImagesDir, imageFilename);
final Uri uri = FileProvider.getUriForFile(context, contentAuthority, imageFile);

// Container where the image originally appears in the source app.
ImageView srcImageView = findViewById(R.id.imageView);

// Enable the view to detect and start the drag event.
new DragStartHelper(srcImageView, (view, helper) -> {
    ClipData clipData = new ClipData(new ClipDescription("Image Description",
                                                          new String[] {"image/*"}),
                                     new ClipData.Item(uri));
    // Must include DRAG_FLAG_GLOBAL to allow for dragging data between apps.
    // This example provides read-only access to the data.
    int flags = View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ;
    return view.startDragAndDrop(clipData,
                                 new View.DragShadowBuilder(view),
                                 null,
                                 flags);
}).attach();

Actividad de arrastrar y soltar de origen

Kotlin

// Container for where the image is to be dropped in the target app.
val targetImageView = findViewById<ImageView>(R.id.imageView)

targetImageView.setOnDragListener { view, event ->

    when (event.action) {

        ACTION_DROP -> {
            val imageItem: ClipData.Item = event.clipData.getItemAt(0)
            val uri = imageItem.uri

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            val dropPermissions = requestDragAndDropPermissions(event)
            (view as ImageView).setImageURI(uri)

            // Release the permission immediately afterwards because it's
            // no longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type was received.
        else -> {
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            return@setOnDragListener false
        }

    }
}

Java

// Container where the image is to be dropped in the target app.
ImageView targetImageView = findViewById(R.id.imageView);

targetImageView.setOnDragListener( (view, event) -> {

    switch (event.getAction()) {

        case ACTION_DROP:
            ClipData.Item imageItem = event.getClipData().getItemAt(0);
            Uri uri = imageItem.getUri();

            // Request permission to access the image data being
            // dragged into the target activity's ImageView element.
            DragAndDropPermissions dropPermissions =
                requestDragAndDropPermissions(event);

            ((ImageView)view).setImageURI(uri);

            // Release the permission immediately afterwards because
            // it's no longer needed.
            dropPermissions.release();

            return true;

        // Implement logic for other DragEvent cases here.

        // An unknown action type was received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;
});

DropHelper para simplificar la acción de arrastrar y soltar

La clase DropHelper simplifica la implementación de las funciones de arrastrar y soltar. Un miembro de la biblioteca DragAndDrop de Jetpack, DropHelper, proporciona retrocompatibilidad hasta el nivel de API 24.

Usa DropHelper para especificar destinos para soltar, personalizar el resaltado de objetivos de soltar y definir cómo se controlan los datos descartados.

Destinos para soltar

DropHelper#configureView() es un método estático y de sobrecarga que te permite especificar destinos para soltar. Entre los parámetros, se incluyen los siguientes:

Por ejemplo, a fin de crear un destino para soltar que acepte imágenes, usa cualquiera de las siguientes llamadas a métodos:

Kotlin

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    options,
    onReceiveContentListener)

// or

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    onReceiveContentListener)

Java

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    options,
    onReceiveContentlistener);

// or

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    onReceiveContentlistener);

En la segunda llamada, se omiten las opciones de configuración del destino para soltar, en cuyo caso el color de resaltado del destino para soltar se configura en el color secundario (o de acento) del tema, el radio de la esquina destacada se establece en 16 dp y la lista de EditTexts queda vacía (consulta la Configuración del destino para soltar a continuación).

Configuración del destino para soltar

La clase interna DropHelper.Options te permite configurar destinos para soltar. Proporciona una instancia de la clase al método DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener) (consulta Destinos para soltar más arriba).

Elementos destacados del destino para soltar

DropHelper configura los destinos para soltar de manera que muestren una selección a medida que los usuarios arrastran contenido sobre los destinos. DropHelper proporciona un estilo predeterminado, pero DropHelper.Options te permite establecer el color del resaltado y especificar el radio de la esquina del rectángulo destacado.

Usa la clase DropHelper.Options.Builder para crear una instancia de DropHelper.Options y establece las opciones de configuración, por ejemplo:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .setHighlightColor(getColor(R.color.purple_300))
                                      .setHighlightCornerRadiusPx(resources.getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .setHighlightColor(getColor(R.color.purple_300))
                                     .setHighlightCornerRadiusPx(getResources().getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                     .build();

Componentes EditText en destinos para soltar

DropHelper también controla el enfoque dentro del destino para soltar cuando el objetivo contiene campos de texto editables.

Los destinos para soltar pueden ser una sola vista o una jerarquía de vistas. Si la jerarquía de vistas del destino para soltar contiene un componente EditText o más, debes proporcionar una lista de los componentes DropHelper.Options.Builder#addInnerEditTexts(EditText...) a fin de garantizar que los elementos destacados del destino para soltar y el manejo de datos de texto funcionen correctamente.

DropHelper evita que los componentes EditText dentro de la jerarquía de vistas del destino para soltar roben el enfoque de la vista contenedora durante las interacciones de arrastre.

Además, si la función de arrastrar y soltar ClipData incluye datos de texto y URI, DropHelper selecciona uno de los componentes EditText en el destino para soltar a fin de controlar la datos de texto. La selección se basa en el siguiente orden de prioridad:

  1. El elemento EditText en el que se soltó ClipData
  2. El elemento EditText que contiene el cursor de texto (signo de intercalación)
  3. El primer elemento EditText proporcionado a la llamada a DropHelper.Options.Builder#addInnerEditTexts(EditText...)

Para establecer un elemento EditText como el controlador de datos de texto predeterminado, pasa EditText como el primer argumento de la llamada a DropHelper.Options.Builder#addInnerEditTexts(EditText...). Por ejemplo, si tu objetivo de soltar controla las imágenes, pero contiene campos de texto editables T1, T2 y T3, haz que T2 sea el valor predeterminado de la siguiente manera:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .addInnerEditTexts(T2, T1, T3)
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .addInnerEditTexts(T2, T1, T3)
                                     .build();

Control de los datos del destino para soltar

El método DropHelper#configureView() acepta un elemento OnReceiveContentListener que puedes crear para controlar las funciones de arrastrar y soltar ClipData. Los datos de arrastrar y soltar se proporcionan al objeto de escucha en un objeto ContentInfoCompat. Los datos de texto están presentes en el objeto. El contenido multimedia, como las imágenes, está representado por URI.

OnReceiveContentListener también controla los datos proporcionados al destino mediante interacciones de usuario que no son la acción de arrastrar y soltar (como copiar y pegar) cuando se usa DropHelper#configureView() para configurar los siguientes tipos de vistas:

  • Todas las vistas, si el usuario ejecuta Android 12 o una versión posterior
  • AppCompatEditText hasta Android 7.0

Tipos de MIME, permisos y validación de contenido

La comprobación del tipo de MIME de DropHelper se basa en la función de arrastrar y soltar ClipDescription, que la app crea al proporcionar los datos de arrastrar y soltar. Debes validar el elemento ClipDescription para asegurarte de que los tipos de MIME se hayan configurado correctamente.

DropHelper solicita todos los permisos de acceso para los URI de contenido que se encuentran en la acción de arrastrar y soltar ClipData (consulta DragAndDropPermissions). Los permisos te permiten resolver los URI de contenido cuando se procesan los datos de la acción de arrastrar y soltar.

DropHelper no valida los datos que muestran los proveedores de contenido cuando resuelven URI en los datos soltados. Debes comprobar la nulabilidad y comprobar la precisión de los datos resueltos.

Recursos adicionales