Quantcast
Channel: vidasConcurrentes » canvas
Viewing all articles
Browse latest Browse all 4

Usando el multi-touch en Android

0
0

Desde la llegada de las pantallas táctiles a los dispositivos móviles hemos sufrido una transformación en nuestra forma de pensar a la hora de utilizar dichos dispositivos. El control se ha vuelto mucho más intuitivo a la hora de usar este método de entrada/salida. Con la llegada del Lemur Input Device apareció el concepto de multitáctil, si bien fue Apple quien registró la palabra multi-touch junto a su iPhone.

En esta entrada vamos a aprender cómo detectar en nuestros dispositivos con Android la entrada multitáctil a lo largo de un ejemplo sencillo de uso.

La entrada multitáctil es el conjunto de hardware y software que permite que funcione esta interacción persona-ordenador. Esto es importante, puesto que nosotros podemos querer detectar una entrada de cuatro dedos en pantalla, pero si nuestro hardware sólo detecta dos no vamos a poder darle esa funcionalidad (por mucho que el software sí lo permita).

Comenzamos la entrada abriendo nuestro Eclipse configurado con el Android SDK y creando un nuevo proyecto de Android. Vamos a realizar el siguiente ejemplo basándonos en el ejemplo que desarrollamos en esta entrada (importante leerla si no entiendes los pasos que damos en esta entrada).

Nuestra pequeña aplicación de prueba va a constar de un SurfaceView donde haremos tanto el pintado como la detección de que hemos tocado la pantalla, y de un hilo que se encargará de pintar dicho SurfaceView en pantalla. No va a haber explicación de la estructura base, aunque sí veremos el código, por lo que recomendamos la lectura de la entrada mencionada anteriormente para comprender los pasos.

Lo primero será crear la clase que va a modelar el hilo encargado del pintado, y su código es este:

package com.vidasconcurrentes.usandomultitouch;

import android.graphics.Canvas;
import android.view.SurfaceHolder;

public class MultitouchPaintThread extends Thread {

	private SurfaceHolder sh;
	private MultitouchView view;
	private boolean run;

	public MultitouchPaintThread(SurfaceHolder sh, MultitouchView view) {
		this.sh = sh;
		this.view = view;
		run = false;
	}

	public void setRunning(boolean run) {
		this.run = run;
	}

	public void run() {
		Canvas canvas;
		while(run) {
			canvas = null;
			try {
				canvas = sh.lockCanvas(null);
				synchronized(sh) {
					view.onDraw(canvas);
				}
			} finally {
				if(canvas != null)
					sh.unlockCanvasAndPost(canvas);
			}
		}
	}
}

Ahora tenemos que crear la clase que va a modelar el SurfaceView. Pegamos el código siguiente:

package com.vidasconcurrentes.usandomultitouch;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MultitouchView extends SurfaceView implements SurfaceHolder.Callback {

	private MultitouchPaintThread thread;

	public MultitouchView(Context context) {
		super(context);
		getHolder().addCallback(this);
	}

	@Override
	public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {

	}

	@Override
	public void surfaceCreated(SurfaceHolder arg0) {
		thread = new MultitouchPaintThread(getHolder(), this);
		thread.setRunning(true);
		thread.start();
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder arg0) {
		boolean retry = true;
		thread.setRunning(false);
		while (retry) {
			try {
				thread.join();
				retry = false;
			} catch (InterruptedException e) { }
		}
	}

	@Override
	protected void onDraw(Canvas canvas) {
		Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
		paint.setColor(Color.RED);
		canvas.drawCircle(getWidth()/2, getHeight()/2, getHeight()/4, paint);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return true;
	}
}

Y por último modificamos nuestra Activity creada por defecto y le cambiamos el constructor de modo que tenemos el siguiente código:

package com.vidasconcurrentes.usandomultitouch;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;

public class UsandoMultitouch extends Activity {

	@Override
	public void onCreate(Bundle savedInstanceState) {
	    super.onCreate(savedInstanceState);
	    requestWindowFeature(Window.FEATURE_NO_TITLE);
	    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
	    setContentView(new MultitouchView(this));
	}
}

Después de haber creado esto modificamos el AndroidManifest.xml del proyecto para cambiar la orientación de la pantalla de la Activity, vamos a ponerla obligatoriamente apaisada:

<activity android:name=".UsandoMultitouch"
          android:label="@string/app_name"
          android:screenOrientation="landscape">

Ahora sí, después de haber creado y modificado todo lo dicho, vamos a ejecutar el proyecto y veremos lo siguiente:

Después de haber visto todo lo anterior sin explicación (recordemos que hay que revisar esta entrada si hay dudas), comenzamos con lo que de verdad tiene que ver con esta entrada.

