Activer le glisser-déposer

Le framework de glisser-déposer Android vous permet d'ajouter des fonctionnalités interactives de glisser-déposer à votre application. Il permet aux utilisateurs de copier ou de déplacer du texte, des images, des objets et tout contenu pouvant être représenté par un URI, d'une View à une autre au sein d'une application, ou entre des applications en mode multifenêtre.

Chaîne de texte et image glissées-déposées dans une application. Chaîne de texte et image glissée-déposée d'une application à une autre en mode Écran partagé.
Figure 1 : Glisser-déposer dans une application
Figure 2 : Glisser-déposer entre les applications

Le framework comprend une classe d'événement de déplacement, des écouteurs de déplacement, ainsi que des classes et des méthodes d'assistance. Bien qu'il soit principalement conçu pour permettre le transfert de données, vous pouvez utiliser le framework pour d'autres actions de l'interface utilisateur. Par exemple, vous pouvez créer une application qui mélange des couleurs lorsque l'utilisateur fait glisser une icône de couleur sur une autre. Cependant, le reste du document décrit le framework de glisser-déposer dans le contexte du transfert de données.

Présentation

Une opération de glisser-déposer commence lorsque l'utilisateur effectue un geste d'interface utilisateur que votre application reconnaît comme un signal pour commencer à faire glisser des données. En réponse, l'application informe le système qu'une opération de glisser-déposer est en cours. Le système rappelle votre application pour obtenir une représentation des données en cours de déplacement, appelée ombre de déplacement.

Lorsque l'utilisateur déplace l'ombre de déplacement sur la mise en page de l'application, le système envoie des événements de déplacement aux écouteurs d'événements de déplacement et aux méthodes de rappel associées aux objets View de la mise en page. Si l'utilisateur libère l'ombre de déplacement sur une vue qui peut accepter les données (une cible de dépôt), le système envoie les données à la cible. L'opération de glisser-déposer se termine lorsque l'utilisateur libère l'ombre de déplacement, que celle-ci se trouve ou non au-dessus d'une cible de dépôt.

Créez un écouteur d'événements de déplacement en implémentant View.OnDragListener. Définissez l'écouteur d'une cible de lancement avec la méthode setOnDragListener() de l'objet View. Chaque vue de la mise en page possède également une méthode de rappel onDragEvent().

Votre application informe le système de démarrer une opération de glisser-déposer en appelant la méthode startDragAndDrop(), qui indique au système d'envoyer des événements de déplacement. Cette méthode fournit également au système les données que l'utilisateur fait glisser et des métadonnées décrivant les données. Vous pouvez appeler startDragAndDrop() sur n'importe quel View de la mise en page actuelle. Le système n'utilise l'objet View que pour accéder aux paramètres généraux de la mise en page.

Lors de l'opération de glisser-déposer, le système envoie des événements de déplacement aux écouteurs d'événements de déplacement ou aux méthodes de rappel des objets View de la mise en page. Les écouteurs ou les méthodes de rappel utilisent les métadonnées pour décider d'accepter ou non les données lorsqu'elles sont supprimées. Si l'utilisateur dépose les données sur une cible de dépôt (un View qui accepte les données), le système envoie un objet d'événement de déplacement contenant les données à l'écouteur d'événements de déplacement ou à la méthode de rappel de la cible de dépôt.

Faire glisser des écouteurs d'événements et des méthodes de rappel

Un View reçoit des événements de déplacement avec un écouteur d'événements de déplacement qui implémente View.OnDragListener ou avec la méthode de rappel onDragEvent() de la vue. Lorsque le système appelle la méthode ou l'écouteur, il fournit un argument DragEvent.

Dans la plupart des cas, il est préférable d'utiliser un écouteur plutôt que la méthode de rappel. Lorsque vous concevez des interfaces utilisateur, vous ne sous-classez généralement pas les classes View, mais l'utilisation de la méthode de rappel vous oblige à créer des sous-classes pour remplacer la méthode. En comparaison, vous pouvez implémenter une classe d'écouteur, puis l'utiliser avec plusieurs objets View différents. Vous pouvez également l'implémenter en tant que classe intégrée anonyme ou expression lambda. Pour définir l'écouteur pour un objet View, appelez setOnDragListener().

