Attiva il trascinamento

Il framework di trascinamento di Android ti consente di aggiungere funzionalità interattive di trascinamento alla tua app. Con il trascinamento, gli utenti possono copiare o spostare testo, immagini, oggetti e qualsiasi contenuto che può essere rappresentato da un URI, da un elemento View a un altro all'interno di un'app o tra app in modalità multi-finestra.

Stringa di testo e immagine trascinate all'interno di un'app. Stringa di testo e immagine trascinate tra le app in modalità schermo diviso.
Figura 1. Trascina all'interno di un'app.
Figura 2. Trascinare le app tra le app.

Il framework include una classe di eventi di trascinamento, listener di trascinamento e classi e metodi helper. Sebbene sia progettato principalmente per consentire il trasferimento dei dati, puoi utilizzare il framework per altre azioni dell'interfaccia utente. Ad esempio, puoi creare un'app che mescola i colori quando l'utente trascina un'icona di un colore su un'altra. Tuttavia, il resto del documento descrive il framework a trascinamento nel contesto del trasferimento dei dati.

Panoramica

Un'operazione di trascinamento inizia quando l'utente esegue un gesto nell'interfaccia utente che la tua app riconosce come indicatore per iniziare a trascinare i dati. In risposta, l'app comunica al sistema l'avvio di un'operazione di trascinamento. Il sistema richiama l'app per ottenere una rappresentazione dei dati trascinati, chiamata ombra di trascinamento.

Quando l'utente sposta l'ombra di trascinamento sul layout dell'app, il sistema invia eventi di trascinamento ai listener di eventi di trascinamento e ai metodi di callback associati agli oggetti View nel layout. Se l'utente rilascia l'ombra di trascinamento su una vista che può accettare i dati (una destinazione di rilascio), il sistema invia i dati alla destinazione. L'operazione di trascinamento termina quando l'utente rilascia l'ombra di trascinamento, indipendentemente dal fatto che l'ombra si trovi o meno sopra una destinazione.

Crea un listener di eventi di trascinamento implementando View.OnDragListener. Imposta il listener per una destinazione di rilascio con il metodo setOnDragListener() dell'oggetto View. Ogni vista nel layout ha anche un metodo di callback onDragEvent().

L'applicazione comunica al sistema di avviare un'operazione di trascinamento chiamando il metodo startDragAndDrop(), che indica al sistema di inviare eventi di trascinamento. Il metodo fornisce inoltre al sistema i dati che l'utente sta trascinando e i metadati che li descrivono. Puoi chiamare startDragAndDrop() su qualsiasi View nel layout corrente. Il sistema utilizza l'oggetto View solo per ottenere l'accesso alle impostazioni globali nel layout.

Durante l'operazione di trascinamento, il sistema invia gli eventi di trascinamento ai listener degli eventi di trascinamento o ai metodi di callback degli oggetti View nel layout. Gli ascoltatori o i metodi di callback utilizzano i metadati per decidere se accettare o meno i dati quando vengono eliminati. Se l'utente rilascia i dati su una destinazione di rilascio, ovvero un View che accetta i dati, il sistema invia un oggetto evento di trascinamento contenente i dati al metodo di callback o al listener di eventi di trascinamento della destinazione di rilascio.

Trascinare i listener di eventi e i metodi di callback

Un View riceve eventi di trascinamento con un listener di eventi di trascinamento che implementa View.OnDragListener o con il metodo di callback onDragEvent() della vista. Quando il sistema chiama il metodo o il listener, fornisce un argomento DragEvent.

Nella maggior parte dei casi, è preferibile utilizzare un listener rispetto al metodo di callback. Quando progetta le UI, in genere non sottoclassi View, ma il metodo di callback ti costringe a creare sottoclassi per sostituire il metodo. Al confronto, puoi implementare una classe listener e quindi utilizzarla con più oggetti View diversi. Puoi implementarla anche come classe in linea anonima o espressione lambda. Per impostare il listener per un oggetto View, chiama setOnDragListener().

