Atelier de programmation sur le glisser-déposer

1. Avant de commencer

Dans cet atelier de programmation, découvrez des instructions pratiques sur les principes de base de l'implémentation du glisser-déposer pour les vues. Vous allez apprendre à mettre en œuvre le glisser-déposer pour les vues, aussi bien au sein de votre application que d'une application à une autre. Vous allez apprendre à implémenter des interactions de glisser-déposer au sein de votre application, mais aussi d'une application à une autre. Cet atelier de programmation vous apprendra à utiliser DropHelper pour permettre le glisser-déposer, à personnaliser le retour visuel lors du glisser-déposer avec ShadowBuilder, à ajouter des autorisations pour le glisser-déposer entre applications et à implémenter un récepteur de contenu fonctionnant de manière universelle.

Conditions préalables

Voici les conditions à remplir pour effectuer cet atelier de programmation :

Objectifs de l'atelier

Créer une application simple qui :

  • implémente la fonctionnalité de glisser-déposer à l'aide de DragStartHelper et DropHelper ;
  • modifie ShadowBuilder ;
  • ajoute l'autorisation de glisser-déposer entre différentes applications ;
  • implémente un récepteur de contenus enrichis pour une implémentation universelle.

Ce dont vous avez besoin

2. Événement de glisser-déposer

Un processus de glisser-déposer peut être vu comme un événement en quatre étapes :

  1. Démarrage : le système lance l'opération de glisser-déposer en réponse au geste de déplacement de l'utilisateur.
  2. Poursuite de l'opération : l'utilisateur continue le geste de déplacement et DragShadowBuilder intervient lorsque l'utilisateur arrive dans la vue cible.
  3. Fin : l'utilisateur relâche le déplacement dans le cadre de délimitation d'une cible de glisser-déposer, à savoir la zone cible de dépôt.
  4. Sortie : le système envoie un signal pour mettre fin à l'opération de glisser-déposer.

Le système envoie l'événement de déplacement dans l'objet DragEvent. L'objet DragEvent peut contenir les données suivantes :

  1. ActionType : valeur d'action de l'événement basée sur l'événement de cycle de vie du glisser-déposer. Par exemple, ACTION_DRAG_STARTED, ACTION_DROP, etc.
  2. ClipData : données faisant l'objet du déplacement, encapsulées dans un objet ClipData.
  3. ClipDescription : métadonnées concernant l'objet ClipData.
  4. Result : résultat de l'opération de glisser-déposer.
  5. X : coordonnée X de la position actuelle de l'objet déplacé.
  6. Y : coordonnée Y de la position actuelle de l'objet déplacé.

3. Configuration

Créez un projet et sélectionnez le modèle "Empty Views Activity" (Activité Vues vide) :

2fbd2bca1483033f.png

Conservez les valeurs par défaut de tous les paramètres. Laissez le projet se synchroniser et s'indexer. Vous verrez que le fichier MainActivity.kt a été créé, ainsi que la vue activity_main.xml.

4. Glisser-déposer à l'aide de vues

Ajoutons des valeurs de chaîne dans string.xml.

<resources>
    <string name="app_name">DragAndDropCodelab</string>
    <string name="drag_image">Drag Image</string>
    <string name="drop_image">drop image</string>
 </resources>

Ouvrez le fichier source activity_main.xml et modifiez la mise en page pour inclure deux éléments ImageViews qui serviront respectivement de source de déplacement et de cible de dépôt.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_greeting"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/iv_source"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_source"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="@string/drag_image"
        app:layout_constraintBottom_toTopOf="@id/iv_target"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_greeting" />

    <ImageView
        android:id="@+id/iv_target"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="@string/drop_image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iv_source" />
</androidx.constraintlayout.widget.ConstraintLayout>

Dans build.gradle.kts, activez la liaison de vue.

buildFeatures{
   viewBinding = true
}

Dans build.gradle.kts, ajoutez une dépendance pour Glide.

