[Ejemplos Android] Torres de Hanoi II

Día 1. Descripción del proyecto

El proyecto contiene tres clases:
  • HanoiGameActivity
    Extiende de android.app.Activity y representa la clase principal de la aplicación. Contiene view que mostrará el escenario del juego y atenderá el evento Touch para que el usuario interactúe con la aplicación.
  • HanoiGameView
    Extiende de android.view.View y será la encargada de pintar el escenario, discos y controlar la lógica del juego.
  • HanoiDiskShape
    Alojada dentro de HanoiGameView, se encarga de representar los discos que se apilan en cada varilla.
También muestro una imagen con algunas medidas que harán falta introducir cuando sea necesario pintar los discos en el escenario:

Día 2. Programación

HanoiGameActivity

void onCreate(Bundle savedInstanceState)

Se lanza al cargar el activity y se utiliza para inicializar y cargar la interfaz gráfica.

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
 
		// initialize Hanoi Game
		hanoiGame = new HanoiGameView(this, DEFAULT_NUMBER_OF_DISKS);
 
		// Hide the window title and top bar
		requestWindowFeature(Window.FEATURE_NO_TITLE);
 
		getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
                WindowManager.LayoutParams.FLAG_FULLSCREEN); 
 
		setContentView(hanoiGame);
	}

boolean onTouchEvent(MotionEvent event)

Recoge el evento Touch ACTION_DOWN e pasa el control a la lógica del juego para que determine la acción pertinente. En caso que el juego haya finalizado muestra un mensaje y reinicia la partida.

	@Override
	public boolean onTouchEvent(MotionEvent event) {
 
		if (event.getAction() == MotionEvent.ACTION_DOWN) {
 
			if (hanoiGame.onTouch(event.getX(), event.getY())) {
 
				// when game is completed, shows an alert and starts a new game
 
				Toast.makeText(this, "You win!!", Toast.LENGTH_LONG).show();
 
				hanoiGame.startGame(DEFAULT_NUMBER_OF_DISKS);
			}
		}
 
		return true;
	}

HanoiGameView

HanoiGameView(Context context, int _numberOfDisks)

	public HanoiGameView(Context context, int _numberOfDisks) {
		super(context);
 
		// loads the background image that contains the bottom wood and the
		// figure of the three rods
		Bitmap hanoiBackground = BitmapFactory.decodeResource(getResources(),
				R.drawable.hanoi_background);
		setBackgroundDrawable(new BitmapDrawable(hanoiBackground));
 
		startGame(_numberOfDisks);
	}

void startGame(int _numberOfDisks)

inicializa el contenido de las varillas con el número de discos pasado por parámetro.

	protected void startGame(int _numberOfDisks) {
 
		if (_numberOfDisks > MAX_DISKS) {
 
			throw new RuntimeException(ERROR_MAX_NUMBER_DISKS_EXCEEDED);
		}
 
		numberOfDisks = _numberOfDisks;
 
		// create the three rod objects and fill the left one with
		// the total number of disks
 
		leftRod = new Stack();
		middleRod = new Stack();
		rightRod = new Stack();
 
		for (int diskSize = numberOfDisks; diskSize >= 1; diskSize--) {
 
			leftRod.push(new HanoiDiskShape(diskSize));
		}
	}

void onDraw(Canvas canvas)

pinta el contenido de las tres varillas.

	@Override
	protected void onDraw(Canvas canvas) {
 
		// draw the disks of the three rods
 
		// the idea is to translate the canvas to the left
		// for each rod. inside of the loops, the canvas is translated
		// upward for each disk.
 
		// some numbers to understand the translations
		// first rod located at: x = 90px
		// first disk located at: y = 230px
		// distance between rods: x = 150px
		// distance between disks: y -= 25px
 
		canvas.translate(90, 230);
		canvas.save();
		for (HanoiDiskShape disk : leftRod) {
 
			disk.draw(canvas);
			canvas.translate(0, -25);
		}
		canvas.restore();
 
		canvas.translate(150, 0);
		canvas.save();
		for (HanoiDiskShape disk : middleRod) {
 
			disk.draw(canvas);
			canvas.translate(0, -25);
		}
		canvas.restore();
 
		canvas.translate(150, 0);
		canvas.save();
		for (HanoiDiskShape disk : rightRod) {
 
			disk.draw(canvas);
			canvas.translate(0, -25);
		}
		canvas.restore();
	}