Vous pouvez également modifier l'implémentation par défaut de onDragEvent() sans remplacer la méthode. Définissez un OnReceiveContentListener sur une vue. Pour en savoir plus, consultez setOnReceiveContentListener(). La méthode onDragEvent() effectue ensuite les opérations suivantes par défaut:

  • Renvoie la valeur "true" en réponse à l'appel de startDragAndDrop().
  • Elle appelle performReceiveContent() si les données de glisser-déposer sont déposées sur la vue. Les données sont transmises à la méthode en tant qu'objet ContentInfo. La méthode appelle OnReceiveContentListener.

  • Renvoie la valeur "true" si les données de glisser-déposer sont déposées sur la vue et que OnReceiveContentListener consomme l'un des contenus.

Définissez OnReceiveContentListener pour gérer les données spécifiquement pour votre application. Pour assurer la rétrocompatibilité jusqu'au niveau d'API 24, utilisez la version Jetpack de OnReceiveContentListener.

Vous pouvez utiliser un écouteur d'événements de déplacement et une méthode de rappel pour un objet View. Dans ce cas, le système appelle d'abord l'écouteur. Le système n'appelle pas la méthode de rappel, sauf si l'écouteur renvoie false.

La combinaison de la méthode onDragEvent() et de View.OnDragListener est analogue à celle des onTouchEvent() et View.OnTouchListener utilisés avec les événements tactiles.

Processus de glisser-déposer

Le processus de glisser-déposer comporte quatre étapes ou états: démarré, continu, déposé et terminé.

Date de lancement

En réponse au geste de glisser-déposer d'un utilisateur, votre application appelle startDragAndDrop() pour indiquer au système de lancer une opération de glisser-déposer. Les arguments de la méthode fournissent les éléments suivants:

  • Données à faire glisser.
  • Rappel permettant de dessiner l'ombre de déplacement
  • Métadonnées décrivant les données déplacées : le système répond en rappelant votre application pour obtenir une ombre de déplacement. Le système affiche ensuite l'ombre du glissement sur l'appareil. Le système envoie ensuite un événement de déplacement avec le type d'action ACTION_DRAG_STARTED à l'écouteur d'événements de déplacement de tous les objets View de la mise en page actuelle. Pour continuer à recevoir des événements de déplacement, y compris un événement de déplacement potentiel, l'écouteur d'événements de déplacement doit renvoyer true. L'écouteur est alors enregistré auprès du système. Seuls les écouteurs enregistrés continuent de recevoir des événements de déplacement. À ce stade, les écouteurs peuvent également modifier l'apparence de leur objet "déposer" View pour indiquer que la vue peut accepter un événement de dépôt. Si l'écouteur d'événements de déplacement renvoie false, il ne reçoit aucun événement de déplacement pour l'opération en cours tant que le système n'a pas envoyé un événement de déplacement avec le type d'action ACTION_DRAG_ENDED. En renvoyant false, l'écouteur indique au système qu'il n'est pas intéressé par l'opération de glisser-déposer et qu'il ne veut pas accepter les données déplacées.