In alternativa, puoi modificare l'implementazione predefinita di onDragEvent() senza eseguire l'override del metodo. Imposta un OnReceiveContentListener su una vista. Per ulteriori dettagli, consulta setOnReceiveContentListener(). Per impostazione predefinita, il metodo onDragEvent() effettua quindi le seguenti operazioni:

  • Restituisce true in risposta alla chiamata a startDragAndDrop().
  • Richiama performReceiveContent() se i dati relativi al trascinamento vengono rilasciati nella visualizzazione. I dati vengono passati al metodo come oggetto ContentInfo. Il metodo richiama OnReceiveContentListener.

  • Restituisce true se i dati di trascinamento vengono rilasciati nella visualizzazione e OnReceiveContentListener consuma qualsiasi contenuto.

Definisci il OnReceiveContentListener per gestire i dati specifici per la tua app. Per la compatibilità con le versioni precedenti fino al livello API 24, utilizza la versione Jetpack di OnReceiveContentListener.

Per un oggetto View puoi avere un listener di eventi di trascinamento e un metodo di callback, nel qual caso il sistema chiama prima il listener. Il sistema non chiama il metodo di callback, a meno che il listener non restituisca false.

La combinazione del metodo onDragEvent() e di View.OnDragListener è analogo alla combinazione di onTouchEvent() e View.OnTouchListener utilizzata con gli eventi tocco.

Procedura di trascinamento

Il processo di trascinamento prevede quattro passaggi o stati: avviato, continuo, eliminato e terminato.

Avviato

In risposta al gesto di trascinamento di un utente, l'applicazione chiama startDragAndDrop() per indicare al sistema di avviare un'operazione di trascinamento. Gli argomenti del metodo forniscono quanto segue:

  • I dati da trascinare.
  • Un callback per disegnare l'ombra di trascinamento
  • Metadati che descrivono i dati trascinati: il sistema risponde richiamando l'applicazione per ottenere un'ombra. Il sistema visualizza l'ombra di trascinamento sul dispositivo. : il sistema invia un evento di trascinamento con tipo di azione ACTION_DRAG_STARTED al listener di eventi di trascinamento di tutti gli oggetti View nel layout corrente. Per continuare a ricevere eventi di trascinamento, incluso un possibile evento di trascinamento, il listener di eventi di trascinamento deve restituire true. In questo modo il listener viene registrato nel sistema. Solo i listener registrati continuano a ricevere eventi di trascinamento. A questo punto, i listener possono anche modificare l'aspetto dell'oggetto View di destinazione di rilascio per indicare che la visualizzazione può accettare un evento di rilascio. : se il listener di eventi di trascinamento restituisce false, non riceve eventi di trascinamento per l'operazione corrente finché il sistema non invia un evento di trascinamento con tipo di azione ACTION_DRAG_ENDED. Se restituisci false, il listener comunica al sistema che non è interessato all'operazione di trascinamento e non vuole accettare i dati trascinati.
Avanzamento
L'utente continua a trascinare. Quando l'ombra di trascinamento interseca il riquadro di delimitazione di una destinazione, il sistema invia uno o più eventi di trascinamento al listener di eventi di trascinamento della destinazione. Il listener potrebbe modificare l'aspetto della destinazione di rilascio View in risposta all'evento. Ad esempio, se l'evento indica che l'ombra di trascinamento entra nel riquadro di delimitazione della destinazione del rilascio (tipo di azione ACTION_DRAG_ENTERED), l'ascoltatore può reagire evidenziando View.
Interrotto
L'utente rilascia l'ombra di trascinamento all'interno del riquadro di delimitazione di una destinazione. Il sistema invia al listener della destinazione di rilascio un evento di trascinamento con tipo di azione ACTION_DROP. L'oggetto dell'evento di trascinamento contiene i dati trasmessi al sistema nella chiamata a startDragAndDrop() che avvia l'operazione. Il listener dovrebbe restituire il valore booleano true al sistema se elabora correttamente i dati eliminati. : questo passaggio si verifica solo se l'utente rilascia l'ombra di trascinamento all'interno del riquadro di delimitazione di un View il cui listener è registrato per ricevere eventi di trascinamento (una destinazione di rilascio). Se l'utente rilascia l'ombra di trascinamento in qualsiasi altra situazione, non viene inviato alcun evento di trascinamento ACTION_DROP.
Terminata

