Approche naturelle de Drag and Drop en Android

Ce billet présente une approche naturelle pour faire du Drag and Drop de widgets dans un conteneur d'une application Android.

En introduction, voyons les solutions qu'on retrouve en grand nombre sur le net.

Nombreuses solutions de Drag and Drop - solutions orientées canvas

En recherchant sur le net on trouve pleins de solutions différentes de DnD de Views avec Android. Un peu déstabilisant.

Par exemple le billet suivant. Ici l'approche consiste a avoir un conteneur qui implémente une méthode onDraw() et qui prend en charge le ré-affichage de tous les bitmaps.

Ce même conteneur capture la position et le mouvement du doigt. Déjà, lors de l'événement DOWN, on regarde si ya un des items qui correspond a la position du doigt. Lors du MOVE, on actualise des propriétés X,Y du bitmap. On appelle invalidate(), qui invoque le onDraw() qui re-dessine tout. Notamment notre bitmap avec une position actualisée.

Ci-dessous un exemple avec une classe Draggable, type data-objet qui contient simplement le bitmap et les coordonnées x et y.

@Override protected void onDraw(Canvas canvas) {
    	for (Draggable ball : draggables) {
            canvas.drawBitmap(ball.img, ball.x, ball.y, null);
          }
    }

Après, il y a d'autres variantes : on met le listener non pas sur le conteneur mais sur chacun des widgets qui encapsule le bitmap. Mais toujours avec une approche de onDraw(.) globale.

Je dirais que ces d'approche sont orienté canvas.

Approche naturelle - orientée layout

Plus naturel, est simplement de déplacer le seul item sous le doigt. Sans recourt au moindre onDraw(.). Si on a des centaines d'items, cette approche est aussi plus soucieuse des performances.

Pour cela il suffit d'actualiser unitairement le layout du widget en drag. Android 2.2 a bani l'AbsolutLayout; mais on a à disposition le FrameLayout.

Une sous-classe direct de ViewGroup, qui supporte donc un addView(widget) pour ajouter des widgets avec la possibilité d'une gestion de la position (x,y), via le layout.

public class BallView extends ImageView {
	public BallView(Context context, int X, int Y) { 
	this.setImageDrawable( context.getResources().getDrawable(R.drawable.bol_rood));
	setOnTouchListener(this);
    	FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(WIDTH, HEIGHT, Gravity.TOP);
    	layout.leftMargin = X - layout.width/2;
    	layout.topMargin = Y - layout.height/2;
    	this.setLayoutParams(layout);
	}

Ci-dessus, notre widget ImageView affiche un drawable, qui sera centrée à la position X,Y si le conteneur parent sait gérer un layout absolu.

On a construit un LayoutParams avec les dimensions WIDTH et HEIGTH souhaitées pour le widget ; ces constantes sont définies plus haute.

On portera une attention particulière au fait qu'on a opté pour le constructeur à 3 paramètres. Sans ce paramètre, avec par exemple la valeur Gravity.TOP, on n'a pas le positionnement effectif, pourquoi?

Maintenant il suffit que le widget s'abonne lui-même aux événements MotionEvent. Pour simplement actualiser les propriétés leftMargin et topMargin pour induire son propre repositionement au sein de son conteneur.

Dans le principe tout simplement,ceci !

public class BallView extends ImageView implements OnTouchListener {
	public boolean onTouch(View v, MotionEvent event) {
	FrameLayout.LayoutParams layout = (FrameLayout.LayoutParams) v.getLayoutParams();
    	if (event.getAction()==MotionEvent.ACTION_MOVE) {
		layout.leftMargin = (int) event.getRawX();    		
		layout.topMargin = (int) event.getRawY();
               }
	v.setLayoutParams(layout);
	return true;
    	}

L'approche tient à la méthode d'instance .setLayoutParams(.) qui repositionne effectivement l'instance au sein de son conteneur.

Cette approche je la qualifierais orientée layout.

On notera qu'on a casté en FrameLayout.LayoutParams le layout obtenu à partir de la View cible du MotionEvent. C'est précisément ainsi qu'on accède aux propriétés leftMargin et topMargin sur lesquelles repose notre approche.

Ceci fonctionne très bien.

Avec un petit désagrément toutefois : un petit décalage opère suivant l'endroit où on clic réellement dans le widget. Pour compenser il suffit de stocker cette valeur lors du DOWN, pour la retrancher lors du MOVE :

private int localX=0;
	private int localY=0;
	public boolean onTouch(View v, MotionEvent event) {
		FrameLayout.LayoutParams layout = (LayoutParams) v.getLayoutParams();
    	if (event.getAction()==MotionEvent.ACTION_DOWN) {
    		localX = (int)event.getX();
    		localY = (int)event.getY();
    		v.bringToFront();
    		return true;
    	}
    	if (event.getAction()==MotionEvent.ACTION_MOVE) {
			layout.leftMargin = (int) event.getRawX() - localX;    		
			layout.topMargin = (int) event.getRawY() - v.getHeight()/2 - localY;
    	}
		v.setLayoutParams(layout);
		return true;
	}

Voilà, un DnD des plus naturelle.

On peut aussi en profiter pour mettre au premier plan le widget cible du DnD, avec simplement bringToFront().

Démo et code source

On pourra télécharger le code source de l'exemple suivant. Il est légèrement plus complet : si on clic dans le conteneur, mais pas sur un widget, alors on ajoute un widget, avec un rayon en proportion à la pression du doigt.

http://blog.scoutant.org/public/layout-dnd.zip

Au final on a plein de billes rouges comme dans l'image. Que l'on peut déplacer unitairement. La réactivité est maintenue même avec des centaines de billes.

On pourra aussi tester directement en accédant à l'application Android : layout-dnd.apk.

layout-dnd.png

Jusqu'ici on déplace seulement une bille à fois.

Mais des doigts, en a plusieurs! Si on veut déplacer plusieurs items à la fois c'est tout à fait possible... prochain post!

Commentaires

2. Le vendredi, mai 13 2011, 18:50 par eric nantes

Bonjour,
Je suis débutant dans le développement d'application Android et votre tuto est vraiment intéressant, bravo !!
Je souhaiterai pouvoir choisir les couleurs de boule via la touche menu (j'ai déjà créé mon menu xml), mais je n'arrive pas à lier l'ensemble.
Merci de me renseigner si vous en avez le temps. Cordialement, Eric

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.

La discussion continue ailleurs

URL de rétrolien : http://blog.scoutant.org/index.php?trackback/7