Utiliser les Widgets Draggable et DragTarget
Les widgets Draggable et DragTarget travaillent ensemble pour créer une expérience de glisser-déposer (Drag and drop en anglais) dans Flutter. En effet, Draggable va vous permettre de rendre un élément déplaçable, tandis que DragTarget définit une zone où celui-ci peut être relâché pour déclencher une action.
Comprendre le widget Draggable
Le widget Draggable permet à un widget (comme une image, un texte ou un bouton) d’être déplacé avec le doigt. Ainsi, lorsqu’un utilisateur commence à faire glisser l’élément, un widget de feedback suit le doigt ou la souris, et des données peuvent être transportées pour être transmises à un DragTarget.
Voici quelques une des propriétés principales de ce widget :
- child : Le widget qui est affiché quand l’élément n’est pas en cours de déplacement.
- feedback : Le widget qui est affiché sous le doigt/souris pendant le drag (par exemple, une version semi-transparente de l’élément).
- childWhenDragging : Ce qui reste à la place de l’élément pendant le déplacement (par exemple un widget grisé).
- data : Les données transportées avec le widget (par exemple, un String, un int, ou un objet) et qui seront transmises au DragTarget.
- axis : Restreint le mouvement à un axe (horizontal ou vertical).
- maxSimultaneousDrags : Limite le nombre de drags simultanés (utile pour les appareils multitouch).
Vous pouvez également définir des actions pendant le drag and drop, avec les propriétés suivantes :
- onDragStarted : Déclenché quand l’utilisateur commence à faire glisser l’élément, par exemple pour jouer un son ou afficher un indicateur visuel.
- onDragUpdate : Appelé à chaque mouvement du feedback pendant le drag. Il fournit un DragUpdateDetails avec la position actuelle, ce qui peut être utile pour créer des effets visuels pendant le déplacement.
- onDragEnd : Déclenché quand le drag se termine, que l’élément soit relâché sur un DragTarget ou non. Il fournit un DraggableDetails indiquant si le drop a été accepté (wasAccepted), la vitesse finale, et la position.
- onDragCompleted : Appelé uniquement si l’élément est relâché et accepté par un DragTarget. Vous allez donc pouvoir l’utiliser pour confirmer qu’une action est réussie, comme pour mettre à jour une liste, par exemple.
- onDraggableCanceled : Déclenché si le drag est annulé (par exemple, l’élément est relâché hors d’un DragTarget ou le geste est interrompu). Il fournit des détails comme la vitesse et la position finale.
Comprendre le widget DragTarget
Le DragTarget définit une zone où un Draggable peut être relâché. Il vérifie si les données transportées sont valides et permet d’exécuter une action lorsque l’élément est accepté.
Voici quelques une des propriétés principales de ce widget :
-
builder : Permet de construire l’apparence du DragTarget grâce à trois paramètres :
- context : Le contexte de construction.
- candidateData : Une liste des données des Draggable actuellement au-dessus de la zone (permet de changer l’apparence si un élément est détecté).
- rejectedData : Les données des Draggable non acceptables.
- onWillAccept : Une fonction qui décide si les données d’un Draggable sont acceptables. Elle retourne true pour accepter ou false pour rejeter. Par exemple, vous pouvez vérifier que le Draggable est bien du type esperé (String, int, etc).
- onAccept : Cette propriété est appelée quand un Draggable est relâché et accepté. Elle reçoit les données transportées et permet de déclencher une action (comme ajouter un élément à une liste).
- onLeave : Appelée quand un Draggable quitte la zone sans être relâché, ce qui peut être utilie pour réinitialiser un état visuel. Par exemple, mon DragTarget peut devenir rouge quand le Draggable passe au-dessus, puis redevenir normal si je le bouge en dehors.
Exemple 1 : Créer une Liste d’Éléments Réorganisables
Voici un exemple où je crée une liste de trois éléments textuels que l’utilisateur peut réorganiser en les faisant glisser. Chaque élément est à la fois un Draggable et un DragTarget.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Liste Réorganisable')),
body: ReorderableListExample(),
),
);
}
}
class ReorderableListExample extends StatefulWidget {
@override
_ReorderableListExampleState createState() => _ReorderableListExampleState();
}
class _ReorderableListExampleState extends State<ReorderableListExample> {
List<String> items = ['Tâche 1', 'Tâche 2', 'Tâche 3'];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return Draggable<int>(
data: index,
feedback: Material(
child: Container(
color: Colors.blue.withOpacity(0.5),
padding: EdgeInsets.all(16),
child: Text(items[index], style: TextStyle(fontSize: 20)),
),
),
childWhenDragging: Container(
color: Colors.grey,
height: 50,
margin: EdgeInsets.symmetric(vertical: 4),
),
child: DragTarget<int>(
builder: (context, candidateData, rejectedData) {
return Container(
color: candidateData.isNotEmpty ? Colors.green : Colors.blue,
margin: EdgeInsets.symmetric(vertical: 4),
padding: EdgeInsets.all(16),
child: Text(items[index], style: TextStyle(fontSize: 20, color: Colors.white)),
);
},
onAccept: (oldIndex) {
setState(() {
final item = items.removeAt(oldIndex);
items.insert(index, item);
});
},
),
);
},
);
}
}
Comment fonctionne le drag-and-drop ?
Ici, chaque élément de la liste (par exemple, « Tâche 1 », « Tâche 2 », « Tâche 3 ») est à la fois un Draggable et un DragTarget. Voici comment fonctionne l’interaction :
1. Le Draggable :
Chaque élément a un Draggable<int> avec data: index, où index est la position de l’élément dans la liste (Donc 0 pour « Tâche 1 », 1 pour « Tâche 2 », etc.). Quand on fait glisser un élément, il transporte avec lui cette donnée (l’index de l’élément déplacé).
2. Le DragTarget :
De plus, chaque élément est aussi un DragTarget<int>, ce qui signifie qu’il peut accepter une donnée (ici un index) provenant d’un autre Draggable. Quand on relâche un élément sur un autre, le DragTarget de l’élément cible reçoit l’index de l’élément qui a été déplacé (via la propriété onAccept).
3. L’action dans onAccept :
Grâce à la propriété onAccept du widget DragTarget cette fonction va venir s’executer :
setState(() {
final item = items.removeAt(oldIndex);
items.insert(index, item);
});
Ici, oldIndex est l’index de l’élément qu’on a fait glisser, et index est l’index de l’élément cible. Voici donc ce qui se passe :
- On supprime l’élément à la position oldIndex de la liste items.
- Puis on insère ce même élément à la position index (la position de l’élément cible).
- SetState permet de rafraichir l’application et de réorganiser visuellement la liste dans le nouvel ordre donné.
En conclusion, le nouvel élément ne vient pas réellement se mettre à la place de l’ancien, on reconstruit juste l’application sur la base du nouvel ordre des tâches.
Exemple 2 : Un caddie de courses en ligne
Voici un deuxième exemple avec un caddie de course dans lequel l’utilisateur peut faire glisser des produits.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Caddie de Courses')),
body: ShoppingCartExample(),
),
);
}
}
class ShoppingCartExample extends StatefulWidget {
@override
_ShoppingCartExampleState createState() => _ShoppingCartExampleState();
}
class _ShoppingCartExampleState extends State<ShoppingCartExample> {
List<String> cart = [];
@override
Widget build(BuildContext context) {
return Column(
children: [
// Produits Draggable
Wrap(
spacing: 10,
children: ['Pomme', 'Banane', 'Orange'].map((product) {
return Draggable<String>(
data: product,
feedback: Material(
child: Container(
padding: EdgeInsets.all(8),
color: Colors.orange.withOpacity(0.5),
child: Text(product, style: TextStyle(fontSize: 18)),
),
),
childWhenDragging: Container(),
child: Container(
padding: EdgeInsets.all(8),
color: Colors.orange,
child: Text(product, style: TextStyle(fontSize: 18, color: Colors.white)),
),
);
}).toList(),
),
SizedBox(height: 20),
// Caddie DragTarget
DragTarget<String>(
builder: (context, candidateData, rejectedData) {
return Container(
width: 200,
height: 200,
color: candidateData.isNotEmpty ? Colors.green : Colors.grey,
child: Center(
child: Text(
cart.isEmpty ? 'Caddie vide' : 'Caddie : ${cart.join(', ')}',
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
);
},
onAccept: (product) {
setState(() {
cart.add(product);
});
},
),
],
);
}
}
Comment fonctionne le drag-and-drop ?
- Draggable : Chaque produit est un Draggable avec data: product pour transporter son nom. Le feedback est une version semi-transparente du texte, et childWhenDragging est vide, faisant disparaître le produit pendant le drag.
- DragTarget : Le caddie est un DragTarget qui accepte des String. Quand un produit est relâché, onAccept l’ajoute à la liste cart, et setState met à jour l’affichage.
- Personnalisation : Le caddie devient vert quand un produit est au-dessus (via candidateData), et affiche la liste des produits ajoutés.