Dopo che l'utente rilascia l'ombra di trascinamento e dopo che il sistema ha inviato

un evento di trascinamento con tipo di azione ACTION_DROP. Se necessario, il sistema invia un evento di trascinamento con tipo di azione ACTION_DRAG_ENDED per indicare che l'operazione di trascinamento è terminata. indipendentemente dal punto in cui l'utente rilascia l'ombra di trascinamento. L'evento viene inviato a ogni listener registrato per ricevere eventi di trascinamento, anche se il listener riceve anche l'evento ACTION_DROP.

Ciascuno di questi passaggi è descritto più dettagliatamente nella sezione Un'operazione di trascinamento.

Trascina eventi

Il sistema invia un evento di trascinamento sotto forma di oggetto DragEvent, che contiene un tipo di azione che descrive ciò che sta accadendo nel processo di trascinamento. A seconda del tipo di azione, l'oggetto può contenere anche altri dati.

I listener di eventi di trascinamento ricevono l'oggetto DragEvent. Per ottenere il tipo di azione, gli ascoltatori chiamano DragEvent.getAction(). Esistono sei valori possibili definiti da costanti nella classe DragEvent, descritti nella tabella 1:

Tabella 1. Tipi di azione DragEvent

Tipo di azione Significato
ACTION_DRAG_STARTED L'applicazione chiama startDragAndDrop() e ottiene un'ombra di trascinamento. Se il listener vuole continuare a ricevere eventi di trascinamento per questa operazione, deve restituire il valore booleano true al sistema.
ACTION_DRAG_ENTERED L'ombra di trascinamento entra nel riquadro di delimitazione del listener di eventi di trascinamento View. Questo è il primo tipo di azione evento che il listener riceve quando l'ombra di trascinamento entra nel riquadro di delimitazione.
ACTION_DRAG_LOCATION Dopo un evento ACTION_DRAG_ENTERED, l'ombra di trascinamento si trova ancora all'interno del riquadro di delimitazione dell'View del listener di eventi di trascinamento.
ACTION_DRAG_EXITED Dopo un evento ACTION_DRAG_ENTERED e almeno un evento ACTION_DRAG_LOCATION, l'ombra di trascinamento si sposta all'esterno del riquadro di delimitazione dell'View del listener di eventi di trascinamento.
ACTION_DROP L'ombra di trascinamento viene rilasciata su View del listener di eventi di trascinamento. Questo tipo di azione viene inviato al listener di un oggetto View solo se il listener restituisce true booleano in risposta all'evento di trascinamento ACTION_DRAG_STARTED. Questo tipo di azione non viene inviato se l'utente rilascia l'ombra di trascinamento su un View il cui listener non è registrato o se l'utente rilascia l'ombra di trascinamento su qualsiasi elemento che non fa parte del layout corrente.

Il listener restituisce true booleano se elabora correttamente l'eliminazione. In caso contrario, deve restituire false.

ACTION_DRAG_ENDED Il sistema sta terminando l'operazione di trascinamento. Questo tipo di azione non è necessariamente preceduto da un evento ACTION_DROP. Se il sistema invia un ACTION_DROP, la ricezione del tipo di azione ACTION_DRAG_ENDED non implica che l'eliminazione sia riuscita. Il listener deve chiamare getResult(), come mostrato nella tabella 2, per ottenere il valore che viene restituito in risposta a ACTION_DROP. Se un evento ACTION_DROP non viene inviato, getResult() restituisce false.