Poursuite de l'opération…
L'utilisateur poursuit son déplacement. Lorsque l'ombre de déplacement rencontre le cadre de délimitation d'une cible de dépôt, le système envoie un ou plusieurs événements de déplacement à l'écouteur d'événements de déplacement de la cible. L'écouteur peut modifier l'apparence de la cible de dépôt View en réponse à l'événement. Par exemple, si l'événement indique que l'ombre du déplacement entre dans le cadre de délimitation de la cible de dépôt (type d'action ACTION_DRAG_ENTERED), l'écouteur peut réagir en mettant en surbrillance View.
Déplacé
L'utilisateur libère l'ombre du déplacement dans le cadre de délimitation d'une cible de dépôt. Le système envoie à l'écouteur de la cible de dépôt un événement de déplacement avec le type d'action ACTION_DROP. L'objet d'événement de déplacement contient les données qui sont transmises au système dans l'appel à startDragAndDrop() qui lance l'opération. L'écouteur doit renvoyer la valeur booléenne true au système s'il traite correctement les données supprimées. Cette étape ne se produit que si l'utilisateur dépose l'ombre de déplacement dans la zone de délimitation d'un View dont l'écouteur est enregistré pour recevoir des événements de déplacement (une cible de dépôt). Si l'utilisateur lève l'ombre de déplacement dans toute autre situation, aucun événement de déplacement ACTION_DROP n'est envoyé.
Terminée

Une fois que l'utilisateur a libéré l'ombre du déplacement et après que le système a envoyé

un événement de déplacement de type ACTION_DROP. Si nécessaire, le système envoie un événement de déplacement de type ACTION_DRAG_ENDED pour indiquer que l'opération de glisser-déposer est terminée. Cela se fait quel que soit l'endroit où l'utilisateur libère l'ombre du déplacement. L'événement est envoyé à chaque écouteur enregistré pour recevoir des événements de déplacement, même s'il reçoit également l'événement ACTION_DROP.

Chacune de ces étapes est décrite plus en détail dans la section intitulée Une opération de glisser-déposer.

Événements de déplacement

Le système envoie un événement de déplacement sous la forme d'un objet DragEvent, qui contient un type d'action décrivant le processus de glisser-déposer. Selon le type d'action, l'objet peut également contenir d'autres données.

Les écouteurs d'événements de déplacement reçoivent l'objet DragEvent. Pour obtenir le type d'action, les écouteurs appellent DragEvent.getAction(). Six valeurs possibles sont définies par des constantes dans la classe DragEvent, décrites dans le tableau 1:

Tableau 1. Types d'actions DragEvent

Type d'action Signification
ACTION_DRAG_STARTED L'application appelle startDragAndDrop() et obtient une ombre de déplacement. Si l'écouteur souhaite continuer à recevoir des événements de déplacement pour cette opération, il doit renvoyer la valeur booléenne true au système.
ACTION_DRAG_ENTERED L'ombre de déplacement entre dans le cadre de délimitation du View de l'écouteur d'événements de déplacement. Il s'agit du premier type d'action d'événement que l'écouteur reçoit lorsque l'ombre du déplacement entre dans la zone de délimitation.
ACTION_DRAG_LOCATION Après un événement ACTION_DRAG_ENTERED, l'ombre de déplacement reste dans le cadre de délimitation du View de l'écouteur d'événements de déplacement.
ACTION_DRAG_EXITED Après un événement ACTION_DRAG_ENTERED et au moins un événement ACTION_DRAG_LOCATION, l'ombre de déplacement se déplace en dehors du cadre de délimitation du View de l'écouteur d'événements de déplacement.
ACTION_DROP L'ombre de déplacement se relâche au-dessus du View de l'écouteur d'événements de déplacement. Ce type d'action n'est envoyé à l'écouteur d'un objet View que s'il renvoie la valeur booléenne true en réponse à l'événement de déplacement ACTION_DRAG_STARTED. Ce type d'action n'est pas envoyé si l'utilisateur libère l'ombre de déplacement sur un View dont l'écouteur n'est pas enregistré ou s'il libère l'ombre de déplacement sur tout élément qui ne fait pas partie de la mise en page actuelle.

L'écouteur renvoie la valeur booléenne true s'il traite correctement la suppression. Sinon, elle doit renvoyer false.

ACTION_DRAG_ENDED Le système met fin à l'opération de glisser-déposer. Ce type d'action n'est pas nécessairement précédé d'un événement ACTION_DROP. Si le système envoie un ACTION_DROP, la réception du type d'action ACTION_DRAG_ENDED n'implique pas que la suppression a réussi. L'écouteur doit appeler getResult(), comme indiqué dans le tableau 2, pour obtenir la valeur renvoyée en réponse à ACTION_DROP. Si aucun événement ACTION_DROP n'est envoyé, getResult() renvoie false.

L'objet DragEvent contient également les données et les métadonnées fournies par votre application au système dans l'appel de startDragAndDrop(). Certaines données ne sont valides que pour certains types d'actions, comme résumé dans le tableau 2. Pour en savoir plus sur les événements et les données associées, consultez la section Opération de glisser-déposer.

Tableau 2. Données DragEvent valides par type d'action

Valeur de getAction()
Valeur de getClipDescription()
Valeur de getLocalState()
Valeur de getX()
Valeur de getY()
Valeur de getClipData()
Valeur de getResult()
ACTION_DRAG_STARTED &coche; &coche; &coche; &coche;    
ACTION_DRAG_ENTERED &coche; &coche;        
ACTION_DRAG_LOCATION &coche; &coche; &coche; &coche;    
ACTION_DRAG_EXITED &coche; &coche;        
ACTION_DROP &coche; &coche; &coche; &coche; &coche;  
ACTION_DRAG_ENDED   &coche;       &coche;

Les méthodes DragEvent getAction(), describeContents(), writeToParcel() et toString() renvoient toujours des données valides.

Si une méthode ne contient pas de données valides pour un type d'action particulier, elle renvoie null ou 0, en fonction de son type de résultat.

Ombre de déplacement

Lors d'une opération de glisser-déposer, le système affiche une image que l'utilisateur fait glisser. Pour les mouvements de données, cette image représente les données en cours de déplacement. Pour les autres opérations, l'image représente un aspect de l'opération de déplacement.

Il s'agit d'une ombre du déplacement. Vous le créez à l'aide de méthodes que vous déclarez pour un objet View.DragShadowBuilder. Vous transmettez le compilateur au système lorsque vous lancez une opération de glisser-déposer à l'aide de startDragAndDrop(). Dans le cadre de sa réponse à startDragAndDrop(), le système appelle les méthodes de rappel que vous définissez dans View.DragShadowBuilder pour obtenir une ombre de déplacement.

La classe View.DragShadowBuilder comporte deux constructeurs:

View.DragShadowBuilder(View)

Ce constructeur accepte tous les objets View de votre application. Le constructeur stocke l'objet View dans l'objet View.DragShadowBuilder afin que les rappels puissent y accéder pour construire l'ombre de déplacement. La vue ne doit pas nécessairement être un View sélectionné par l'utilisateur pour lancer l'opération de déplacement.

Si vous utilisez ce constructeur, vous n'avez pas besoin d'étendre View.DragShadowBuilder ni de remplacer ses méthodes. Par défaut, vous obtenez une ombre de déplacement qui a la même apparence que l'élément View que vous transmettez en tant qu'argument, centrée sous l'endroit où l'utilisateur touche l'écran.

View.DragShadowBuilder()

Si vous utilisez ce constructeur, aucun objet View n'est disponible dans l'objet View.DragShadowBuilder. Le champ est défini sur null. Vous devez étendre View.DragShadowBuilder et remplacer ses méthodes, sinon vous obtiendrez une ombre de déplacement invisible. Le système ne génère pas d'erreur.

La classe View.DragShadowBuilder comporte deux méthodes qui créent ensemble l'ombre du déplacement:

onProvideShadowMetrics()

Le système appelle cette méthode immédiatement après avoir appelé startDragAndDrop(). Utilisez cette méthode pour envoyer les dimensions et le point de contact de l'ombre de déplacement au système. Cette méthode comporte deux paramètres:

outShadowSize:objet Point. La largeur de l'ombre de déplacement correspond à x, et sa hauteur à y.

outShadowTouchPoint:objet Point. Le point de contact est l'emplacement dans l'ombre du glissement qui doit se trouver sous le doigt de l'utilisateur pendant le déplacement. Sa position X correspond à x et sa position Y à y.

onDrawShadow()

Immédiatement après l'appel de onProvideShadowMetrics(), le système appelle onDrawShadow() pour créer l'ombre du déplacement. La méthode comporte un seul argument, un objet Canvas que le système construit à partir des paramètres que vous fournissez dans onProvideShadowMetrics(). La méthode dessine l'ombre du déplacement sur le Canvas fourni.

Pour améliorer les performances, réduisez la taille de l'ombre du déplacement. Pour un seul élément, vous pouvez utiliser une icône. Pour une sélection de plusieurs éléments, vous pouvez utiliser des icônes dans une pile plutôt que des images complètes réparties sur l'écran.

Une opération de glisser-déposer

Cette section explique, étape par étape, comment lancer un glisser-déposer, répondre aux événements pendant le déplacement, répondre à un événement de dépôt et mettre fin à l'opération de glisser-déposer.

Commencer un déplacement

L'utilisateur commence à faire glisser un élément (généralement un appui prolongé) sur un objet View. En réponse, votre application doit:

  1. Créez un objet ClipData et un objet ClipData.Item pour les données en cours de déplacement. Dans le cadre de ClipData, fournissez les métadonnées stockées dans un objet ClipDescription au sein de ClipData. Pour une opération de glisser-déposer qui ne représente pas un déplacement de données, vous pouvez utiliser null au lieu d'un objet réel.

    Par exemple, cet extrait de code montre comment répondre à un geste d'appui de manière prolongée sur un ImageView en créant un objet ClipData contenant la balise (ou le libellé) d'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. Définissez myDragShadowBuilder en remplaçant les méthodes dans View.DragShadowBuilder. L'extrait de code suivant crée une petite ombre de déplacement grise rectangulaire pour un 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);
    }
    }
    

