Arrastrar y soltar

Con el marco de trabajo para arrastrar/soltar de Android, puedes permitir a los usuarios mover datos de una View a otra utilizando un gesto gráfico. El marco de trabajo incluye una clase de evento de arrastre, objetos de escucha de arrastre y clases y métodos auxiliares.

Aunque el marco de trabajo está diseñado principalmente para el movimiento de datos, puedes usarlo para 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 del texto, sin embargo, se describe el marco de trabajo en términos de movimiento de datos.

También deberías consultar los siguientes recursos relacionados:

Descripción general

Una operación de arrastrar y soltar comienza cuando el usuario hace algún gesto que reconoces como una señal para comenzar a arrastrar datos. En respuesta, tu app le dice al sistema que se está iniciando el arrastre. El sistema vuelve a llamar a tu app para obtener una representación de los datos que se arrastran. A medida que el dedo del usuario mueve esta representación (una "sombra de arrastre") sobre el diseño actual, el sistema envía eventos de arrastre a los objetos de objetos de escucha de eventos de arrastre y métodos de devolución de llamada de eventos de arrastre asociados con los objetos View en el diseño. Una vez que el usuario suelta la sombra de arrastre, el sistema finaliza la operación de arrastre.

La creación de un objeto de escucha de evento de arrastre ("objetos de escucha") es a partir de una clase que implementa View.OnDragListener. Estableces el objeto de objeto de escucha de eventos de arrastre en una View con el método setOnDragListener() del objeto View. Cada objeto View también tiene un método onDragEvent() de devolución de llamada. Ambos se describen con más detalle en la sección El objeto de escucha de eventos de arrastre y el método de devolución de llamada.

Nota: Para una mayor simplicidad, en las siguientes secciones, se hace referencia a la rutina que recibe eventos de arrastre como el "objeto de escucha de eventos de arrastre", aunque, en realidad, puede ser un método de devolución de llamada.

Cuando inicias un arrastre, incluyes los datos que estás moviendo y los metadatos que describen esos datos como parte de la llamada al sistema. Mientras arrastras algo, el sistema envía eventos de arrastre a los objetos de escucha de eventos de arrastre o a los métodos de devolución de llamada de cada View en el diseño. Los objetos de escucha o los métodos de devolución de llamada pueden usar los metadatos para decidir si quieren aceptar los datos cuando se sueltan. Si el usuario suelta los datos sobre un objeto View, y el método de devolución de llamada o el objeto de escucha de ese objeto View le dijo previamente al sistema que desea aceptar esa acción, el sistema envía los datos al objeto de escucha o método de devolución de llamada en un evento de arrastre.

Tu app le indica al sistema que inicie un arrastre llamando al método startDrag(). Esto le dice al sistema que comience a enviar eventos de arrastre. El método también envía los datos que arrastras.

Puedes llamar a startDrag() para cualquier View adjunta en el diseño actual. El sistema usa solamente el objeto View para obtener acceso a la configuración global en el diseño.

Una vez que la app llama a startDrag(), el resto del proceso utiliza eventos que envía el sistema a los objetos View en el diseño actual.

Nota: Si las apps se ejecutan en modo multiventana, los usuarios pueden arrastrar y soltar datos de una app a otra. Para obtener más información, consulta Cómo admitir la función de arrastrar y soltar.

El proceso de arrastrar y soltar

El proceso de arrastrar y soltar tiene cuatro pasos o estados:

Iniciado
En respuesta al gesto del usuario para comenzar un arrastre, la app llama a startDrag() para avisarle al sistema que inicie la acción. Los argumentos startDrag() proporcionan los datos que se arrastrarán, los metadatos para esos datos y una devolución de llamada para dibujar la sombra de arrastre.

Primero, el sistema responde volviendo a llamar 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 a los objetos de escucha de eventos de arrastre para todos los objetos View en el 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 objetos de escucha registrados siguen recibiendo eventos de arrastre. En este punto, los objetos de escucha también pueden cambiar el aspecto de su objeto View para mostrar que el objeto de escucha 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. Con el envío de false, el objeto de escucha le dice al sistema que no está interesado en la operación de arrastre y no quiere aceptar los datos arrastrados.