L'oggetto DragEvent contiene anche i dati e i metadati che l'applicazione fornisce al sistema nella chiamata a startDragAndDrop(). Alcuni dati sono validi solo per determinati tipi di azioni, come riepilogato nella tabella 2. Per ulteriori informazioni sugli eventi e sui relativi dati, consulta la sezione Un'operazione di trascinamento.

Tabella 2. Dati DragEvent validi per tipo di azione

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

I metodi DragEvent getAction(), describeContents(), writeToParcel() e toString() restituiscono sempre dati validi.

Se un metodo non contiene dati validi per un determinato tipo di azione, restituisce null o 0, a seconda del tipo di risultato.

Ombra di trascinamento

Durante un'operazione di trascinamento, il sistema mostra un'immagine che l'utente trascina. Per lo spostamento dei dati, questa immagine rappresenta i dati trascinati. Per le altre operazioni, l'immagine rappresenta alcuni aspetti dell'operazione di trascinamento.

L'immagine è chiamata ombra di trascinamento. Puoi crearlo con i metodi dichiarati per un oggetto View.DragShadowBuilder. Puoi passare il builder al sistema quando avvii un'operazione di trascinamento utilizzando startDragAndDrop(). Come parte della sua risposta a startDragAndDrop(), il sistema richiama i metodi di callback che hai definito in View.DragShadowBuilder per ottenere un'ombra di trascinamento.

La classe View.DragShadowBuilder ha due costruttori:

View.DragShadowBuilder(View)

Questo costruttore accetta tutti gli oggetti View dell'applicazione. Il costruttore archivia l'oggetto View nell'oggetto View.DragShadowBuilder, in modo che i callback possano accedervi per creare l'ombra di trascinamento. La vista non deve essere necessariamente un elemento View selezionato dall'utente per avviare l'operazione di trascinamento.

Se utilizzi questo costruttore, non devi estendere View.DragShadowBuilder o eseguire l'override dei relativi metodi. Per impostazione predefinita, ottieni un'ombra a trascinamento che ha lo stesso aspetto dell'oggetto View trasmesso come argomento, centrata sotto la posizione in cui l'utente tocca lo schermo.

View.DragShadowBuilder()

Se utilizzi questo costruttore, nell'oggetto View.DragShadowBuilder non sarà disponibile nessun oggetto View. Il campo è impostato su null. Devi estendere View.DragShadowBuilder e sostituire i relativi metodi, altrimenti otterrai un'ombra di trascinamento invisibile. Il sistema non genera un errore.

La classe View.DragShadowBuilder ha due metodi che insieme creano l'ombra di trascinamento:

onProvideShadowMetrics()

Il sistema chiama questo metodo subito dopo la chiamata a startDragAndDrop(). Utilizza il metodo per inviare al sistema le dimensioni e il punto di contatto dell'ombra di trascinamento. Il metodo ha due parametri:

outShadowSize: un oggetto Point. La larghezza dell'ombra di trascinamento viene inserita in x e l'altezza su y.

outShadowTouchPoint: un oggetto Point. Il punto di contatto è la posizione all'interno dell'ombra del trascinamento che deve trovarsi sotto il dito dell'utente durante il trascinamento. La posizione X va in x, mentre la posizione Y va in y.

onDrawShadow()

Subito dopo la chiamata a onProvideShadowMetrics(), il sistema chiama onDrawShadow() per creare l'ombra di trascinamento. Il metodo ha un singolo argomento, ovvero un oggetto Canvas che il sistema crea dai parametri che fornisci in onProvideShadowMetrics(). Il metodo disegna l'ombra di trascinamento sull'elemento Canvas fornito.