dependencies {
    implementation("com.github.bumptech.glide:glide:4.16.0")
    annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")

    //other dependencies
}

Ajoutez des URL d'images et un texte d'accueil dans string.xml.

<string name="greeting">Drag and Drop</string>
<string name="target_url">https://services.google.com/fh/files/misc/qq2.jpeg</string>
<string name="source_url">https://services.google.com/fh/files/misc/qq10.jpeg</string>

Dans MainActivity.kt, initialisez les vues.

class MainActivity : AppCompatActivity() {
   val binding by lazy(LazyThreadSafetyMode.NONE) {
       ActivityMainBinding.inflate(layoutInflater)
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(binding.root)
       binding.tvGreeting.text = getString(R.string.greeting)
       Glide.with(this).asBitmap()
           .load(getString(R.string.source_url))
           .into(binding.ivSource)
       Glide.with(this).asBitmap()
           .load(getString(R.string.target_url))
           .into(binding.ivTarget)
   }
}

À ce stade, votre application doit afficher un texte d'accueil et deux images en orientation verticale.

b0e651aaee336750.png

5. Rendre la vue déplaçable

Pour qu'une vue spécifique soit déplaçable, elle doit implémenter la méthode startDragAndDrop() lorsqu'un geste de déplacement est effectué.

Implémentons un rappel pour onLongClickListener lorsque l'utilisateur commence à déplacer la vue.

draggableView.setOnLongClickListener{ v ->
   //drag logic here
   true
}

Ce rappel rend la vue cliquable de manière prolongée, même si elle ne l'est pas à l'origine. La valeur renvoyée est un booléen. "True" signifie que le déplacement est consommé par le rappel.

Préparer ClipData : les données à déplacer

Définissons les données que nous souhaitons glisser-déposer. Les données peuvent être de n'importe quel type. Il peut s'agir aussi bien de texte simple que d'une vidéo. Ces données sont encapsulées dans l'objet ClipData. L'objet ClipData contient un ou plusieurs ClipItem complexes.

Ces éléments possèdent différents types MIME définis dans ClipDescription.

Nous faisons glisser l'URL de l'image de la vue source. ClipData se compose de trois éléments principaux :

  1. label : texte simple permettant d'indiquer à l'utilisateur ce qu'il fait glisser.
  2. mimeTypes : type MIME des éléments que l'utilisateur fait glisser.
  3. clipItem : élément à faire glisser, encapsulé dans un objet ClipData.Item.

Créons maintenant un objet ClipData.

val label = "Dragged Image Url"
val clipItem = ClipData.Item(v.tag as? CharSequence)
val mimeTypes = arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)
val draggedData = ClipData(
   label, mimeTypes, clipItem
)

Démarrer le glisser-déposer

Maintenant que les données sont prêtes à être déplacées, nous allons lancer le déplacement. Pour cela, nous utiliserons startDragAndDrop.

La méthode startDragAndDrop accepte quatre arguments :

  1. data : données que l'utilisateur fait glisser sous la forme d'un objet ClipData..
  2. shadowBuilder : DragShadowBuilder permettant de créer l'ombre de la vue déplacée.
  3. myLocalState : objet contenant des données locales concernant l'opération de glisser-déposer. Lors de la distribution d'événements de déplacement vers des vues de la même activité, cet objet sera disponible via DragEvent.getLocalState().
  4. Indicateurs : indicateurs permettant de contrôler les opérations de glisser-déposer.

Une fois cette fonction appelée, l'ombre de l'objet déplacé est créée en fonction de la classe View.DragShadowBuilder. Une fois que le système présente l'ombre de l'objet déplacé, l'opération de glisser-déposer démarre avec l'envoi de l'événement à la vue visible, qui a implémenté l'interface OnDragListener.

v.startDragAndDrop(
   draggedData,
   View.DragShadowBuilder(v),
   null,
   0
)

Nous avons donc configuré notre vue pour l'opération de déplacement et défini les données à déplacer. L'implémentation finale se présente comme suit :