Répondre à un début de déplacement

Pendant l'opération de déplacement, le système envoie des événements de déplacement aux écouteurs d'événements de déplacement des objets View dans la mise en page actuelle. Les écouteurs réagissent en appelant DragEvent.getAction() pour obtenir le type d'action. Au début d'un déplacement, cette méthode renvoie ACTION_DRAG_STARTED.

En réponse à un événement de type d'action ACTION_DRAG_STARTED, un écouteur d'événements de déplacement doit:

  1. Appelez DragEvent.getClipDescription() et utilisez les méthodes de type MIME dans le ClipDescription renvoyé pour voir si l'écouteur peut accepter les données en cours de déplacement.

    Si l'opération de glisser-déposer ne représente pas un déplacement de données, cela peut être inutile.

  2. Si l'écouteur d'événements de déplacement peut accepter un déplacement, il doit renvoyer true pour indiquer au système de continuer à envoyer des événements de déplacement à l'écouteur. Si l'écouteur ne peut pas accepter de déplacement, il doit renvoyer false. Le système cesse alors d'envoyer des événements de déplacement à l'écouteur jusqu'à ce qu'il envoie ACTION_DRAG_ENDED pour terminer l'opération de glisser-déposer.

Pour un événement ACTION_DRAG_STARTED, les méthodes DragEvent suivantes ne sont pas valides: getClipData(), getX(), getY() et getResult().