Per migliorare le prestazioni, riduci le dimensioni dell'ombra del trascinamento. Per un singolo elemento, ti consigliamo di utilizzare un'icona. Per la selezione di più elementi, puoi utilizzare icone impilate anziché immagini intere disposte sullo schermo.

Un'operazione di trascinamento

Questa sezione mostra passo passo come avviare un trascinamento, rispondere agli eventi durante il trascinamento, rispondere a un evento di trascinamento e terminare l'operazione di trascinamento.

Avvia un trascinamento

L'utente avvia il trascinamento con un gesto di trascinamento, in genere toccando e tenendo premuto, su un oggetto View. Di conseguenza, l'app deve:

  1. Crea un oggetto ClipData e un oggetto ClipData.Item per i dati da spostare. Come parte di ClipData, fornisci i metadati archiviati in un oggetto ClipDescription all'interno di ClipData. Per un'operazione di trascinamento che non rappresenta lo spostamento dei dati, potresti utilizzare null anziché un oggetto effettivo.

    Ad esempio, questo snippet di codice mostra come rispondere a un gesto tocco e pressione su un ImageView creando un oggetto ClipData che contiene il tag (o l'etichetta) di un ImageView:

    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;
    });
    
  2. Definisci myDragShadowBuilder sostituendo i metodi in View.DragShadowBuilder. Lo snippet di codice riportato di seguito crea una piccola ombra di trascinamento grigia e rettangolare per un elemento TextView:

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

Rispondere a un avvio del trascinamento

Durante l'operazione di trascinamento, il sistema invia gli eventi di trascinamento agli ascoltatori degli eventi di trascinamento degli oggetti View nel layout corrente. I listener reagiscono chiamando DragEvent.getAction() per ottenere il tipo di azione. All'inizio di un trascinamento, questo metodo restituisce ACTION_DRAG_STARTED.

In risposta a un evento con il tipo di azione ACTION_DRAG_STARTED, un listener di eventi di trascinamento deve:

  1. Chiama DragEvent.getClipDescription() e utilizza i metodi di tipo MIME nel valore ClipDescription restituito per vedere se il listener può accettare i dati trascinati.

    Se l'operazione di trascinamento non rappresenta lo spostamento dei dati, potrebbe non essere necessario.

  2. Se il listener di eventi di trascinamento può accettare un rilascio, deve restituire true per comunicare al sistema di continuare a inviare eventi di trascinamento al listener. Se il listener non può accettare un rilascio, deve restituire false e il sistema smette di inviare eventi di trascinamento al listener fino a quando non invia ACTION_DRAG_ENDED per concludere l'operazione di trascinamento.

Per un evento ACTION_DRAG_STARTED, i seguenti metodi DragEvent non sono validi: getClipData(), getX(), getY() e getResult().

Gestire gli eventi durante il trascinamento

Durante l'azione di trascinamento, i listener di eventi di trascinamento che restituiscono true in risposta all'evento di trascinamento ACTION_DRAG_STARTED continuano a ricevere gli eventi di trascinamento. I tipi di eventi di trascinamento che un listener riceve durante il trascinamento dipendono dalla posizione dell'ombra di trascinamento e dalla visibilità dell'elemento View del listener. I listener utilizzano gli eventi di trascinamento principalmente per decidere se modificare l'aspetto del proprio View.

Durante l'azione di trascinamento, DragEvent.getAction() restituisce uno dei tre valori seguenti:

  • ACTION_DRAG_ENTERED: il listener riceve questo tipo di azione dell'evento quando il punto di contatto (il punto sullo schermo sotto il dito o il mouse dell'utente) entra nel riquadro di delimitazione del View del listener.
  • ACTION_DRAG_LOCATION: una volta che il listener riceve un evento ACTION_DRAG_ENTERED, riceve un nuovo evento ACTION_DRAG_LOCATION ogni volta che il punto di contatto si sposta finché non riceve un evento ACTION_DRAG_EXITED. I metodi getX() e getY() restituiscono le coordinate X e Y del punto di contatto.
  • ACTION_DRAG_EXITED: questo tipo di azione evento viene inviato a un listener che in precedenza riceve ACTION_DRAG_ENTERED. L'evento viene inviato quando il punto di contatto dell'ombra di trascinamento viene spostato dall'interno del riquadro di delimitazione del View del listener all'esterno del riquadro di delimitazione.