fun setupDrag(draggableView: View) {
   draggableView.setOnLongClickListener { v ->
       val label = "Dragged Image Url"
       val clipItem = ClipData.Item(v.tag as? CharSequence)
       val mimeTypes = arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)
       val draggedData = ClipData(
           label, mimeTypes, clipItem
       )
       v.startDragAndDrop(
           draggedData,
           View.DragShadowBuilder(v),
           null,
           0
       )
   }
}

À ce stade, vous devriez pouvoir faire glisser la vue à l'aide d'un clic prolongé.

526e9e2a7f3a90ea.gif

Passons à la configuration de la vue de dépôt.

6. Configurer la vue pour DropTarget

Une vue peut servir de cible de dépôt si elle a implémenté l'interface OnDragListener.

Configurons la deuxième vue d'image pour en faire une cible de dépôt.

private fun setupDrop(dropTarget: View) {
   dropTarget.setOnDragListener { v, event ->
       // handle drag events here
       true
   }
}

Nous allons remplacer la méthode onDrag de l'interface OnDragListener. La méthode onDrag comporte deux arguments :

  1. Vue ayant reçu l'événement de déplacement
  2. Objet de l'événement de déplacement

Cette méthode renvoie la valeur "true" si l'événement de déplacement est traité correctement, ou "false" dans le cas contraire.

DragEvent

Indique qu'un paquet de données est transmis par le système à différents stades d'une opération de glisser-déposer. Cet ensemble de données encapsule des informations essentielles concernant l'opération elle-même et les données concernées.

DragEvent applique une action de déplacement différente selon l'étape de l'opération de glisser-déposer :

  1. ACTION_DRAG_STARTED : début de l'opération de glisser-déposer.
  2. ACTION _DRAG_LOCATION : l'utilisateur a déposé l'élément déplacé dans la zone d'entrée, c'est-à-dire en dehors de la zone de dépôt cible.
  3. ACTION_DRAG_ENTERED : la vue déplacée se trouve dans les limites de la vue de dépôt cible.
  4. ACTION_DROP : l'utilisateur a déposé l'élément déplacé dans la zone de dépôt cible.
  5. ACTION_DRAG_ENDED : l'opération de glisser-déposer est terminée.
  6. ACTION_DRAG_EXITED : fin de l'opération de glisser-déposer.

Valider DragEvent

Vous pouvez choisir de réaliser l'opération de glisser-déposer si toutes vos contraintes sont remplies dans l'événement ACTION_DRAG_STARTED. Dans cet exemple, nous pouvons vérifier si le type des données entrantes est correct ou non.

DragEvent.ACTION_DRAG_STARTED -> {
   Log.d(TAG, "ON DRAG STARTED")
   if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
       (v as? ImageView)?.alpha = 0.5F
       v.invalidate()
       true
   } else {
       false
   }
}

Dans cet exemple, nous avons vérifié si la ClipDescription de l'événement présentait un type MIME acceptable ou non. Si oui, nous indiquons que c'est le cas à l'aide d'un repère visuel et renvoyons la valeur "true", qui signale que les données déplacées sont en train d'être traitées. Sinon, nous renvoyons la valeur "false" pour indiquer que la vue déplacée n'est pas acceptée par la vue de dépôt cible.

Gérer les données déposées

Dans l'événement ACTION_DROP, nous pouvons choisir comment traiter les données déposées. Dans cet exemple, nous extrayons l'URL que nous avons ajoutée à ClipData en tant que texte. Nous faisons passer cette image de l'URL à notre vue d'image cible.

DragEvent.ACTION_DROP -> {
   Log.d(TAG, "On DROP")
   val item: ClipData.Item = event.clipData.getItemAt(0)
   val dragData = item.text
   Glide.with(this).load(item.text).into(v as ImageView)
   (v as? ImageView)?.alpha = 1.0F
   true
}