boolean onTouch(float x, float y)

comprueba sobre cual varilla se ha pulsado y llama al método actionOnTouchedRod.

	public boolean onTouch(float x, float y) {
 
		boolean gameFinished = false;
 
		// limits to detect which rod has been touched
 
		int topLimit = 30;
		int bottomLimit = 250;
 
		int leftLimitLeftRod = 20;
		int rightLimitLeftRod = leftLimitLeftRod + 150;
 
		int rightLimitMiddleRod = rightLimitLeftRod + 150;
 
		int rightLimitRightRod = rightLimitMiddleRod + 150;
 
		if (y > topLimit && y < bottomLimit) { 			if (x > leftLimitLeftRod && x < rightLimitLeftRod) { 				gameFinished = actionOnTouchedRod(leftRod); 			} else if (x > rightLimitLeftRod && x < rightLimitMiddleRod) { 				gameFinished = actionOnTouchedRod(middleRod); 			} else if (x > rightLimitMiddleRod && x < rightLimitRightRod) {
 
				gameFinished = actionOnTouchedRod(rightRod);
			}
 
			// forces to redraw
			invalidate();
		}
 
		return gameFinished;
	}

boolean actionOnTouchedRod(Stack<HanoiDiskShape> touchedRod)

implementa la lógica del juego recibiendo por parámetro la varila que acaba de ser pulsada.

	private boolean actionOnTouchedRod(Stack touchedRod) {
 
		boolean gameFinished = false;
 
		// if there isn't any selected rod and touchedRod contains disks...
		if (rodWithDiskSelected == null &amp;&amp; touchedRod.size() &gt; 0) {
 
			touchedRod.lastElement().select();
			rodWithDiskSelected = touchedRod;
		}
		else if (rodWithDiskSelected != null) {
 
			// there is a rod with a disk selected, so...
 
			// if user has touched the same rod -&gt; unselect disk
			if (rodWithDiskSelected.equals(touchedRod)) {
 
				touchedRod.lastElement().unselect();
				rodWithDiskSelected = null;
 
			// if not, check if it's a valid move
			} else if (touchedRod.size() == 0
					|| (rodWithDiskSelected.lastElement().size &lt; touchedRod
							.lastElement().size)) {
 
				rodWithDiskSelected.lastElement().unselect();
				touchedRod.push(rodWithDiskSelected.pop());
				rodWithDiskSelected = null;
			}
		}
 
		// if all disks are in the middle or right rod, game finished!
		if (middleRod.size() == numberOfDisks
				|| rightRod.size() == numberOfDisks) {
 
			gameFinished = true;
		}
 
		return gameFinished;
	}

HanoiDiskGame

HanoiDiskShape(int _size)

Crea un disco del tamaño pasado por parámetro.

		public HanoiDiskShape(int _size) {
			super(new RoundRectShape(diskOuterRadius, null, null));
 
			this.unselect();
 
			this.size = _size * 25;
			this.setBounds(0, 0, this.size, 20);
		}

void select()

Colorea el disco de un tono transparente para que se note que está seleccionado.

		public void select() {
 
			this.getPaint().setColor(diskSelectedColor);
		}

void unselect()

Vuelva dejar el disco con el color por defecto.

		public void unselect() {
 
			this.getPaint().setColor(diskUnselectedColor);
		}

void onDraw(Shape shape, Canvas canvas, Paint paint)

Pinta el disco en el escenario.

		@Override
		protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
 
			canvas.save();
 
			// translates the half of the size to the left, to draw
			// the disk on the center of the rod
			canvas.translate(-size / 2, 0);
			shape.draw(canvas, paint);
 
			canvas.restore();
		}

Anexo

Hay un par de detalles que quedan fuera del código Java y faltaría mencionar.

Forzar Activity en landscape

Para evitar que el activity, HanoiGameActivity en nuestro caso, se gire según si el móvil está en horizontal en vertical, se puede forzar que siempre esté en modo horizontal (landscape) o vertical (portrait). Para esta aplicación nos interesa que siempre se encuentre en posición horizontal para que haya el máximo espacio para manipular los discos. Esto se consigue editando el fichero de configuración AndroidManifest.xml. Localizamos el tag activity que queremos ajustar la posición y lo modificamos añadiendo el valor:

android:screenOrientation="landscape"

Girar orientación emulador

Por útlimo, también es posible girar la vista del emulador para que coincida con la orientación landscape que necesitamos para esta aplicación. Esto se puede hacer presionando la combinación de teclas CTRL-F11. Hay muchas más opciones disponibles en el emulador, para más info: http://developer.android.com/guide/developing/tools/emulator.html

Código fuente completo

Aquí puedes descargar el código fuente en formato .zip: HanoiTowers.zip

También se puede encontrar en el repositorio SVN de: http://code.google.com/p/android-tutorials/. Aunque es posible que contenga modificaciones con respecto a lo aquí mostrado.

Conclusiones

Más que conclusiones autocrítica. Me parece que sirve de poco copy-pastear el código si no he explicado nada. No quedo para nada satisfecho con dejarlo tal cual. Espero que el código fuente sirva a alguien aunque no será gracias a mis explicaciones porque no las hay. Para la próxima o bien hago otro desarrollo enfocando desde el principio que lo único que ofreceré será el codigo fuente sin explicaciones (que no creo que tampoco sea insuficiente) o intentaré abarbar un tema concreto (por ejemplo trabajar con las bases de datos) y en lugar de hacer una aplicación haré pequeños códigos que expliquen los conceptos relacionados al tema.

[Ejemplos Android] Torres de Hanoi I

Introducción a la serie a.k.a. rollo

Después de mucho tiempo siguiéndole la pista al desarrollo Android, sigo con la sensación de que tengo poca práctica y muchas lagunas que me gustaría despejar. Por ese motivo empiezo en este post lo que espero sea una serie de proyectos para Android con la intención que sirvan como ejemplos para aquellos que como yo queremos aprender a desarrollar aplicaciones móviles sobre esta plataforma. La idea consiste en desarrollar aplicaciones sencillas que cubran diferentes aspectos del SDK sirviendo como ejercicios de aprendizaje.

Este tipo de ejercicios estarán orientados a quien tenga unos conceptos básicos de programación y se haya mirado un poco sobre la plataforma Android. Seguramente no sea muy extenso en las explicaciones ya que me cuesta bastante documentar, soy más de acción. A cambio procuraré que el código sea lo más legible y sencillo posible. Cualquier duda pueden levantar la mano, aunque parece que se han quedado muchas sillas libres en esta sesión…

Día 0. Introducción

El ejemplo a desarrollar es el conocido juego matemático llamada las Torres de Hanoi, más rollo aquí. En resumen, tres palos (rods) que albergan un montón de discos apilados dispuestos de mayor a menor diámetro. El objetivo es desplazar el montón del palo inicial a un palo diferente respetando que el disco sobre el que se coloque sea mayor que el disco desplazado. ¿No está claro verdad? eso es porque no te fuiste al enlace de la wikipedia, segundo y último intento.

Trabajaré sobre la versión 1.5 del SDK de Android que a día de hoy aún somos un 31.0% de pringadetes.

Screenshots

Algunas capturas del desarrollo final:

Temas de interés

Los puntos de interés que se cubrirán en el desarrollo son:

  • trabajar con vistas
  • gráficos 2D, extendiendo vistas
  • modo landscape
  • rotar emulador
  • touch event
  • mostrar alerts con Toast
  • modo fullscreen

Esto no significa que vaya a comentar en detalle  estos conceptos, explicaré cómo los he usado y poco más. No es porque no hayan pagado para entrar (que tampoco), si no que eso es lo único que conozco.

Para ir acabando el día 0, aquí van los recursos de referncia que utilizo para resolver mis dudas:

  • el ejemplo ApiDemos que viene dentro del SDK de Android. Muchas de las cosas que se me ocurren están ya implementadas de alguna manera en esta fantástica guía práctica. Ideal para conocer cómo se usa una clase o método de manera práctica.
  • listas de correo oficiales de android, http://developer.android.com/resources/community-groups.html#ApplicationDeveloperLists. Seguramente que donde te encuentres atascado ya lo han estado otros.
  • stackoverflow, el oráculo de Delfos para los programadores. Hace no mucho google anunció que stackoverflow sería un lugar oficial para resolver dudas sobre Android.

Ya lo último, el código será alojado el un repositorio SVN dentro del servicio de hosting de Google, aquí http://code.google.com/p/android-tutorials/. Aquí se podrá descargar la última versión (por si hubiera correciones a lo explicado en el blog) de los ejemplos, aunque también dejaré el empaquetado del código tal cual lo haya explicado en el último post del ejemplo.

Ea.