Il listener di eventi di trascinamento non deve reagire a nessuno di questi tipi di azioni. Se il listener restituisce un valore al sistema, questo viene ignorato.

Di seguito sono riportate alcune linee guida per rispondere a ciascuno di questi tipi di azioni:

  • In risposta a ACTION_DRAG_ENTERED o ACTION_DRAG_LOCATION, il listener può modificare l'aspetto di View per indicare che la visualizzazione è un potenziale target di rilascio.
  • Un evento con il tipo di azione ACTION_DRAG_LOCATION contiene dati validi per getX() e getY() corrispondenti alla posizione del punto di contatto. L'ascoltatore può utilizzare queste informazioni per modificare l'aspetto di View nel punto di contatto o per determinare la posizione esatta in cui l'utente può rilasciare l'ombra di trascinamento, ovvero eliminare i dati.
  • In risposta a ACTION_DRAG_EXITED, il listener deve reimpostare qualsiasi modifica dell'aspetto applicata in risposta a ACTION_DRAG_ENTERED o ACTION_DRAG_LOCATION. Questo indica all'utente che View non è più un target di rilascio imminente.

Rispondere a un calo

Quando l'utente rilascia l'ombra di trascinamento su View e in precedenza l'View indica che può accettare i contenuti trascinati, il sistema invia un evento di trascinamento a View con il tipo di azione ACTION_DROP.

Il listener di eventi di trascinamento deve:

  1. Chiama getClipData() per ottenere l'oggetto ClipData originariamente fornito nella chiamata a startDragAndDrop() ed elaborare i dati. Se l'operazione di trascinamento non rappresenta lo spostamento dei dati, non è necessario.

  2. Restituisci il valore booleano true per indicare che il calo è stato elaborato correttamente, oppure false in caso contrario. Il valore restituito diventa il valore restituito da getResult() per l'eventuale evento ACTION_DRAG_ENDED. Se il sistema non invia un evento ACTION_DROP, il valore restituito da getResult() per un evento ACTION_DRAG_ENDED è false.

Per un evento ACTION_DROP, getX() e getY() usano il sistema di coordinate del View che riceve il calo per restituire la posizione X e Y del punto di contatto al momento del lancio.

Il sistema consente all'utente di rilasciare l'ombra di trascinamento su un elemento View il cui listener di eventi di trascinamento non riceve eventi di trascinamento. Consente inoltre all'utente di rilasciare l'ombreggiatura su regioni vuote dell'interfaccia utente dell'applicazione o su aree esterne all'applicazione. In tutti questi casi, il sistema non invia un evento con il tipo di azione ACTION_DROP, anche se invia un evento ACTION_DRAG_ENDED.

Rispondere a una fine del trascinamento

Subito dopo che l'utente rilascia l'ombra di trascinamento, il sistema invia un evento di trascinamento con un tipo di azione ACTION_DRAG_ENDED a tutti i listener di eventi di trascinamento nell'applicazione. Questo indica che l'operazione di trascinamento è terminata.

Ogni listener di eventi di trascinamento deve:

  1. Se il listener modifica l'aspetto del relativo oggetto View durante l'operazione, deve reimpostare l'aspetto predefinito di View. Si tratta di un'indicazione visiva per l'utente che l'operazione è terminata.
  2. Il listener può facoltativamente chiamare getResult() per scoprire di più sull'operazione. Se un listener restituisce true in risposta a un evento di tipo azione ACTION_DROP, getResult() restituisce l'operatore booleano true. In tutti gli altri casi, getResult() restituisce false booleano, anche quando il sistema non invia un evento ACTION_DROP.
  3. Per indicare il completamento corretto dell'operazione di trascinamento, l'ascoltatore deve restituire true booleano al sistema.