En plus de la gestion du dépôt, nous pouvons configurer ce qui se passe lorsqu'un utilisateur fait glisser la vue dans le cadre de délimitation de la vue de dépôt cible, et ce qui se passe lorsqu'il fait glisser la vue hors de la zone cible.

Ajoutons des repères visuels lorsque l'élément déplacé entre dans la zone cible.

DragEvent.ACTION_DRAG_ENTERED -> {
   Log.d(TAG, "ON DRAG ENTERED")
   (v as? ImageView)?.alpha = 0.3F
   v.invalidate()
   true
}

Ajoutons aussi des repères visuels lorsque l'utilisateur fait glisser la vue hors du cadre de délimitation de la vue de dépôt cible.

DragEvent.ACTION_DRAG_EXITED -> {
   Log.d(TAG, "ON DRAG EXISTED")
   (v as? ImageView)?.alpha = 0.5F
   v.invalidate()
   true
}

Ajoutons d'autres repères visuels pour indiquer la fin de l'opération de glisser-déposer.

DragEvent.ACTION_DRAG_ENDED -> {
   Log.d(TAG, "ON DRAG ENDED")
   (v as? ImageView)?.alpha = 1.0F
   true
}

À ce stade, vous devriez pouvoir faire glisser une image vers la vue d'image cible. Une fois cette image déposée, l'image de la vue d'image cible est modifiée en conséquence.

114238f666d84c6f.gif

7. Glisser-déposer en mode multifenêtre

Des éléments peuvent être déplacés d'une application à une autre, à condition que les applications se partagent l'écran en mode multifenêtre. L'implémentation permettant d'activer le glisser-déposer d'une application à une autre est identique, sauf que nous devons ajouter des indicateurs lors du déplacement, ainsi qu'une autorisation lors du dépôt.

Configurer des indicateurs lors du déplacement

Pour rappel, startDragAndDrop comporte un argument qui permet de spécifier les indicateurs et qui, à terme, contrôle l'opération de glisser-déposer.

v.startDragAndDrop(
   draggedData,
   View.DragShadowBuilder(v),
   null,
   View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
)

View.DRAG_FLAG_GLOBAL indique que le déplacement peut se faire hors des limites de la fenêtre et View.DRAG_FLAG_GLOBAL_URI_READ indique que le destinataire du déplacement est en mesure de lire le ou les URI de contenu.

Pour que la cible de dépôt puisse lire les données déplacées depuis d'autres applications, la vue de cible de dépôt doit déclarer l'autorisation de lecture.

val dropPermission = requestDragAndDropPermissions(event)

Elle doit aussi libérer l'autorisation une fois que les données déplacées ont été traitées.

dropPermission.release()

Voici à quoi ressemble le traitement final de l'élément déplacé :

DragEvent.ACTION_DROP -> {
   Log.d(TAG, "On DROP")
   val dropPermission = requestDragAndDropPermissions(event)
   val item: ClipData.Item = event.clipData.getItemAt(0)
   val dragData = item.text
   Glide.with(this).load(item.text).into(v as ImageView)
   (v as? ImageView)?.alpha = 1.0F
   dropPermission.release()
   true
}

À ce stade, vous devriez pouvoir faire glisser cette image vers une autre application. De même, les données déplacées depuis une autre application devraient pouvoir être gérées correctement.

8. Bibliothèque de glisser-déposer

Jetpack fournit une bibliothèque DragAndDrop pour simplifier l'implémentation du glisser-déposer.

Ajoutons une dépendance dans le fichier build.gradle.kts afin de pouvoir utiliser la bibliothèque DragAndDrop.

implementation("androidx.draganddrop:draganddrop:1.0.0")

Pour cet exercice, créez une activité distincte appelée DndHelperActivity.kt, comportant deux éléments ImageView en orientation verticale. Ils serviront respectivement de source de déplacement et de cible de dépôt.

Modifiez strings.xml pour ajouter des ressources de chaîne.