Continuando
El usuario continúa con el arrastre. A medida que la sombra de arrastre se cruza con el cuadro de límite de un objeto View, el sistema envía uno o más eventos de arrastre al objeto de escucha de eventos de arrastre del objeto View (si está registrado para recibir eventos). El objeto de escucha puede optar por alterar el aspecto de su objeto View en respuesta al evento. Por ejemplo, si el evento indica que la sombra de arrastre ingresó en el cuadro de límite de la View (tipo de acción ACTION_DRAG_ENTERED), el objeto de escucha puede reaccionar destacando su View.
Soltado
El usuario libera la sombra de arrastre dentro del cuadro de límite de una View que puede aceptar los datos. El sistema envía al objeto de escucha del objeto View un evento de arrastre con el tipo de acción ACTION_DROP. El evento de arrastre contiene los datos que se pasaron al sistema en la llamada a startDrag() que inició la operación. Se espera que el objeto de escucha muestre un valor booleano true al sistema si el código para aceptar la acción de soltar es correcto.

Ten en cuenta que este paso solo se produce si el usuario suelta la sombra de arrastre dentro del cuadro de límite de una View cuyo objeto de escucha está registrado para recibir eventos de arrastre. 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 recibieron el evento ACTION_DROP.

Cada uno de los cuatro pasos se describe con más detalle en la sección Cómo diseñar una operación de arrastrar y soltar.

El objeto de escucha de eventos de arrastre y el método de devolución de llamada

Una View recibe eventos de arrastre con un objeto de escucha de eventos de arrastre que implementa View.OnDragListener o con su método de devolución de llamada onDragEvent(DragEvent). Cuando el sistema llama al método o al objeto de escucha, les pasa un objeto DragEvent.

Es probable que quieras usar el objeto de escucha en la mayoría de los casos. Cuando se diseñan IU, por lo general, no se dividen en subclases las clases View, pero el uso del método de devolución de llamada te obliga a hacerlo 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. Para configurar el objeto de escucha de un objeto View, llama a setOnDragListener().

Puedes tener un método de devolución de llamada y un objeto de escucha para el objeto View. Si ese es el 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(DragEvent) y View.OnDragListener es análoga a la combinación de onTouchEvent() y View.OnTouchListener que se usa con los eventos táctiles.

Eventos de arrastre

El sistema envía un evento de arrastre en forma de un objeto DragEvent. El objeto contiene un tipo de acción que le dice al objeto de escucha qué sucede en el proceso de arrastrar/soltar. El objeto incluye otros datos, según el tipo de acción.

Para obtener el tipo de acción, un objeto de escucha llama a getAction(). Hay seis valores posibles, definidos por constantes en la clase DragEvent. Estos se enumeran en la tabla 1.

El objeto DragEvent también contiene los datos que tu app proporcionó al sistema en la llamada a startDrag(). Algunos de los datos son válidos solo para determinados tipos de acciones. Los datos válidos para cada tipo de acción se resumen en la tabla 2. También se describen en detalle con el evento para el que son válidos en la sección Cómo diseñar una operación de arrastrar y soltar.

Tabla 1: Tipos de acción DragEvent