Rispondere agli eventi di trascinamento: esempio

Tutti gli eventi di trascinamento vengono ricevuti dal metodo degli eventi di trascinamento o dal listener. Il seguente snippet di codice è un semplice esempio di risposta agli eventi di trascinamento:

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;

});

Trascina in modalità multi-finestra

I dispositivi con Android 7.0 (livello API 24) o versioni successive supportano la modalità multi-finestra, che consente agli utenti di spostare i dati da un'app all'altra tramite un'operazione di trascinamento. Per ulteriori informazioni, consulta Supporto multi-finestra.

L'app di origine, da cui inizia l'operazione di trascinamento, fornisce i dati. L'app di destinazione, dove termina l'operazione di trascinamento, riceve i dati.

All'avvio dell'operazione di trascinamento, l'app di origine deve impostare il flag DRAG_FLAG_GLOBAL per indicare che l'utente può trascinare i dati in un'altra app.

Poiché i dati si spostano oltre i confini delle app, queste condividono l'accesso ai dati tramite un URI dei contenuti. Ciò richiede quanto segue:

  • L'app di origine deve impostare uno o entrambi i flag DRAG_FLAG_GLOBAL_URI_READ e DRAG_FLAG_GLOBAL_URI_WRITE, a seconda dell'accesso in lettura o scrittura ai dati che l'app di origine vuole concedere all'app di destinazione.
  • L'app di destinazione deve chiamare requestDragAndDropPermissions() immediatamente prima di gestire i dati che l'utente trascina nell'app. Se l'app di destinazione non ha più bisogno di accedere ai dati di trascinamento, l'app può chiamare release() sull'oggetto restituito da requestDragAndDropPermissions(). In caso contrario, le autorizzazioni vengono rilasciate quando l'attività che lo contiene viene eliminata. Se l'implementazione prevede l'avvio di una nuova attività per elaborare gli URI eliminati, dovrai concedere le stesse autorizzazioni alla nuova attività. Devi impostare i dati del clip e un flag:

    Kotlin

    intent.setClipData(clipData)
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    

    Java

    intent.setClipData(clipData);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    

I seguenti snippet di codice mostrano come rilasciare l'accesso di sola lettura per trascinare i dati subito dopo l'esecuzione dell'operazione di trascinamento. Per un esempio più completo, vedi l'esempio di DragonAndDrop su GitHub.

Attività di trascinamento dell'origine

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();

Attività di trascinamento della selezione

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 per il trascinamento semplificato

La classe DropHelper semplifica l'implementazione delle funzionalità di trascinamento. Membro della libreria Jetpack DragAndDrop, DropHelper fornisce la compatibilità con le versioni precedenti fino al livello API 24.

Usa DropHelper per specificare i target di rilascio, personalizzare l'evidenziazione dei target di rilascio e definire in che modo vengono gestiti i dati eliminati.

Specifica i target di rilascio

DropHelper.configureView() è un metodo statico e sovraccarico che ti consente di specificare i target di rilascio. I suoi parametri includono:

Ad esempio, per creare un target di rilascio che accetta immagini, utilizza una delle seguenti chiamate al metodo:

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);

Nella seconda chiamata vengono omesse le opzioni di configurazione della destinazione di rilascio. In questo caso il colore di evidenziazione della destinazione di rilascio è impostato sul colore secondario (o intenso) del tema, il raggio dell'angolo di evidenziazione è impostato su 16 dp e l'elenco di componenti EditText è vuoto. Per informazioni dettagliate, consulta la sezione seguente.

Configura le destinazioni di rilascio

La classe interna DropHelper.Options consente di configurare i target di rilascio. Fornisci un'istanza della classe al metodo DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener. Per ulteriori informazioni, consulta la sezione precedente.