<string name="greeting_1">DragStartHelper and DropHelper</string>
<string name="target_url_1">https://services.google.com/fh/files/misc/qq9.jpeg</string>
<string name="source_url_1">https://services.google.com/fh/files/misc/qq8.jpeg</string>

Modifiez activity_dnd_helper.xml pour inclure des éléments ImageView.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:padding="24dp"
   tools:context=".DnDHelperActivity">

   <TextView
       android:id="@+id/tv_greeting"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toTopOf="@id/iv_source"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <ImageView
       android:id="@+id/iv_source"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drag_image"
       app:layout_constraintBottom_toTopOf="@id/iv_target"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/tv_greeting" />

   <ImageView
       android:id="@+id/iv_target"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drop_image"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/iv_source" />
</androidx.constraintlayout.widget.ConstraintLayout>

Pour finir, initialisez les vues dans DnDHelperActivity.kt.

class DnDHelperActivity : AppCompatActivity() {
   private val binding by lazy(LazyThreadSafetyMode.NONE) {
       ActivityMainBinding.inflate(layoutInflater)
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(binding.root)
       binding.tvGreeting.text = getString(R.string.greeting)
       Glide.with(this).asBitmap()
           .load(getString(R.string.source_url_1))
           .into(binding.ivSource)
       Glide.with(this).asBitmap()
           .load(getString(R.string.target_url_1))
           .into(binding.ivTarget)
       binding.ivSource.tag = getString(R.string.source_url_1)
   }
}

Veillez à modifier le fichier AndroidManifest.xml pour définir DndHelperActivity comme activité du lanceur d'applications.

<activity
   android:name=".DnDHelperActivity"
   android:exported="true">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>

DragStartHelper

Nous avons précédemment configuré la vue pour qu'elle soit déplaçable en implémentant onLongClickListener et en appelant startDragAndDrop. DragStartHelper simplifie l'implémentation en fournissant des méthodes utilitaires.

DragStartHelper(draggableView)
{ view: View, _: DragStartHelper ->
   // prepare clipData

   // startDrag and Drop
}.attach()

DragStartHelper accepte la vue à faire glisser en tant qu'argument. Ici, nous avons implémenté la méthode OnDragStartListener, dans laquelle nous allons préparer l'objet ClipData et lancer l'opération de glisser-déposer.

L'implémentation finale se présente comme suit :

DragStartHelper(draggableView)
{ view: View, _: DragStartHelper ->
   val item = ClipData.Item(view.tag as? CharSequence)
   val dragData = ClipData(
       view.tag as? CharSequence,
       arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
       item
   )
   view.startDragAndDrop(
       dragData,
       View.DragShadowBuilder(view),
       null,
       0
   )
}.attach()

DropHelper

DropHelper simplifie la configuration de la vue de dépôt cible en fournissant une méthode utilitaire appelée configureView.

La méthode configureView accepte quatre arguments :

  1. Activity : l'activité en cours
  2. dropTarget : la vue en cours de configuration
  3. mimeTypes : le type MIME des éléments de données déposés
  4. L'interface OnReceiveContentListener permettant de gérer les données déposées

Personnalisez la mise en surbrillance de la cible de dépôt.

DropHelper.configureView(
   This, // Current Activity
   dropTarget,
   arrayOf("text/*"),
   DropHelper.Options.Builder().build()
) {
   // handle the dropped data
}

OnReceiveContentListener reçoit le contenu déposé. Elle comporte deux paramètres :

  1. View : l'emplacement où le contenu est déposé.
  2. Payload : le contenu à déposer.
private fun setupDrop(dropTarget: View) {
   DropHelper.configureView(
       this,
       dropTarget,
       arrayOf("text/*"),
   ) { _, payload: ContentInfoCompat ->
       // TODO: step through clips if one cannot be loaded
       val item = payload.clip.getItemAt(0)
       val dragData = item.text
       Glide.with(this)
           .load(dragData)
           .centerCrop().into(dropTarget as ImageView)
       // Consume payload by only returning remaining items
       val (_, remaining) = payload.partition { it == item }
       remaining
   }
}

À ce stade, vous devriez pouvoir glisser-déposer des données à l'aide de DragStartHelper et DropHelper.

2e32d6cd80e19dcb.gif

Configurer la mise en surbrillance de la zone de dépôt

Comme vous l'avez vu, lorsqu'un élément déplacé entre dans la zone de dépôt, celle-ci est mise en surbrillance. Avec DropHelper.Options, nous pouvons personnaliser la façon dont la zone de dépôt est mise en surbrillance lorsqu'un élément déplacé entre dans les limites de la vue.

DropHelper.Options peut être utilisé pour configurer la couleur et l'arrondi d'angle de la mise en surbrillance de la zone cible de dépôt.

DropHelper.Options.Builder()
   .setHighlightColor(getColor(R.color.green))
   .setHighlightCornerRadiusPx(16)
   .build()

Ces options doivent être transmises en tant qu'arguments à la méthode configureView de DropHelper.

private fun setupDrop(dropTarget: View) {
   DropHelper.configureView(
       this,
       dropTarget,
       arrayOf("text/*"),
       DropHelper.Options.Builder()
           .setHighlightColor(getColor(R.color.green))
           .setHighlightCornerRadiusPx(16)
           .build(),
   ) { _, payload: ContentInfoCompat ->
       // TODO: step through clips if one cannot be loaded
       val item = payload.clip.getItemAt(0)
       val dragData = item.text
       Glide.with(this)
           .load(dragData)
           .centerCrop().into(dropTarget as ImageView)
       // Consume payload by only returning remaining items
       val (_, remaining) = payload.partition { it == item }
       remaining
   }
}

Vous devriez voir la couleur et l'arrondi d'angle de la mise en surbrillance lors du glisser-déposer.

9d5c1c78ecf8575f.gif

9. Recevoir du contenu enrichi

OnReceiveContentListener est l'API unifiée qui permet de recevoir du contenu enrichi, y compris du texte, du code HTML, des images, des vidéos, etc. Il est possible d'insérer le contenu dans les vues à l'aide du clavier, du glisser-déposer ou du presse-papiers. La gestion du rappel pour chaque mécanisme d'entrée peut s'avérer pénible. OnReceiveContentListener permet de recevoir des contenus tels que du texte, du balisage, de l'audio, de la vidéo, des images, etc. à l'aide d'une seule API. L'API OnReceiveContentListener consolide ces différents chemins de code sous la forme d'une seule API à implémenter. Vous pouvez ainsi vous concentrer sur la logique propre à votre application et laisser la plate-forme gérer le reste.

Pour cet exercice, créez une activité distincte appelée ReceiveRichContentActivity.kt, comportant deux éléments ImageView en orientation verticale. Ils serviront respectivement de source de déplacement et de cible de dépôt.

Modifiez strings.xml pour ajouter des ressources de chaîne.

<string name="greeting_2">Rich Content Receiver</string>
<string name="target_url_2">https://services.google.com/fh/files/misc/qq1.jpeg</string>
<string name="source_url_2">https://services.google.com/fh/files/misc/qq3.jpeg</string>

Modifiez activity_receive_rich_content.xml pour y inclure des éléments ImageViews.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".ReceiveRichContentActivity">

   <TextView
       android:id="@+id/tv_greeting"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toTopOf="@id/iv_source"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <ImageView
       android:id="@+id/iv_source"
       android:layout_width="320dp"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drag_image"
       app:layout_constraintBottom_toTopOf="@id/iv_target"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/tv_greeting" />

   <ImageView
       android:id="@+id/iv_target"
       android:layout_width="320dp"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drop_image"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/iv_source" />
</androidx.constraintlayout.widget.ConstraintLayout>

Pour finir, initialisez les vues dans ReceiveRichContentActivity.kt.

