Quantcast
Channel: vidasConcurrentes » java
Viewing all articles
Browse latest Browse all 10

Detectando Drag & Drop en un Canvas de Android – Parte II

$
0
0

Continuamos, y con esta entrada concluimos, la entrada anterior en la que hablaba sobre cómo dibujar figuras en un Canvas en Android. Si no has leido primero esa entrada, por favor hazlo antes de continuar con esta. En este caso vamos a detectar pulsaciones de ratón (nuestro dedo) y emular el evento de Drag & Drop.

Vamos a partir del código que se puede encontrar en este directorio de nuestro repositorio. Recordamos además que nuestra entrada anterior concluyó con lo siguiente en nuestro terminal:

Vamos a incluir un par de modificaciones a nuestro código anterior para hacer las cosas mejor y no pasar números diréctamente en la función onDraw().
La idea es que vamos a crear una superclase abstracta que llamaremos Figura y de ahí crearemos nuestra clase Círculo y nuestra clase Rectángulo. Cada una de las figuras tendrá asociado un identificador único para poder identificar sobre qué figura hemos hecho nuestras futuras pulsaciones. Comencemos entonces.

El primer paso es crear la superclase abstracta Figura que será de la siguiente manera:

package com.vidasconcurrentes.dragdropcanvas;

public abstract class Figura {

	protected int id;
	protected int x;
	protected int y;

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return y;
	}

	public void setY(int y) {
		this.y = y;
	}

	public int getId() {
		return id;
	}
}

Nada complejo. Sabemos que no podremos crear ninguna instancia de esta clase por ser abstracta, así que obligamos a que las clases que quieran ser figuras tengan un identificador. Además, toda figura va a tener un punto central, así que servimos las funciones necesarias para operar con ello. Nótese que no queremos que el identificador cambie, por eso no hay un setter.

Ahora creamos las clases Círculo y Rectángulo:

package com.vidasconcurrentes.dragdropcanvas;

public class Circulo extends Figura {

	private int radio;

	public Circulo(int id, int x, int y, int radio) {
		this.id = id;
		this.x = x;
		this.y = y;
		this.radio = radio;
	}

	public int getRadio() {
		return radio;
	}
}
package com.vidasconcurrentes.dragdropcanvas;

public class Rectangulo extends Figura {

	private int ancho;
	private int alto;

	public Rectangulo(int id, int x, int y, int ancho, int alto) {
		this.id = id;
		this.x = x;
		this.y = y;
		this.ancho = ancho;
		this.alto = alto;
	}

	public int getAncho() {
		return ancho;
	}

	public int getAlto() {
		return alto;
	}
}

De acuerdo, nada raro por aquí tampoco. No queremos que se modifiquen el radio en el caso del Círculo, ni el ancho o alto en el caso del Rectángulo.

Adaptemos ahora nuestro código anterior a esta nueva especificación. Todos los cambios necesarios serán en nuestro SurfaceView. Lo primero será añadir un nuevo atributo a nuestro SurfaceView que contendrá las figuras, de la siguiente pinta:

private ArrayList<Figura> figuras;

Y necesitamos inicializarlo también. Podemos hacerlo en el código del surfaceCreated()de la siguiente manera (antes del código que había antes, por ejemplo):

int id = 0;
figuras = new ArrayList<Figura>();
figuras.add(new Circulo(id++,200,200,100));
figuras.add(new Rectangulo(id++,200,500,200,200));

Hay que darse cuenta de que ahora especificamos el ancho y el alto del rectángulo, no las coordenadas del punto superior derecho. Nuestra función onDraw() será de la siguiente forma:

@Override
public void onDraw(Canvas canvas) {
	Paint p = new Paint();
	p.setColor(Color.BLACK);
	p.setAntiAlias(true);

	canvas.drawColor(Color.WHITE);

	Circulo c = (Circulo) figuras.get(0);
	canvas.drawCircle(c.getX(), c.getY(), c.getRadio(), p);
	Rectangulo r = (Rectangulo) figuras.get(1);
	canvas.drawRect(r.getX(), r.getY(), r.getX()+r.getAncho(), r.getY()+r.getAlto(), p);
}

La forma de acceder a estos elementos quizá no es la más bonita ni correcta, pero en lugar de usar atributos extra, nos permitimos la licencia de hacer esto. Si ejecutamos el código ahora, obtenemos la siguiente imagen (que es igual que la anterior, como pretendíamos):

Ahora es cuando vamos a agregar el evento de click. Vamos a redefinir la función onTouchEvent() para que haga lo que queremos. Además, añadimos un nuevo atributo a nuestra clase que contendrá el identificador de la figura activa. Vamos por partes entonces, primero creando la función:

@Override
public boolean onTouchEvent(MotionEvent event) {
	int x = (int) event.getX();
	int y = (int) event.getY();

	return true;
}

