Implémenter le glisser-déposer avec les vues

Vous pouvez implémenter le processus de glisser-déposer dans les vues en répondant à des événements pouvant déclencher un démarrage de glisser-déposer, puis en répondant et en consommant des événements de dépôt.

Commencer un déplacement

L'utilisateur commence un déplacement par un geste, généralement en appuyant ou en cliquant de manière prolongée sur un élément qu'il souhaite faire glisser.

Pour gérer cela dans un View, créez un objet ClipData et un objet ClipData.Item pour les données à déplacer. 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. We use this imageView object
        // to create the default builder.
        val myShadow = View.DragShadowBuilder(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. We use this imageView object
    // to create the default builder.
    View.DragShadowBuilder myShadow = new View.DragShadowBuilder(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;
});

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 glisser-déposer, 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 déposer le contenu.
  • 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.

Bien que l'utilisateur puisse 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, des régions vides de l'interface utilisateur de votre application ou même des zones en dehors de votre application, Android n'enverra pas d'événement avec le type d'action ACTION_DROP et n'enverra qu'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 indique que l'opération de déplacement est terminée.

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

  1. Si l'écouteur modifie son apparence au cours de l'opération, il doit rétablir son apparence par défaut pour indiquer visuellement à 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 la réussite de l'opération de suppression, l'écouteur doit renvoyer la valeur booléenne true au système. Si vous ne renvoyez pas false, un repère visuel montrant l'ombre projetée qui revient à sa source peut suggérer à l'utilisateur que l'opération a échoué.

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

});

Personnaliser une ombre de déplacement

Vous pouvez définir un myDragShadowBuilder personnalisé 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);
    }
}