Dectectando eventos táctiles

Hemos visto que hemos creado la función onTouchEvent() en la clase que modela el SurfaceView. Ahora vamos a escribir la funcionalidad para dicho método. Como una imagen vale más que mil palabras, vamos a posicionar un círculo en la pantalla allá donde hayamos tocado (comenzando con un sólo dedo en pantalla).

Como vimos en esta entrada existen diferentes eventos que van a lanzar el método onTouchEvent(). Tenemos que saber que vamos a detectar tres tipos diferentes de evento:

  • Hemos pulsado con un dedo la pantalla.
  • Hemos movido un dedo.
  • Hemos levantado un dedo de la pantalla.

Comenzamos creando un nuevo atributo en la clase MultitouchView que va a almacenar las coordenadas en las que pulsamos con el dedo:

private int[] coordenadasDedo1 = {-1, -1};

Este nuevo atributo va a almacenar la coordenada X y la coordenada Y del lugar donde pulsamos con el dedo. El valor -1 va a ser el que vamos a considerar valor nulo. Vamos a la función onTouchEvent() y la modificamos:

@Override
public boolean onTouchEvent(MotionEvent event) {
	switch(event.getAction()) {
	case MotionEvent.ACTION_DOWN:
		coordenadasDedo1[0] = (int) event.getX();
		coordenadasDedo1[1] = (int) event.getY();
		break;
	case MotionEvent.ACTION_UP:
		coordenadasDedo1[0] = -1;
		coordenadasDedo1[1] = -1;
	}
	return true;
}

En caso de haber detectado que hemos tocado con un dedo (ACTION_DOWN) almacenamos las coordenadas y en caso de detectar que hemos levantado ese dedo (ACTION_UP), entonces las devolvemos al valor nulo que hemos definido (el cual podría interesarnos que fuera una constante). Lo siguiente es hacer que se pinte un círculo allá donde hayamos puesto el dedo, para lo cual modificamos la función onDraw():

@Override
protected void onDraw(Canvas canvas) {
	Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
	paint.setColor(Color.RED);

	canvas.drawColor(Color.BLACK);

	if(coordenadasDedo1[0] >= 0 && coordenadasDedo1[1] >= 0) {
		canvas.drawCircle(coordenadasDedo1[0], coordenadasDedo1[1], getHeight()/6, paint);
	}
}

Una observación importante: miremos el método drawCircle() de la clase Canvas. Recibe cuatro atributos: coordenada X del origen, coordenada Y del origen, radio y el objeto de la clase Paint usado para pintarlo. ¿Por qué he usado getHeight()/6 como radio en lugar de un número entero? Cuando estamos desarrollando nuestras aplicaciones habitualmente contamos con un único dispositivo físico donde probarlas (porque los dispositivos virtuales creados con el emulador se nos quedan cortos muy rápido). En este dispositivo en el que probamos vemos el aspecto y ajustamos las dimensiones de nuestros objetos en pantalla hasta que vemos cómo queda mejor. Pongamos mi caso, yo uso un HTC Desire que tiene una resolución de pantalla de 800×480. Si usamos un radio como número entero de píxeles (valores absolutos), ¿qué aspecto tendrá en resoluciones más pequeñas? ¿y en resoluciones más grandes?

Por tanto, la forma de solucionar esto es comenzar ajustando los tamaños en valores absolutos para, una vez hayamos encontrado el aspecto deseado, modificar estos valores para hacerlos relativos al ancho o alto de la pantalla. Haciendo esto conseguiremos que el aspecto sea el mismo en cualquier dispositivo: en un dispositivo con menor resolución todo será mas pequeño pero mantendrá el aspecto, en un dispositivo con resolución más grande veremos todo más grande pero igualmente en la misma relación.

Después de esta pequeña nota de aviso, podemos ejecutar nuestra aplicación y comprobar que funciona como queríamos. Vamos a hacer lo mismo para un segundo dedo, comenzamos añadiendo un nuevo atributo a la clase similar al que teníamos:

private int[] coordenadasDedo2 = {-1, -1};

Lo siguiente es modificar el método onTouchEvent(). El Android SDK está preparado para aceptar hasta 256 dedos en pantalla (a partir de ahora los llamaremos punteros, no confundir con programación), pero el hardware actual no soporta más de 4 punteros en el mejor de los casos. De hecho, si miramos en nuestro proyecto, el Android SDK sólo nos va a ofrecer control para 3 punteros. Podríamos crear el siguiente código, por tanto:

@Override
public boolean onTouchEvent(MotionEvent event) {
	switch(event.getAction()) {
	case MotionEvent.ACTION_DOWN:
		coordenadasDedo1[0] = (int) event.getX();
		coordenadasDedo1[1] = (int) event.getY();
		break;
	case MotionEvent.ACTION_POINTER_2_DOWN:
		coordenadasDedo2[0] = (int) event.getX();
		coordenadasDedo2[1] = (int) event.getY();
		break;
	case MotionEvent.ACTION_UP:
		coordenadasDedo1[0] = -1;
		coordenadasDedo1[1] = -1;
	case MotionEvent.ACTION_POINTER_2_UP:
		coordenadasDedo2[0] = -1;
		coordenadasDedo2[1] = -1;
		break;
	}

	return true;
}

Sin embargo, con este código sólo vamos a conseguir detectar la posición donde pulsamos con el primer dedo. De hecho, si modificamos el método onDraw() de la siguiente forma podéis comprobarlo vosotros mismos:

@Override
protected void onDraw(Canvas canvas) {
	Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
	paint.setColor(Color.RED);

	canvas.drawColor(Color.BLACK);

	if(coordenadasDedo1[0] >= 0 && coordenadasDedo1[1] >= 0) {
		canvas.drawCircle(coordenadasDedo1[0], coordenadasDedo1[1], getHeight()/6, paint);
	}
	if(coordenadasDedo2[0] >= 0 && coordenadasDedo2[1] >= 0) {
		canvas.drawCircle(coordenadasDedo2[0], coordenadasDedo2[1], getHeight()/6, paint);
	}
}

Una vez hemos comprobado que esto no funciona como queremos, es momento de modificar por completo el método onTouchEvent(), aunque vamos a dejar el método onDraw() ya así.

Cuando un puntero ha sido detectado en pantalla se guarda el identificador de dicho puntero para saber qué puntero ha sido el que ha realizado la acción. Cuando intentamos obtener las coordenadas del evento podemos usar event.getX() o event.getX(int pointerID) para obtener la coordenada X por ejemplo. Nuestro objetivo por tanto es obtener ese identificador. Comenzamos recodificando la función de esta forma:

@Override
public boolean onTouchEvent(MotionEvent event) {
	int pointerID = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;

	return true;
}

Con esto obtendremos un 0 en caso de haber pulsado con un dedo, 1 en caso de haber detectado un segundo dedo. En mi HTC Desire no está soportado un tercer o cuarto dedo, así que aunque toque con más dedos no los detectaré. ¡Os invito a probar cuántos dedos soporta vuestro dispositivo y a que lo comentéis al final del artículo!

Ahora modificamos el método de nuevo para funcionar con este nuevo identificador:

@Override
public boolean onTouchEvent(MotionEvent event) {
	int pointerID = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;

	if(event.getAction() != MotionEvent.ACTION_MOVE)
		Log.d("Colorshapes", "Event: " + event.getAction() + ", ID: " + pointerID);

	switch(event.getAction()) {
	case MotionEvent.ACTION_DOWN:
	case MotionEvent.ACTION_POINTER_DOWN:
	case MotionEvent.ACTION_POINTER_2_DOWN:
	case MotionEvent.ACTION_MOVE:
		if(pointerID == 0) {
			coordenadasDedo1[0] = (int) event.getX(pointerID);
			coordenadasDedo1[1] = (int) event.getY(pointerID);
		} else {
			coordenadasDedo2[0] = (int) event.getX(pointerID);
			coordenadasDedo2[1] = (int) event.getY(pointerID);
		}
		break;
	case MotionEvent.ACTION_POINTER_UP:
	case MotionEvent.ACTION_POINTER_2_UP:
		if(pointerID == 0) {
			coordenadasDedo1[0] = -1;
			coordenadasDedo1[1] = -1;
		} else {
			coordenadasDedo2[0] = -1;
			coordenadasDedo2[1] = -1;
		}
		break;
	case MotionEvent.ACTION_UP:
		coordenadasDedo1[0] = -1;
		coordenadasDedo1[1] = -1;
		coordenadasDedo2[0] = -1;
		coordenadasDedo2[1] = -1;
		break;
	}

	return true;
}

Aquí vemos varias cosas. La primera es que cuando pulsamos con el primer dedo se activará el evento ACTION_DOWN y que cuando pulsemos con un segundo dedo se dispara el evento ACTION_POINTER_2_DOWN. Si ahora, por ejemplo, levantamos el primer dedo y volvemos a pulsar sin haber soltado el segundo, dispararemos el evento ACTION_POINTER_DOWN. Podríamos haber hecho una elección de los datos más eficiente para no tener que haber usado un if para diferenciar el pointerID, sino haber accedido diréctamente con él en un array, por ejemplo.