Esta función se va a llamar siempre que se haga una pulsación en pantalla. Por ahora sólo estamos recogiendo en qué posición de la pantalla (coordenada x e y) hemos hecho esa pulsación.

Ahora nos interesan tres tipos de pulsaciones, con sus respectivas acciones:

  • Hemos pulsado y tenemos el dedo aún pulsando -> si hemos pulsado donde hay una figura, esa figura es la activa. La acción se encuentra en MotionEvent.ACTION_DOWN.
  • Con el dedo pulsando hemos movido el dedo -> si había alguna figura activa, modificamos su posición para que siga al dedo. La acción se encuentra en MotionEvent.ACTION_MOVE.
  • Hemos levantado el dedo -> no hay ninguna figura activa. La acción se encuentra en MotionEvent.ACTION_UP.

Esto lo podemos implementar con una estructura switch. Usamos -1 como elemento nulo para nuestra figura activa porque no existen índices negativos en un ArrayList.

Para el caso de hacer una pulsación:

case MotionEvent.ACTION_DOWN:
	for(Figura f : figuras) {
		if(f instanceof Circulo) {
			Circulo c = (Circulo) f;
			int distanciaX = x - c.getX();
			int distanciaY = y - c.getY();
			if(Math.pow(c.getRadio(), 2) > (Math.pow(distanciaX, 2) + Math.pow(distanciaY, 2))) {
				figuraActiva = c.getId();
				break;
			}
		} else {
			Rectangulo r = (Rectangulo) f;
			if(x > r.getX() && x < r.getX()+r.getAncho() && y > r.getY() && y < r.getY()+r.getAlto()) {
				figuraActiva = r.getId();
				break;
			}
		}
	}
	break;

Aquí vamos recorriendo todas las figuras que tenemos (está preparado para un número variable de círculos y rectángulos). Si el elemento que estamos mirando es un círculo, comprobamos que hemos pinchado dentro del área del círculo. Si es un rectángulo hacemos lo propio, pero como son figuras distintas, necesitamos hacer códigos distintos.

Para el caso de arrastrar:

case MotionEvent.ACTION_MOVE:
	if(figuraActiva != -1) {
		if(figuras.get(figuraActiva) instanceof Circulo) {
			figuras.get(figuraActiva).setX(x);
			figuras.get(figuraActiva).setY(y);
		} else {
			Rectangulo r = (Rectangulo) figuras.get(figuraActiva);
			r.setX(x - r.getAncho()/2);
			r.setY(y - r.getAlto()/2);
		}
	}
	break;

Aquí hacemos algo similar al caso de arriba. Si es un círculo, movemos el centro a donde esté el dedo. Si es un rectángulo movemos teniendo en cuenta que el dedo estará en el centro del rectángulo siempre. Esto necesitaría de una corrección por si al pulsar no estábamos en el centro… pero para mantener la explicación simple, tomamos este pequeño error de cálculo como válido.

Para el caso de levantar el dedo:

case MotionEvent.ACTION_UP:
	figuraActiva = -1;
	break;

Aquí símplemente decimos que no tenemos ninguna figura activa.

Para acabar con esta entrada sólo apuntar un detalle. Actualmente el código en el caso de tocar y comprobar sobre qué figura estamos pulsando se queda con la figura de menor índice que encuentra (por los break dentro del if). Esto tiene un problema. Tal y como está hecha la función de pintado, se pinta primero el círculo y sobre él se pinta el rectángulo. Imaginemos que arrastramos el círculo encima del rectángulo. Cambiando un poco de color, tendríamos esto:

Ahora supongamos que pulsamos encima del rectángulo en una posición que también corresponda al círculo. Visualmente el rectángulo está por encima, pero el resultado es que se mueve el círculo. Existen dos soluciones para esto (siempre que mantengamos que el rectángulo se pinta encima del círculo):

  • La más simple: quitar los breaks de los dos if en el primer caso del switch. De esta forma nos quedamos con la de mayor identificador que esté en la misma posición.
  • La mejor en caso de tener muchas figuras: cambiar la forma de iterar sobre los elementos para ir desde el de mayor identificador al menor, y en cuanto encontremos uno, hacer el break.

En esta entrada hemos visto la continuación del mini-ciclo en el que explicábamos cómo pintar figuras primitivas de una forma simple en dispositivos Android, y además poder detectar eventos de pulsación sobre las figuras que formaban parte de un mismo Canvas.

Una vez visto esto, hay muchísimas cosas que se podrían hacer usando estas técnicas y quizá las haya mejores… pero esta es simple y funcional.

Como siempre, el código completo de esta entrada está disponible en este directorio de nuestro repositorio.

Un saludo para todos.

Más información:
Android Developers


Viewing all articles
Browse latest Browse all 10