Gérer les événements pendant le déplacement

Pendant l'action de déplacement, les écouteurs d'événements de déplacement qui renvoient true en réponse à l'événement de déplacement ACTION_DRAG_STARTED continuent à recevoir des événements de déplacement. Les types d'événements de déplacement qu'un écouteur reçoit pendant le déplacement dépendent de l'emplacement de l'ombre de déplacement et de la visibilité du View de l'écouteur. Les écouteurs utilisent principalement les événements de déplacement pour décider s'ils doivent modifier l'apparence de leur View.

Pendant l'action de déplacement, DragEvent.getAction() renvoie l'une des trois valeurs suivantes:

  • ACTION_DRAG_ENTERED : l'écouteur reçoit ce type d'action d'événement lorsque le point de contact (le point sur l'écran situé sous le doigt ou la souris de l'utilisateur) entre dans le cadre de délimitation du View de l'écouteur.
  • ACTION_DRAG_LOCATION : une fois que l'écouteur reçoit un événement ACTION_DRAG_ENTERED, il reçoit un nouvel événement ACTION_DRAG_LOCATION chaque fois que le point de contact se déplace jusqu'à ce qu'il reçoive un événement ACTION_DRAG_EXITED. Les méthodes getX() et getY() renvoient les coordonnées X et Y du point de contact.
  • ACTION_DRAG_EXITED : ce type d'action d'événement est envoyé à un écouteur qui reçoit précédemment ACTION_DRAG_ENTERED. L'événement est envoyé lorsque le point de contact de l'ombre de déplacement se déplace de l'intérieur du cadre de délimitation du View de l'écouteur vers l'extérieur du cadre de délimitation.

L'écouteur d'événements de déplacement n'a pas besoin de réagir à ces types d'actions. Si l'écouteur renvoie une valeur au système, elle est ignorée.

Voici quelques consignes pour répondre à chacun de ces types d'actions:

  • En réponse à ACTION_DRAG_ENTERED ou ACTION_DRAG_LOCATION, l'écouteur peut modifier l'apparence de View pour indiquer que la vue est une cible de suppression potentielle.
  • Un événement de type d'action ACTION_DRAG_LOCATION contient des données valides pour getX() et getY() correspondant à l'emplacement du point de contact. L'écouteur peut utiliser ces informations pour modifier l'apparence de View au niveau du point de contact ou pour déterminer la position exacte où l'utilisateur peut libérer l'ombre de déplacement, c'est-à-dire déposer les données.
  • En réponse à ACTION_DRAG_EXITED, l'écouteur doit réinitialiser toutes les modifications d'apparence qu'il applique en réponse à ACTION_DRAG_ENTERED ou ACTION_DRAG_LOCATION. Cela indique à l'utilisateur que View n'est plus une cible de dépôt imminente.

Réagir à une baisse

Lorsque l'utilisateur libère l'ombre de déplacement sur un View et que l'View indique précédemment qu'il peut accepter le contenu en cours de déplacement, le système envoie un événement de déplacement à View avec le type d'action ACTION_DROP.

L'écouteur d'événements de déplacement doit procéder comme suit:

  1. Appelez getClipData() pour obtenir l'objet ClipData initialement fourni dans l'appel de startDragAndDrop() et traiter les données. Si l'opération de glisser-déposer ne représente pas le déplacement de données, cela n'est pas nécessaire.

  2. Renvoyez la valeur booléenne true pour indiquer que la suppression a bien été traitée, ou false dans le cas contraire. La valeur renvoyée devient la valeur renvoyée par getResult() pour l'événement ACTION_DRAG_ENDED éventuel. Si le système n'envoie pas d'événement ACTION_DROP, la valeur renvoyée par getResult() pour un événement ACTION_DRAG_ENDED est false.

Pour un événement ACTION_DROP, getX() et getY() utilisent le système de coordonnées de la View qui reçoit la suppression pour renvoyer les positions X et Y du point de contact au moment de la baisse.

Le système permet à l'utilisateur de libérer l'ombre de déplacement sur un View dont l'écouteur d'événements de déplacement ne reçoit pas d'événements de déplacement. Elle permet également à l'utilisateur de libérer l'ombre de déplacement sur des zones vides de l'interface utilisateur de l'application ou sur des zones en dehors de votre application. Dans tous ces cas, le système n'envoie pas d'événement avec le type d'action ACTION_DROP, bien qu'il envoie un événement ACTION_DRAG_ENDED.

Répondre à une fin de déplacement

Immédiatement après que l'utilisateur a libéré l'ombre de déplacement, le système envoie un événement de déplacement avec un type d'action ACTION_DRAG_ENDED à tous les écouteurs d'événements de déplacement de votre application. Cela signifie que l'opération de glisser-déposer est terminée.

