El framework para arrastrar y soltar de Android te permite agregar funciones interactivas de arrastrar y soltar a tu app. Con esta función, los usuarios pueden copiar o mover texto, imágenes, objetos y cualquier contenido que se pueda representar con un URI, de un View
a otro dentro de una app o entre apps en el modo multiventana.
|
|
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, puedes usar el framework para otras acciones de la IU. Por ejemplo, puedes crear una app que mezcle colores cuando el usuario arrastre un ícono de color sobre otro ícono. Sin embargo, el resto del documento describe el framework de arrastrar y soltar en el contexto de la transferencia de datos.
Descripción general
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 está iniciando 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, denominada 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 a 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 esta se encuentra sobre un destino para soltar.
Para crear un objeto de escucha de eventos de arrastre, implementa View.OnDragListener
. Configura el objeto de escucha de 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 envíe 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 View
que acepta 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 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, puedes modificar la implementación predeterminada de onDragEvent()
sin anular el método. Configura un objeto OnReceiveContentListener
en una vista. Para obtener más detalles, consulta setOnReceiveContentListener()
.
Luego, 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 arrastrar y soltar se sueltan en la vista. Los datos se pasan al método como un objetoContentInfo
. El método invoca el elementoOnReceiveContentListener
.El resultado es verdadero si los datos de arrastrar y soltar se sueltan en la vista y el objeto
OnReceiveContentListener
consume el contenido.
Define el elemento OnReceiveContentListener
para controlar los datos específicamente para tu app. Para ofrecer 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 usan con los eventos táctiles.
Proceso de arrastrar y soltar
El proceso de arrastrar y soltar tiene cuatro pasos o estados: iniciado, continuo, 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: El sistema responde llamando a tu aplicación 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 objetosView
del diseño actual. Para seguir recibiendo eventos de arrastre, incluido un posible evento de soltar, el objeto de escucha de eventos de arrastre debe mostrartrue
. Esto registra el objeto de escucha en el sistema. Solo los objetos de escucha registrados siguen recibiendo eventos de arrastre. En este punto, los objetos de escucha también pueden cambiar la apariencia de su objetoView
del destino para soltar a fin de mostrar que la vista puede aceptar un evento de soltar. : Si el objeto de escucha de eventos de arrastre muestrafalse
, 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ónACTION_DRAG_ENDED
. Al mostrarfalse
, el objeto de escucha le indica al sistema que no está interesado en la operación de arrastrar y soltar, y que 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 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 podría modificar el aspecto del destino para soltar
View
en respuesta al evento. Por ejemplo, si el evento indica que la sombra de arrastre ingresa en el cuadro de límite del destino para soltar (tipo de acciónACTION_DRAG_ENTERED
), el objeto de escucha puede reaccionar destacandoView
. - 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 de evento de arrastre contiene los datos que pasan al sistema en la llamada astartDragAndDrop()
que inicia la operación. Se espera que el objeto de escucha muestre un valor booleanotrue
al sistema si procesa correctamente los datos descartados. : Este paso solo ocurre si el usuario suelta la sombra de arrastre dentro del cuadro de límite de unView
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 arrastreACTION_DROP
. - Finalizada
Después de que el usuario suelta la sombra de arrastre y después de que el sistema envíe
un evento de arrastre con el tipo de acción
ACTION_DROP
. Si es necesario, el sistema envía un evento de arrastre con el tipo de acciónACTION_DRAG_ENDED
para indicar que finalizó la operación de arrastrar y soltar. Esto se hace independientemente del lugar en el que el usuario suelte la sombra de arrastre. El evento se envía a todos los objetos de escucha que están registrados para recibir eventos de arrastre, incluso si también reciben el eventoACTION_DROP
.
Cada uno de estos 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
, que se describen en la tabla 1:
Tipo de acción | Significado |
---|---|
ACTION_DRAG_STARTED |
La aplicación llama a startDragAndDrop() y obtiene una sombra de arrastre. Si el objeto de escucha desea seguir recibiendo eventos de arrastre para esta operación, debe mostrar un valor booleano true al sistema.
|
ACTION_DRAG_ENTERED |
La sombra de arrastre ingresa en el cuadro delimitador del 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 en el cuadro de límite.
|
ACTION_DRAG_LOCATION |
Luego de un evento ACTION_DRAG_ENTERED , la sombra de arrastre permanece dentro del cuadro delimitador del objeto View del objeto de escucha de eventos de arrastre.
|
ACTION_DRAG_EXITED |
Después de un evento ACTION_DRAG_ENTERED y al menos un evento ACTION_DRAG_LOCATION , la sombra de arrastre se mueve fuera del cuadro de límite del View del objeto de escucha de eventos de arrastre.
|
ACTION_DROP |
La sombra de arrastre se libera sobre el View del objeto de escucha de eventos de arrastre. Este tipo de acción se envía al objeto de escucha de un objeto View solo si el objeto de escucha muestra 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 sobre un 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.
El objeto de escucha muestra un valor booleano |
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 envía un ACTION_DROP , recibir el tipo de acción ACTION_DRAG_ENDED no implica que la acción de soltar se haya realizado correctamente. El objeto de escucha debe llamar a getResult() , como se muestra en la tabla 2, para obtener el valor que se muestra en respuesta a ACTION_DROP . Si no se envía un evento ACTION_DROP , getResult() muestra false .
|
El objeto DragEvent
también contiene los datos y metadatos que tu aplicación proporciona 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 la sección Una operación de arrastrar y soltar.
getAction() valor |
getClipDescription() valor |
getLocalState() valor |
getX() valor |
getY() valor |
getClipData() valor |
getResult() valor |
---|---|---|---|---|---|---|
ACTION_DRAG_STARTED |
✓ | ✓ | ✓ | ✓ | ||
ACTION_DRAG_ENTERED |
✓ | ✓ | ||||
ACTION_DRAG_LOCATION |
✓ | ✓ | ✓ | ✓ | ||
ACTION_DRAG_EXITED |
✓ | ✓ | ||||
ACTION_DROP |
✓ | ✓ | ✓ | ✓ | ✓ | |
ACTION_DRAG_ENDED |
✓ | ✓ |
Los métodos getAction()
, describeContents()
, writeToParcel()
y toString()
de DragEvent
siempre muestran datos válidos.
Si un método no contiene datos válidos para un tipo de acción determinado, 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 denomina sombra de arrastre. Se crea con métodos que declaras para un objeto View.DragShadowBuilder
. Pasa el compilador al sistema cuando inicias una operación de arrastrar y soltar con startDragAndDrop()
. Como parte de la respuesta a startDragAndDrop()
, el sistema invoca los métodos de devolución de llamada que defines 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 objetoView
en el objetoView.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 un objetoView
que el usuario seleccione para iniciar la operación de arrastre.Si usas este constructor, no necesitas extender
View.DragShadowBuilder
ni anular sus métodos. De forma predeterminada, obtienes una sombra de arrastre que tiene el mismo aspecto que el objetoView
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á ningún objeto
View
disponible en el objetoView.DragShadowBuilder
. El campo se configura comonull
. Debes extenderView.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
: Es un objetoPoint
. El ancho de la sombra de arrastre va enx
y su altura va eny
.outShadowTouchPoint
: Es un objetoPoint
. 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 enx
y su posición Y va eny
.onDrawShadow()
Inmediatamente después de la llamada a
onProvideShadowMetrics()
, el sistema llama aonDrawShadow()
para crear la sombra de arrastre. El método tiene un solo argumento, un objetoCanvas
que el sistema construye a partir de los parámetros que proporcionas enonProvideShadowMetrics()
. El método dibuja la sombra de arrastre en el objetoCanvas
proporcionado.
Para mejorar el rendimiento, mantén pequeño el tamaño de la sombra de arrastre. Para un solo elemento, es posible que desees usar un ícono. Para una selección de varios elementos, es posible que desees usar í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 cómo iniciar un arrastre, responder a eventos durante este, 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 suele ser mantener presionado un objeto View
. En respuesta, tu app debe hacer lo siguiente:
Crea un objeto
ClipData
y un objetoClipData.Item
para los datos que se estén moviendo. Como parte deClipData
, proporciona metadatos que se almacenen en un objetoClipDescription
dentro deClipData
. Para una operación de arrastrar y soltar que no represente movimiento de datos, te recomendamos que usesnull
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 objetoClipData
que contiene la etiqueta de unImageView
:Kotlin
// Create a string for the ImageView label. val IMAGEVIEW_TAG = "icon bitmap" ... val imageView = ImageView(context).apply { // Set the bitmap for the ImageView from an icon bitmap 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(view: 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 is 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(context); // Set the bitmap for the ImageView from an icon bitmap defined elsewhere. imageView.setImageBitmap(iconBitmap); // Set the tag. imageView.setTag(IMAGEVIEW_TAG); // Set 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 is handled. return true; });
Define
myDragShadowBuilder
anulando los métodos enView.DragShadowBuilder
. En el siguiente fragmento de código, se crea una pequeña sombra de arrastre gris rectangular y gris para unTextView
:Kotlin
private class MyDragShadowBuilder(view: View) : View.DragShadowBuilder(view) { private val shadow = ColorDrawable(Color.LTGRAY) // Define 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. Set 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) } // Define 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 view) { // Store the View parameter. super(view); // Create a draggable image that fills the Canvas provided by the // system. shadow = new ColorDrawable(Color.LTGRAY); } // Define a callback that sends the drag shadow dimensions and touch point // back to the system. @Override public void onProvideShadowMetrics (Point size, Point touch) { // Define 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. Set 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); } // Define 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 reaccionan 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 debe hacer lo siguiente:
Llama a
DragEvent.getClipDescription()
y usa los métodos de tipo de MIME en el objetoClipDescription
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 ningún movimiento de datos, es posible que esta acción no sea necesaria.
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 mostrarfalse
, y el sistema dejará de enviar eventos de arrastre hasta que envíeACTION_DRAG_ENDED
para finalizar la operación de arrastrar y soltar.
Para un evento ACTION_DRAG_STARTED
, los siguientes métodos DragEvent
no son válidos: 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 muestran 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 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) ingresa al cuadro de límite delView
del objeto de escucha.ACTION_DRAG_LOCATION
: Una vez que el objeto de escucha recibe un eventoACTION_DRAG_ENTERED
, recibe un eventoACTION_DRAG_LOCATION
nuevo cada vez que se mueve el punto táctil hasta que recibe un eventoACTION_DRAG_EXITED
. Los métodosgetX()
ygetY()
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 recibeACTION_DRAG_ENTERED
anteriormente. El evento se envía cuando el punto táctil de la sombra de arrastre se mueve desde el cuadro de límite del elementoView
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 objeto de escucha muestra un valor al sistema, 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
oACTION_DRAG_LOCATION
, el objeto de escucha puede cambiar el aspecto deView
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 paragetX()
ygetY()
que corresponden a la ubicación del punto táctil. El objeto de escucha puede usar esta información para modificar el aspecto deView
en el punto táctil o determinar la posición exacta en la que el usuario puede liberar la sombra de arrastre, es decir, descartar los datos. - En respuesta a
ACTION_DRAG_EXITED
, el objeto de escucha debe restablecer cualquier cambio de aspecto que aplique en respuesta aACTION_DRAG_ENTERED
oACTION_DRAG_LOCATION
. Esto le indica al usuario que el elementoView
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 una View
, y el View
informa 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:
Llama a
getClipData()
para obtener el objetoClipData
que se suministra originalmente en la llamada astartDragAndDrop()
y procesa los datos. Si la operación de arrastrar y soltar no representa movimiento de datos, esto no es necesario.Muestra un valor booleano
true
para indicar que la acción de soltar se procesa correctamente ofalse
si no lo es. El valor mostrado se convierte en el valor quegetResult()
muestra para el eventoACTION_DRAG_ENDED
final. Si el sistema no envía un eventoACTION_DROP
, el valor que muestragetResult()
para un eventoACTION_DRAG_ENDED
esfalse
.
En el caso de un evento ACTION_DROP
, getX()
y getY()
usan el sistema de coordenadas de View
que recibe la bajada para mostrar la posición Y y X del punto táctil en el momento en que se produce.
El sistema permite al usuario soltar la sombra de arrastre sobre un View
cuyo objeto de escucha de eventos de arrastre no recibe eventos de arrastre. También permite al usuario soltar la sombra de arrastre sobre regiones vacías de la IU de la aplicación o sobre á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 el sistema envía 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 de 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:
- Si el objeto de escucha cambia el aspecto de su objeto
View
durante la operación, debe restablecer el objetoView
a su aspecto predeterminado. Esta es una indicación visual para el usuario de que terminó la operación. - 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 muestratrue
en respuesta a un evento del tipo de acciónACTION_DROP
,getResult()
muestra un valor booleanotrue
. En todos los demás casos,getResult()
muestra un valor booleanofalse
, incluso cuando el sistema no envía un eventoACTION_DROP
. - Para indicar que la operación de arrastrar y soltar se completó correctamente, 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 -> // Handle each of the expected events. when (e.action) { DragEvent.ACTION_DRAG_STARTED -> { // Determine whether this View can accept the dragged data. if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { // As an example, apply 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() // Return true to indicate that the View can accept the dragged // data. true } else { // Return false to indicate that, during the current drag and // drop operation, this View doesn't receive events again until // ACTION_DRAG_ENDED is sent. false } } DragEvent.ACTION_DRAG_ENTERED -> { // Apply a green tint to the View. (v as? ImageView)?.setColorFilter(Color.GREEN) // Invalidate the view to force a redraw in the new tint. v.invalidate() // Return true. The value is ignored. true } DragEvent.ACTION_DRAG_LOCATION -> // Ignore the event. true DragEvent.ACTION_DRAG_EXITED -> { // Reset the color tint to blue. (v as? ImageView)?.setColorFilter(Color.BLUE) // Invalidate the view to force a redraw in the new tint. v.invalidate() // Return true. The value is ignored. true } DragEvent.ACTION_DROP -> { // Get the item containing the dragged data. val item: ClipData.Item = e.clipData.getItemAt(0) // Get the text data from the item. val dragData = item.text // Display a message containing the dragged data. Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show() // Turn off color tints. (v as? ImageView)?.clearColorFilter() // Invalidate the view to force a redraw. v.invalidate() // Return true. DragEvent.getResult() returns true. true } DragEvent.ACTION_DRAG_ENDED -> { // Turn off color tinting. (v as? ImageView)?.clearColorFilter() // Invalidate the view to force a redraw. v.invalidate() // Do a getResult() and display what happens. 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() // Return true. The value is ignored. true } else -> { // An unknown action type is 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) -> { // Handle each of the expected events. switch(e.getAction()) { case DragEvent.ACTION_DRAG_STARTED: // Determine whether this View can accept the dragged data. if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { // As an example, apply 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(); // Return true to indicate that the View can accept the dragged // data. return true; } // Return false to indicate that, during the current drag and drop // operation, this View doesn't receive events again until // ACTION_DRAG_ENDED is sent. return false; case DragEvent.ACTION_DRAG_ENTERED: // Apply a green tint to the View. ((ImageView)v).setColorFilter(Color.GREEN); // Invalidate the view to force a redraw in the new tint. v.invalidate(); // Return true. The value is ignored. return true; case DragEvent.ACTION_DRAG_LOCATION: // Ignore the event. return true; case DragEvent.ACTION_DRAG_EXITED: // Reset the color tint to blue. ((ImageView)v).setColorFilter(Color.BLUE); // Invalidate the view to force a redraw in the new tint. v.invalidate(); // Return true. The value is ignored. return true; case DragEvent.ACTION_DROP: // Get the item containing the dragged data. ClipData.Item item = e.getClipData().getItemAt(0); // Get the text data from the item. CharSequence dragData = item.getText(); // Display a message containing the dragged data. Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show(); // Turn off color tints. ((ImageView)v).clearColorFilter(); // Invalidate the view to force a redraw. v.invalidate(); // Return true. DragEvent.getResult() returns true. return true; case DragEvent.ACTION_DRAG_ENDED: // Turn off color tinting. ((ImageView)v).clearColorFilter(); // Invalidate the view to force a redraw. v.invalidate(); // Do a getResult() and displays what happens. 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(); } // Return true. The value is ignored. return true; // An unknown action type is 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 mover datos de una app a otra mediante una operación de arrastrar y soltar. Para obtener más información, consulta Compatibilidad con el modo multiventana.
La app de origen, donde comienza la operación de arrastrar y soltar, proporciona los datos. La app de destino, donde finaliza la operación de arrastrar y soltar, recibe los datos.
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. Para ello, debes hacer lo siguiente:
- La app de origen debe establecer una de las marcas
DRAG_FLAG_GLOBAL_URI_READ
yDRAG_FLAG_GLOBAL_URI_WRITE
, o ambas, según el acceso de lectura o escritura a los datos que la app de origen desee otorgarle a la app de destino. - La app de destino debe llamar a
requestDragAndDropPermissions()
inmediatamente antes de controlar los datos que el usuario arrastra a la app. Si la app de destino ya no necesita acceder a los datos de arrastrar y soltar, la app puede llamar arelease()
en el objeto que se mostró desderequestDragAndDropPermissions()
. De lo contrario, los permisos se liberan cuando se destruye la actividad que los contiene. Si tu implementación implica iniciar una actividad nueva para procesar los URI descartados, deberás otorgarle los mismos permisos a la actividad nueva. Debes configurar los datos de clip y una marca:Kotlin
intent.setClipData(clipData) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
Java
intent.setClipData(clipData); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
En los siguientes fragmentos de código, se muestra cómo liberar el acceso de solo lectura para arrastrar y soltar datos inmediatamente después de que se lleva a cabo la operación de arrastrar y soltar. Consulta la muestra de DragAndDrop en GitHub para ver un ejemplo más completo.
Actividad de arrastrar y soltar de origen
Kotlin
// Drag a file stored in an images/ directory in internal storage. 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 permit 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 in 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 permit 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 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 afterward because it's no // longer needed. dropPermissions.release() return@setOnDragListener true } // Implement logic for other DragEvent cases here. // An unknown action type is 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 afterward 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.
Cómo especificar destinos para soltar
DropHelper.configureView()
es un método estático y sobrecargado que te permite especificar destinos para soltar. Sus parámetros incluyen los siguientes:
- El
Activity
actual, que se usa para los permisos de URI- Es un objeto
View
que funciona como destino para soltar.- Los tipos de MIME que el destino para soltar puede aceptar de los datos descartados.
- Es un objeto
- Opciones de configuración para el destino para soltar; en particular, una lista de campos
EditText
incorporados - Un objeto
OnReceiveContentListener
para controlar los datos soltados
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 establece en el color secundario (o de elementos destacados) del tema, el radio de la esquina de resaltado se establece en 16 dp y la lista de componentes EditText
está vacía. Consulta la siguiente sección para obtener más detalles.
Configura destinos 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 la sección anterior para obtener más información.
Personalizar destacado de 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, y DropHelper.Options
te permite establecer el color del resaltado y especificar el radio de 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, como se muestra en el siguiente 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();
Controla componentes de EditText en destinos para soltar
DropHelper
también controla el enfoque dentro del destino para soltar cuando el destino 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 uno o más componentes EditText
, proporciona una lista de los componentes a DropHelper.Options.Builder.addInnerEditTexts(EditText...)
a fin de garantizar que el resaltado 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:
- El
EditText
en el que se descarta elClipData
. - El
EditText
que contiene el cursor de texto (signo de intercalación). - El primer
EditText
proporcionado a la llamada aDropHelper.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();
Cómo controlar datos en destinos para soltar
El método DropHelper.configureView()
acepta una OnReceiveContentListener
que creas para controlar la función 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 que se proporcionan al destino para soltar a través de interacciones del usuario distintas de 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
, si el usuario ejecuta una versión de Android anterior a la 7.0.
Tipos de MIME, permisos y validación de contenido
La verificación del tipo de MIME de DropHelper
se basa en la función de arrastrar y soltar ClipDescription
, que crea la app que proporciona los datos de arrastrar y soltar. Valida ClipDescription
para asegurarte de que los tipos de MIME estén configurados correctamente.
DropHelper
solicita todos los permisos de acceso para los URI de contenido que se encuentran en la función ClipData
de arrastrar y soltar. Para obtener más información, consulta DragAndDropPermissions
. Los permisos te permiten resolver los URI de contenido cuando procesas los datos de arrastrar y soltar.
DropHelper
no valida los datos que muestran los proveedores de contenido cuando resuelven URIs en los datos soltados. Comprueba la nulabilidad y la precisión de los datos resueltos.