Personalizza l'evidenziazione del target di rilascio

DropHelper configura i target dei rilasci in modo da visualizzare un'evidenziazione quando gli utenti trascinano i contenuti sui target. DropHelper offre uno stile predefinito e DropHelper.Options ti consente di impostare il colore dell'evidenziazione e specificare il raggio angolare del rettangolo dell'evidenziazione.

Utilizza la classe DropHelper.Options.Builder per creare un'istanza DropHelper.Options e impostare le opzioni di configurazione, come mostrato nell'esempio seguente:

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();

Gestire i componenti EditText nelle destinazioni degli elementi di rilascio

DropHelper controlla anche lo stato attivo all'interno della destinazione del rilascio quando il target contiene campi di testo modificabili.

Gli obiettivi di rilascio possono essere una singola visualizzazione o una gerarchia di viste. Se la gerarchia delle viste della destinazione di rilascio contiene uno o più componenti EditText, fornisci un elenco dei componenti per DropHelper.Options.Builder.addInnerEditTexts(EditText...) per assicurarti che l'evidenziazione della destinazione di rilascio e la gestione dei dati di testo funzionino correttamente.

DropHelper impedisce ai componenti EditText all'interno della gerarchia di viste della destinazione di rilascio di sottrarre lo stato attivo alla vista contenitore durante le interazioni di trascinamento.

Inoltre, se il trascinamento ClipData include dati di testo e URI, DropHelper seleziona uno dei EditText componenti nella destinazione del rilascio per gestire i dati di testo. La selezione si basa sul seguente ordine di precedenza:

  1. Il EditText in cui viene rilasciato ClipData.
  2. L'elemento EditText che contiene il cursore di testo (accento circonflesso).
  3. Il primo EditText fornito alla chiamata a DropHelper.Options.Builder.addInnerEditTexts(EditText...).

Per impostare un EditText come gestore dei dati di testo predefinito, passa EditText come primo argomento della chiamata a DropHelper.Options.Builder.addInnerEditTexts(EditText...). Ad esempio, se la destinazione del lancio gestisce le immagini ma contiene i campi di testo modificabili T1, T2 e T3, imposta T2 come valore predefinito nel seguente modo:

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();

Gestire i dati nelle destinazioni di rilascio

Il metodo DropHelper.configureView() accetta un OnReceiveContentListener creato da te per gestire il trascinamento ClipData. I dati di trascinamento vengono forniti al listener in un oggetto ContentInfoCompat. Nell'oggetto sono presenti dati di testo. I contenuti multimediali, come le immagini, sono rappresentati da URI.

OnReceiveContentListener gestisce anche i dati forniti alla destinazione di rilascio dalle interazioni degli utenti diverse dal trascinamento, ad esempio copia e incolla, quando DropHelper.configureView() viene utilizzato per configurare i seguenti tipi di viste:

  • Tutte le visualizzazioni, se l'utente utilizza Android 12 o versioni successive.
  • AppCompatEditText, se l'utente ha una versione di Android con versioni precedenti ad Android 7.0.

Tipi MIME, autorizzazioni e convalida dei contenuti

Il controllo del tipo MIME effettuato da DropHelper si basa sul trascinamento ClipDescription, creato dall'app che fornisce i dati di trascinamento. Convalida ClipDescription per assicurarti che i tipi MIME siano impostati correttamente.

DropHelper richiede tutte le autorizzazioni di accesso per gli URI dei contenuti contenuti nel trascinamento di ClipData. Per maggiori informazioni, consulta DragAndDropPermissions. Le autorizzazioni consentono di risolvere gli URI dei contenuti durante l'elaborazione dei dati di trascinamento.

DropHelper non convalida i dati restituiti dai fornitori di contenuti durante la risoluzione degli URI nei dati eliminati. Controlla se è presente un valore nullo e verifica la correttezza dei dati risolti.