Chaque écouteur d'événements de déplacement doit procéder comme suit:

  1. Si l'écouteur modifie l'apparence de son objet View au cours de l'opération, il doit rétablir l'apparence par défaut de View. Il s'agit d'une indication visuelle pour l'utilisateur que l'opération est terminée.
  2. L'écouteur peut éventuellement appeler getResult() pour en savoir plus sur l'opération. Si un écouteur renvoie true en réponse à un événement de type d'action ACTION_DROP, getResult() renvoie la valeur booléenne true. Dans tous les autres cas, getResult() renvoie une valeur booléenne false, y compris lorsque le système n'envoie pas d'événement ACTION_DROP.
  3. Pour indiquer que l'opération de glisser-déposer a bien été effectuée, l'écouteur doit renvoyer la valeur booléenne true au système.

Répondre aux événements de déplacement: exemple

Tous les événements de déplacement sont reçus par votre méthode ou écouteur d'événements de déplacement. L'extrait de code suivant est un exemple simple de réponse à des événements de déplacement:

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;

});

Glisser-déposer en mode multifenêtre

Les appareils équipés d'Android 7.0 (niveau d'API 24) ou version ultérieure sont compatibles avec le mode multifenêtre, qui permet aux utilisateurs de déplacer des données d'une application à une autre par glisser-déposer. Pour en savoir plus, consultez la section Compatibilité avec le mode multifenêtre.

L'application source, dans laquelle commence l'opération de glisser-déposer, fournit les données. L'application cible, où se termine l'opération de glisser-déposer, reçoit les données.

Lors du lancement de l'opération de glisser-déposer, l'application source doit définir l'indicateur DRAG_FLAG_GLOBAL pour indiquer que l'utilisateur peut faire glisser des données vers une autre application.

Étant donné que les données sont déplacées au-delà des limites de l'application, celles-ci partagent l'accès aux données à l'aide d'un URI de contenu. Pour cela, vous devez disposer des éléments suivants:

  • L'application source doit définir l'un des indicateurs DRAG_FLAG_GLOBAL_URI_READ et DRAG_FLAG_GLOBAL_URI_WRITE, ou les deux, en fonction de l'accès en lecture ou en écriture aux données que l'application source souhaite accorder à l'application cible.
  • L'application cible doit appeler requestDragAndDropPermissions() immédiatement avant de traiter les données que l'utilisateur fait glisser dans l'application. Si l'application cible n'a plus besoin d'accéder aux données par glisser-déposer, elle peut appeler release() sur l'objet renvoyé par requestDragAndDropPermissions(). Sinon, les autorisations sont libérées lorsque l'activité associée est détruite. Si votre implémentation implique le démarrage d'une nouvelle activité pour traiter les URI supprimés, vous devez lui accorder les mêmes autorisations. Vous devez définir les données des extraits et un indicateur:

    Kotlin

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

    Java

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

Les extraits de code suivants montrent comment libérer l'accès en lecture seule pour glisser-déposer des données immédiatement après l'opération de glisser-déposer. Consultez l'exemple DragAndDrop sur GitHub pour obtenir un exemple plus complet.

Activité de glisser-déposer source

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

Cibler l'activité de glisser-déposer

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 pour un glisser-déposer simplifié

La classe DropHelper simplifie l'implémentation des fonctionnalités de glisser-déposer. Membre de la bibliothèque Jetpack DragAndDrop, DropHelper offre une rétrocompatibilité jusqu'au niveau d'API 24.

Utilisez DropHelper pour spécifier des cibles de dépôt, personnaliser leur mise en surbrillance et définir la manière dont les données supprimées sont traitées.

Spécifier des cibles de dépôt

DropHelper.configureView() est une méthode statique surchargée qui vous permet de spécifier des cibles de dépôt. Ses paramètres incluent les suivants:

Par exemple, pour créer une cible de dépôt qui accepte les images, utilisez l'un des appels de méthode suivants:

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