Valor de getAction() Significado
ACTION_DRAG_STARTED Un objeto de escucha de eventos de arrastre del objeto View recibe este tipo de acción de evento justo después de que la app llama a startDrag() y obtiene una sombra de arrastre.
ACTION_DRAG_ENTERED Un objeto de escucha de eventos de arrastre del objeto View recibe este tipo de acción de evento cuando la sombra de arrastre acaba de ingresar al cuadro de límite de la View. 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. 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_LOCATION Un objeto de escucha de eventos de arrastre del objeto View recibe este tipo de acción de evento después de recibir un evento ACTION_DRAG_ENTERED mientras la sombra de arrastre todavía está dentro del cuadro de límite de la View.
ACTION_DRAG_EXITED Un objeto de escucha de eventos de arrastre del objeto View recibe este tipo de acción de evento después de recibir un evento ACTION_DRAG_ENTERED y al menos un evento ACTION_DRAG_LOCATION, y después de que el usuario mueve la sombra de arrastre fuera del cuadro de límite de la View.
ACTION_DROP Un objeto de escucha de eventos de arrastre del objeto View recibe este tipo de acción de evento cuando el usuario suelta la sombra de arrastre sobre el objeto View. 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 una 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 objeto de escucha de eventos de arrastre de un objeto View recibe este tipo de acción de evento cuando el sistema finaliza la operación de arrastre. 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() para obtener el valor que se mostró en respuesta a ACTION_DROP. Si no se envió un evento ACTION_DROP, getResult() muestra false.

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 X X X      
ACTION_DRAG_ENTERED X X X X    
ACTION_DRAG_LOCATION X X X X    
ACTION_DRAG_EXITED X X        
ACTION_DROP X X X X X  
ACTION_DRAG_ENDED X X       X

Los métodos 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.

La 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 y, luego, se pasa al sistema cuando comienzas a arrastrar un elemento usando startDrag(). Como parte de la respuesta a startDrag(), 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 app. El constructor almacena un objeto View en el objeto View.DragShadowBuilder. Por lo tanto, durante la devolución de llamada puedes acceder a él a medida que construyes la sombra de arrastre. No es necesario que esté asociado con la View (si hay una) que seleccionó el usuario para iniciar la operación de arrastre.

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

View.DragShadowBuilder()
Si usas este constructor, no hay objetos View disponibles en el objeto View.DragShadowBuilder (el campo está configurado en null). Si usas este constructor y no extiendes View.DragShadowBuilder ni anulas sus métodos, obtendrás una sombra de arrastre invisible. El sistema no da un error.

La clase View.DragShadowBuilder tiene dos métodos:

onProvideShadowMetrics()
El sistema llama a este método inmediatamente después de que llamas a startDrag(). Úsalo para enviar al sistema las dimensiones y el punto de contacto de la sombra de arrastre. El método tiene dos argumentos:
dimensiones
Un objeto Point. El ancho de la sombra de arrastre va en x y su altura va en y.
touch_point
Un objeto Point. El punto de contacto 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 obtener 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(). Úsalo para dibujar la sombra de arrastre en el objeto Canvas proporcionado.

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

Cómo diseñar una operación de arrastrar y soltar

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

Cómo iniciar un arrastre