class ReceiveRichContentActivity : AppCompatActivity() {
   private val binding by lazy(LazyThreadSafetyMode.NONE) {
       ActivityReceiveRichContentBinding.inflate(layoutInflater)
   }
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(binding.root)
       binding.tvGreeting.text = getString(R.string.greeting_2)
       Glide.with(this).asBitmap()
           .load(getString(R.string.source_url_2))
           .into(binding.ivSource)
       Glide.with(this).asBitmap()
           .load(getString(R.string.target_url_2))
           .into(binding.ivTarget)
       binding.ivSource.tag = getString(R.string.source_url_2)
   }
}

Veillez à modifier AndroidManifest.xml pour définir DndHelperActivity comme activité de lanceur.

<activity
   android:name=".ReceiveRichContentActivity"
   android:exported="true">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>

Commençons par créer un rappel qui implémente OnReceiveContentListener..

val listener = OnReceiveContentListener { view, payload ->
   val (textContent, remaining) =
       payload.partition { item: ClipData.Item -> item.text != null }
   if (textContent != null) {
       val clip = textContent.clip
       for (i in 0 until clip.itemCount) {
           val currentText = clip.getItemAt(i).text
           Glide.with(this)
               .load(currentText)
               .centerCrop().into(view as ImageView)
       }
   }
   remaining
}

Ici, nous avons implémenté l'interface OnRecieveContentListener. La méthode onRecieveContent comporte deux arguments :

  1. Vue actuelle qui reçoit les données
  2. Charge utile de données reçue à partir du clavier, du glisser-déposer ou du presse-papiers sous la forme d'une méthode ContentInfoCompat

Cette méthode renvoie la charge utile qui n'est pas gérée.

Ici, nous avons segmenté la charge utile en contenu textuel et autre contenu à l'aide de la méthode Partition. Nous traitons les données textuelles selon nos besoins et renvoyons la charge utile restante.

Déterminons maintenant quoi faire avec les données déplacées.

val listener = OnReceiveContentListener { view, payload ->
   val (textContent, remaining) =
       payload.partition { item: ClipData.Item -> item.text != null }
   if (textContent != null) {
       val clip = textContent.clip
       for (i in 0 until clip.itemCount) {
           val currentText = clip.getItemAt(i).text
           Glide.with(this)
               .load(currentText)
               .centerCrop().into(view as ImageView)
       }
   }
   remaining
}

Notre écouteur est maintenant prêt. Ajoutons cet écouteur à la vue cible.

ViewCompat.setOnReceiveContentListener(
   binding.ivTarget,
   arrayOf("text/*"),
   listener
)

À ce stade, vous devriez pouvoir effectuer un glisser-déposer d'une image dans la zone cible. Une fois déposée, l'image déplacée doit remplacer l'image d'origine dans la vue de dépôt cible.

e4c3a3163c51135d.gif

10. Félicitations !

Vous savez maintenant implémenter le glisser-déposer dans votre application Android. Dans cet atelier de programmation, vous avez appris à créer des interactions de glisser-déposer interactives dans votre application Android, ainsi que d'une application à une autre, ce qui vous permet d'améliorer l'expérience de l'utilisateur et de lui proposer de nouvelles fonctionnalités. Voici ce que vous avez appris :

  • Principes de base du glisser-déposer : comprendre les quatre étapes d'un événement de glisser-déposer (démarrage, poursuite de l'opération, fin, sortie) et les données clés au sein de l'objet DragEvent
  • Activation du glisser-déposer : rendre la vue déplaçable et gérer son dépôt dans la vue cible à l'aide de DragEvent
  • Glisser-déposer en mode multifenêtre : permettre le glisser-déposer d'une application à une autre en définissant les indicateurs et les autorisations appropriées
  • Utilisation de la bibliothèque DragAndDrop : simplifier l'implémentation du glisser-déposer à l'aide de la bibliothèque Jetpack
  • Réception de contenus enrichis : implémenter la gestion de différents types de contenus (texte, images, vidéos, etc.) à partir de différentes méthodes d'entrée à l'aide d'une API unifiée.

En savoir plus