Le deuxième appel omet les options de configuration de la cible de dépôt. Dans ce cas, la couleur de mise en surbrillance de la cible de chute est définie sur la couleur secondaire (ou d'accentuation) du thème, le rayon de l'angle de mise en surbrillance est défini sur 16 dp et la liste des composants EditText est vide. Pour en savoir plus, consultez la section suivante.

Configurer les cibles de dépôt

La classe interne DropHelper.Options vous permet de configurer des cibles de dépôt. Fournissez une instance de la classe à la méthode DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener). Pour en savoir plus, consultez la section précédente.

Personnaliser la mise en surbrillance des cibles de dépôt

DropHelper configure les cibles de dépôt pour afficher une mise en surbrillance lorsque les utilisateurs font glisser du contenu sur les cibles. DropHelper fournit un style par défaut, et DropHelper.Options vous permet de définir la couleur de la mise en surbrillance et de spécifier l'arrondi d'angle de ce rectangle.

Utilisez la classe DropHelper.Options.Builder pour créer une instance DropHelper.Options et définir les options de configuration, comme illustré dans l'exemple suivant:

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

Gérer les composants EditText dans les cibles de dépôt

DropHelper contrôle également le ciblage dans la cible de dépôt lorsque celle-ci contient des champs de texte modifiables.

Les cibles de suppression peuvent être une vue unique ou une hiérarchie de vues. Si la hiérarchie des vues des cibles de dépôt contient un ou plusieurs composants EditText, fournissez une liste des composants à DropHelper.Options.Builder.addInnerEditTexts(EditText...) pour vous assurer que la mise en surbrillance de la cible de dépôt et la gestion des données textuelles fonctionnent correctement.

DropHelper empêche les composants EditText de la hiérarchie des vues des cibles de dépôt de prendre le focus de la vue parent lors des interactions de déplacement.

De plus, si le glisser-déposer ClipData inclut du texte et des données d'URI, DropHelper sélectionne l'un des composants EditText de la cible de dépôt pour gérer les données textuelles. La sélection est basée sur l'ordre de priorité suivant:

  1. Le EditText sur lequel le ClipData est supprimé.
  2. EditText contenant le curseur de texte (signe d'insertion).
  3. Le premier EditText fourni à l'appel de DropHelper.Options.Builder.addInnerEditTexts(EditText...).

Pour définir EditText comme gestionnaire de données textuelles par défaut, transmettez EditText comme premier argument de l'appel à DropHelper.Options.Builder.addInnerEditTexts(EditText...). Par exemple, si votre cible de dépôt gère les images, mais contient les champs de texte modifiables T1, T2 et T3, définissez T2 comme valeur par défaut:

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

Gérer les données dans les cibles de dépôt

La méthode DropHelper.configureView() accepte un OnReceiveContentListener que vous créez pour gérer le ClipData de glisser-déposer. Les données de glisser-déposer sont fournies à l'écouteur dans un objet ContentInfoCompat. L'objet contient des données textuelles. Les contenus multimédias, tels que les images, sont représentés par des URI.

OnReceiveContentListener gère également les données fournies à la cible de dépôt par les interactions utilisateur autres que le glisser-déposer (comme le copier-coller) lorsque DropHelper.configureView() est utilisé pour configurer les types de vues suivants:

  • Toutes les vues, si l'utilisateur est équipé d'Android 12 ou version ultérieure
  • AppCompatEditText, si l'utilisateur exécute une version d'Android antérieure à Android 7.0.

Types MIME, autorisations et validation du contenu

La vérification du type MIME par DropHelper est basée sur le glisser-déposer ClipDescription, créé par l'application qui fournit les données de glisser-déposer. Validez ClipDescription pour vous assurer que les types MIME sont correctement définis.

DropHelper demande toutes les autorisations d'accès pour les URI de contenu contenus dans l'élément ClipData par glisser-déposer. Pour en savoir plus, consultez DragAndDropPermissions. Les autorisations vous permettent de résoudre les URI de contenu lors du traitement des données par glisser-déposer.

DropHelper ne valide pas les données renvoyées par les fournisseurs de contenu lors de la résolution des URI dans les données supprimées. Recherchez la valeur nulle et vérifiez l'exactitude des données résolues.