El usuario inicia un arrastre con un gesto de arrastre, que es, por lo general, mantener presionado un objeto View. En respuesta, debes hacer lo siguiente:

  1. Según sea necesario, crea un ClipData y ClipData.Item para los datos que se estén moviendo. Como parte del objeto 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, es posible que desees utilizar null en lugar de un objeto real.

    Por ejemplo, en este fragmento de código, se muestra cómo responder cuando se mantiene presionado una ImageView mediante la creación de un objeto ClipData que contiene la etiqueta de una ImageView. Después de este fragmento, se muestra un fragmento que explica cómo anular los métodos en View.DragShadowBuilder:

    Kotlin

        const val IMAGEVIEW_TAG = "icon bitmap"
        ...
        val imageView = ImageView(this).apply {
            setImageBitmap(iconBitmap)
            tag = IMAGEVIEW_TAG
            imageView.setOnLongClickListener { v: View ->
                // 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 will create a new ClipDescription object within the
                // ClipData, and set its MIME type entry to "text/plain"
                val dragData = ClipData(
                        v.tag as? CharSequence,
                        arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
                        item)
    
                // Instantiates the drag shadow builder.
                val myShadow = MyDragShadowBuilder(this)
    
                // Starts the drag
                v.startDrag(
                        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)
                )
            }
        }
        

    Java

        // Create a string for the ImageView label
        private static final String IMAGEVIEW_TAG = "icon bitmap"
    
        // Creates a new ImageView
        ImageView imageView = new ImageView(this);
    
        // Sets the bitmap for the ImageView from an icon bit map (defined elsewhere)
        imageView.setImageBitmap(iconBitmap);
    
        // Sets 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(new View.OnLongClickListener() {
    
            // Defines the one method for the interface, which is called when the View is long-clicked
            public boolean onLongClick(View 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(v.getTag());
    
            // Create a new ClipData using the tag as a label, the plain text MIME type, and
            // the already-created item. This will create a new ClipDescription object within the
            // ClipData, and set its MIME type entry to "text/plain"
            ClipData dragData = new ClipData(
                v.getTag(),
                new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
                item);
    
            // Instantiates the drag shadow builder.
            View.DragShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
            // Starts the drag
    
                    v.startDrag(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)
                    );
    
            }
        }
        
  2. En el siguiente fragmento de código, se define myDragShadowBuilder. Crea una sombra de arrastre para arrastrar un TextView como un rectángulo gris pequeño:

    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) {
                // Sets the width of the shadow to half the width of the original View
                val width: Int = view.width / 2
    
                // Sets 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 will provide. As a result, the drag shadow will fill the
                // Canvas.
                shadow.setBounds(0, 0, width, height)
    
                // Sets the size parameter's width and height values. These get back to the system
                // through the size parameter.
                size.set(width, height)
    
                // Sets 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 in onProvideShadowMetrics().
            override fun onDrawShadow(canvas: Canvas) {
                // Draws the ColorDrawable in 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 thing
            private static Drawable shadow;
    
                // Defines the constructor for myDragShadowBuilder
                public MyDragShadowBuilder(View v) {
    
                    // Stores the View parameter passed to myDragShadowBuilder.
                    super(v);
    
                    // Creates a draggable image that will fill 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
                    private int width, height;
    
                    // Sets the width of the shadow to half the width of the original View
                    width = getView().getWidth() / 2;
    
                    // Sets 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 will provide. As a result, the drag shadow will fill the
                    // Canvas.
                    shadow.setBounds(0, 0, width, height);
    
                    // Sets the size parameter's width and height values. These get back to the system
                    // through the size parameter.
                    size.set(width, height);
    
                    // Sets 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 in onProvideShadowMetrics().
                @Override
                public void onDrawShadow(Canvas canvas) {
    
                    // Draws the ColorDrawable in the Canvas passed in from the system.
                    shadow.draw(canvas);
                }
            }
        

    Nota: Recuerda que no tienes que extender View.DragShadowBuilder. El constructor View.DragShadowBuilder(View) crea una sombra de arrastre predeterminada que tiene el mismo tamaño que el argumento View que se le pasó, con el punto de contacto centrado en la sombra de arrastre.

Cómo responder 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 en el diseño actual. Los objetos de escucha deben reaccionar llamando a 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 debe hacer lo siguiente:

  1. Llamar a getClipDescription() para obtener ClipDescription. Usar los métodos de tipo MIME en ClipDescription 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 eso no sea necesario.

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

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

Cómo manejar eventos durante el arrastre

Durante la operación de arrastre, los objetos de escucha que mostraron true en respuesta al evento de arrastre ACTION_DRAG_STARTED siguen 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 de la View del objeto de escucha.

Durante el arrastre, los objetos de escucha usan principalmente eventos de arrastre para decidir si deben cambiar el aspecto de su View.

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

  • ACTION_DRAG_ENTERED: El objeto de escucha recibe esto cuando el punto de contacto (el punto en la pantalla debajo del dedo del usuario) entra en el cuadro de límite de la 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 de contacto. Los métodos getX() y getY() muestran las coordenadas X e Y del punto de contacto.
  • ACTION_DRAG_EXITED: Este evento se envía a un objeto de escucha que previamente recibió ACTION_DRAG_ENTERED, después de que la sombra de arrastre ya no está dentro del cuadro de límite de la View del objeto de escucha.

El objeto de escucha no necesita reaccionar a ninguno de estos tipos de acciones. Si el objeto de escucha muestra un valor al sistema, se ignora. A continuación, se incluyen algunos lineamientos para responder a cada uno de estos tipos de acciones:

  • En respuesta a ACTION_DRAG_ENTERED o ACTION_DRAG_LOCATION, el objeto de escucha puede cambiar el aspecto de la View para indicar que está a punto de recibir una 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 de contacto. Es posible que el objeto de escucha quiera usar esta información para alterar el aspecto de esa parte de la View que se encuentra en el punto de contacto. El objeto de escucha también puede usar esta información para determinar la posición exacta donde el usuario va a soltar la sombra de arrastre.
  • 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 la View ya no es un objetivo inminente de la acción de soltar.

Cómo responder a la acción de soltar

Cuando el usuario libera la sombra de arrastre en una View en la app, si esa View informó anteriormente que podía aceptar el contenido que se estaba arrastrando, el sistema envía un evento de arrastre a esa View con el tipo de acción ACTION_DROP. El objeto de escucha debe hacer lo siguiente:

  1. Llamar a getClipData() para obtener el objeto ClipData que se suministró originalmente en la llamada a startDrag() y guardarlo. Si la operación de arrastrar y soltar no representa movimiento de datos, es posible que esto no sea necesario.
  2. Mostrar 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 mostrado por getResult() para un evento ACTION_DRAG_ENDED.

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

Para un evento ACTION_DROP, getX() y getY() muestran las posiciones X e Y del punto de arrastre en el momento en que se suelta, utilizando el sistema de coordenadas de la View que recibió la acción de soltar.

El sistema permite al usuario liberar la sombra de arrastre en una View cuyo objeto de escucha no recibe eventos de arrastre. También permitirá al usuario liberar la sombra de arrastre en regiones vacías de la IU de la aplicación o en áreas fuera de tu aplicación. En todos estos casos, el sistema no envía un evento con el tipo de acción ACTION_DROP, aunque sí envía un evento ACTION_DRAG_ENDED.

Cómo responder 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 a todos los objetos de escucha de eventos de arrastre de tu aplicación con un tipo de acción de ACTION_DRAG_ENDED. Esto indica que finalizó la operación de arrastre.

Cada objeto de escucha debe hacer lo siguiente:

  1. Si el objeto de escucha cambió el aspecto del objeto View durante la operación, debería restablecer la View a su aspecto predeterminado. Esta es una indicación visual para el usuario de que terminó la operación.
  2. El objeto de escucha puede llamar opcionalmente 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() mostrará un valor booleano true. En todos los demás casos, getResult() muestra un valor booleano false, incluido cualquier caso en el que el sistema no haya enviado un evento ACTION_DROP.
  3. El objeto de escucha debe mostrar un valor booleano true al sistema.

Cómo responder a eventos de arrastre: un 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 cómo reaccionar a eventos de arrastre en un objeto de escucha:

Kotlin

    // Creates a new drag event listener
    private val dragListen = View.OnDragListener { v, event ->

        // Handles each of the expected events
        when (event.action) {
            DragEvent.ACTION_DRAG_STARTED -> {
                // Determines if this View can accept the dragged data
                if (event.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. 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. Return true; the return value is ignored.
                (v as? ImageView)?.setColorFilter(Color.GREEN)

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

            DragEvent.ACTION_DRAG_LOCATION ->
                // Ignore the event
                true
            DragEvent.ACTION_DRAG_EXITED -> {
                // Re-sets the color tint to blue. Returns true; the return value is ignored.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

                // Invalidate the view to force a redraw in the new tint
                v.invalidate()
                true
            }
            DragEvent.ACTION_DROP -> {
                // Gets the item containing the dragged data
                val item: ClipData.Item = event.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(event.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 OnDragListener.")
                false
            }
        }
    }
    ...
    val imageView = ImageView(this)

    // Sets the drag event listener for the View
    imageView.setOnDragListener(dragListen)
    

Java

    // Creates a new drag event listener
    dragListen = new myDragEventListener();

    View imageView = new ImageView(this);

    // Sets the drag event listener for the View
    imageView.setOnDragListener(dragListen);

    ...

    protected class myDragEventListener implements View.OnDragListener {

        // This is the method that the system calls when it dispatches a drag event to the
        // listener.
        public boolean onDrag(View v, DragEvent event) {

            // Defines a variable to store the action type for the incoming event
            final int action = event.getAction();

            // Handles each of the expected events
            switch(action) {

                case DragEvent.ACTION_DRAG_STARTED:

                    // Determines if this View can accept the dragged data
                    if (event.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.
                        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. 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. Return true; the return value is ignored.

                    v.setColorFilter(Color.GREEN);

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

                    return true;

                case DragEvent.ACTION_DRAG_LOCATION:

                    // Ignore the event
                    return true;

                case DragEvent.ACTION_DRAG_EXITED:

                    // Re-sets the color tint to blue. Returns true; the return value is ignored.
                    v.setColorFilter(Color.BLUE);

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

                    return true;

                case DragEvent.ACTION_DROP:

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

                    // Gets the text data from the item.
                    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
                    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
                    v.clearColorFilter();

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

                    // Does a getResult(), and displays what happened.
                    if (event.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 OnDragListener.");
                    break;
            }

            return false;
        }
    };
    

Permisos de arrastre en modo multiventana

Los dispositivos que ejecutan Android 7.0 (API nivel 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:

  • App de origen: La app que originalmente contiene los datos. Es donde comienza el arrastre.
  • App de destino: La app que recibe los datos. Es donde termina el arrastre.

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 la app, las apps 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, la app puede llamar a release() en el objeto que se mostró desde requestDragAndDropPermissions(). De lo contrario, el permiso se libera cuando se destruye la actividad que lo 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. Se incluye un ejemplo más completo dentro de la muestra DragAndDropAcrossApps, disponible en GitHub.

SourceDragAndDropActivity

Kotlin

    // Drag a file stored under an "images/" directory within internal storage.
    val internalImagesDir = File(context.filesDir, "images")
    val imageFile = File(internalImagesDir, file-name)
    val uri: Uri = FileProvider.getUriForFile(
            context, file-provider-content-authority, imageFile)

    // Container for where the image originally appears in the source app.
    val srcImageView = findViewById(R.id.my-image-id)

    val listener = DragStartHelper.OnDragStartListener = { view, _ ->
        val clipData = ClipData(clip-description, 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, drag-shadow-builder, null, flags)
    }

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

Java

    // Drag a file stored under an "images/" directory within internal storage.
    File internalImagesDir = new File(context.filesDir, "images");
    File imageFile = new File(internalImagesDir, file-name);
    final Uri uri = FileProvider.getUriForFile(
            context, file-provider-content-authority, imageFile);

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

    DragStartHelper.OnDragStartListener listener =
            new DragStartHelper.OnDragStartListener() {
                @Override
                public boolean onDragStart(View v, DragStartHelper helper) {
                    ClipData clipData = new ClipData(
                            clip-description, 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 v.startDragAndDrop(clipData, drag-shadow-builder, null, flags);
                }
            };

    // Detect and start the drag event.
    DragStartHelper helper = new DragStartHelper(srcImageView, listener);
    helper.attach();
    

TargetDragAndDropActivity

Kotlin

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

    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.
        }
    }
    

Java

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

    targetImageView.setOnDragListener(
            new View.OnDragListener() {
                @Override
                public boolean onDrag(View view, DragEvent dragEvent) {
                    switch (dragEvent.getAction()) {
                        case ACTION_DROP:
                            ClipData.Item imageItem =
                                    dragEvent.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(dragEvent);
                            ((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.
                    }
                }
            });