Si hemos pulsado con los dos dedos, al levantar el último con el que pulsamos disparamos el evento ACTION_POINTER_2_UP. Sin embargo, si lo que hicimos fue levantar el primer dedo y no el segundo, disparamos el ACTION_POINTER_UP. Cuando hayamos levantado todos los dedos de la pantalla, el último (sea el que sea) disparará el evento ACTION_UP con identificador 0. Por esta razón en el ACTION_UP ponemos todos los valores al valor nulo.

Por último está la acción ACTION_MOVE. Ésta se dispara cuando movemos un puntero y lo que queremos es recoger dicha nueva posición y asignarla. En mi HTC Desire (y en los Nexus One, desconozco si en algún otro), existe un fallo conocido con esta parte. Cuando nosotros tenemos pulsados dos dedos en pantalla, el evento ACTION_MOVE sólo se dispara con el puntero que primero tocó la pantalla independientemente de qué puntero sea el que se mueve. El bug es tan grande que si levantamos el primer puntero de la pantalla con el segundo aún pulsado se producen situaciones extrañas. ¡Os invito a probarlo en vuestros dispositivos y contarnos vuestra experiencia en los comentarios! ¡Sobre todo si usáis otros dispositivos diferentes a los nombrados!

Detectando zoom in y zoom out

Un gesto muy común cuando usamos dos dedos es el de acercar o alejar los dedos con la intención de ampliar o reducir una imagen, un mapa… en definitiva el tamaño de un objeto. No es objetivo de esta entrada explicar cómo transformar una imagen para ampliarla o reducirla, por ejemplo; pero sí es objetivo el detectar que estamos generando este gesto.

Va a ser un procedimiento muy sencillo, ya que sólo tenemos que detectar dos cosas: tenemos pulsados dos dedos y ver qué distancia hay entre ellos en cada momento.

Los pasos serán:

  • Si hemos pulsado con dos dedos, almacenamos la distancia entre ambos.
  • Si al moverlos la distancia es menor que la inicial, estamos generando un gesto de reducción (zoom out).
  • Si al moverlos la distancia es mayor que la inicial, estamos generando un gesto de ampliación (zoom in).

Las formas de almacenar estos estados son muchas y muy variadas, pasando por enumerados o constantes, por estados… para hacerlo muy simple, aunque sea feo, vamos a crear los siguientes dos atributos a la clase:

private double distancia;
private double gesto = 0;

La variable distancia la vamos a usar cuando pulsemos con dos dedos y no necesitamos reinicializarla en ningún momento. Por su parte, la variable gesto tendrá tres posibles valores: el nulo que es el 0 e implica que no estamos ni haciendo ampliación ni reducción, valores positivos que implican que estamos intentando ampliar y valores negativos que implican que intentamos reducir. Lo primero es modificar el método onDraw() para hacer visible esto:

@Override
protected void onDraw(Canvas canvas) {
	Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

	if(gesto == 0)
		paint.setColor(Color.RED);
	if(gesto > 0)
		paint.setColor(Color.GREEN);
	if(gesto < 0)
		paint.setColor(Color.BLUE);

	canvas.drawColor(Color.BLACK);

	if(coordenadasDedo1[0] >= 0 && coordenadasDedo1[1] >= 0) {
		canvas.drawCircle(coordenadasDedo1[0], coordenadasDedo1[1], getHeight()/6, paint);
	}
	if(coordenadasDedo2[0] >= 0 && coordenadasDedo2[1] >= 0) {
		canvas.drawCircle(coordenadasDedo2[0], coordenadasDedo2[1], getHeight()/6, paint);
	}
}

De modo que los círculos se pintarán en rojo si no hay gesto, verdes si es de ampliación y azules si el gesto es de reducción. Lo siguiente es hacer esta detección. Como ya tenemos una estructura del método onTouchEvent(), no quiero cambiarla demasiado para hacer la explicación más sencilla (y saber diferenciar por partes qué es para cada cosa), pero en la práctica fuera de la explicación habría que hacer una colocación inteligente del código. Modificamos el método de la siguiente forma:

@Override
public boolean onTouchEvent(MotionEvent event) {
	int pointerID = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;

	switch(event.getAction()) {
	case MotionEvent.ACTION_DOWN:
	case MotionEvent.ACTION_POINTER_DOWN:
	case MotionEvent.ACTION_POINTER_2_DOWN:
	case MotionEvent.ACTION_MOVE:
		if(pointerID == 0) {
			coordenadasDedo1[0] = (int) event.getX(pointerID);
			coordenadasDedo1[1] = (int) event.getY(pointerID);
		} else {
			coordenadasDedo2[0] = (int) event.getX(pointerID);
			coordenadasDedo2[1] = (int) event.getY(pointerID);
		}
		break;
	case MotionEvent.ACTION_POINTER_UP:
	case MotionEvent.ACTION_POINTER_2_UP:
		if(pointerID == 0) {
			coordenadasDedo1[0] = -1;
			coordenadasDedo1[1] = -1;
		} else {
			coordenadasDedo2[0] = -1;
			coordenadasDedo2[1] = -1;
		}
		gesto = 0;
		break;
	case MotionEvent.ACTION_UP:
		coordenadasDedo1[0] = -1;
		coordenadasDedo1[1] = -1;
		coordenadasDedo2[0] = -1;
		coordenadasDedo2[1] = -1;
		break;
	}

	if(event.getPointerCount() == 2) {
		int x, y;

		switch(event.getAction()) {
		case MotionEvent.ACTION_POINTER_DOWN:
		case MotionEvent.ACTION_POINTER_2_DOWN:
			x = coordenadasDedo2[0] - coordenadasDedo1[0];
			y = coordenadasDedo2[1] - coordenadasDedo1[1];
			distancia = Math.sqrt(x*x + y*y);
			break;
		case MotionEvent.ACTION_MOVE:
			x = coordenadasDedo2[0] - coordenadasDedo1[0];
			y = coordenadasDedo2[1] - coordenadasDedo1[1];
			gesto = Math.sqrt(x*x + y*y) - distancia;
		}
	}

	return true;
}

La parte que está justo antes del return se encarga de esta gestión y cabe destacar la reinicialización a 0 de la variable gesto en la primera parte (si hemos levantado algún dedo ya no hay gesto posible). El código de gestión de la distancia sólo funciona cuando hay dos dedos pulsados y lo único que hace es, al pulsar ambos, guardar la distancia entre ellos y, posteriormente, generar el gesto al haber movimientos.

Aquí tenemos unas capturas de pantalla que muestran el funcionamiento, aunque recomiendo que lo probéis en vuestros propios dispositivos:

¡Ahora es momento de que pruebes tú! ¡Ejecuta en tu dispositivo para ver si hay fallos con tu hardware igual que lo hay con el mío! (de hecho, es probable que mi código necesite ser pulido por haberlo desarrollado con un dispositivo con bugs).

En esta entrada hemos visto cómo funcionan los eventos táctiles en dispositivos Android, así como la detección de eventos multitáctiles como el gesto de ampliación y el gesto de reducción. Además hemos visto que el tema de la entrada multitáctil es un tema muy delicado e incluso con bugs en dispositivos comerciales. Si necesitas utilizar la entrada multitáctil en tus aplicaciones, asegurate de probarlo a fondo (en varios dispositivos distintos a ser posible) y contemplar todos los casos.

¡Un saludo a todos!


Viewing all articles
Browse latest Browse all 4

Latest Images

Vimeo 10.7.0 by Vimeo.com, Inc.

Vimeo 10.7.0 by Vimeo.com, Inc.

HANGAD

HANGAD

MAKAKAALAM

MAKAKAALAM

Doodle Jump 3.11.30 by Lima Sky LLC

Doodle Jump 3.11.30 by Lima Sky LLC

Doodle Jump 3.11.30 by Lima Sky LLC

Doodle Jump 3.11.30 by Lima Sky LLC

Vimeo 10.6.2 by Vimeo.com, Inc.

Vimeo 10.6.2 by Vimeo.com, Inc.

Vimeo 10.6.1 by Vimeo.com, Inc.

Vimeo 10.6.1 by Vimeo.com, Inc.

Vimeo 10.6.0 by Vimeo.com, Inc.

Vimeo 10.6.0 by Vimeo.com, Inc.

Re:

Re:





Latest Images

Vimeo 10.7.0 by Vimeo.com, Inc.

Vimeo 10.7.0 by Vimeo.com, Inc.

HANGAD

HANGAD

MAKAKAALAM

MAKAKAALAM

Doodle Jump 3.11.30 by Lima Sky LLC

Doodle Jump 3.11.30 by Lima Sky LLC

Doodle Jump 3.11.30 by Lima Sky LLC

Doodle Jump 3.11.30 by Lima Sky LLC

Vimeo 10.6.1 by Vimeo.com, Inc.

Vimeo 10.6.1 by Vimeo.com, Inc.

Vimeo 10.6.0 by Vimeo.com, Inc.

Vimeo 10.6.0 by Vimeo.com, Inc.

Re:

Re:

Re:

Re: