Una de las cosas más importantes que tienen que cumplir nuestras aplicaciones, si no la que más, es la posibilidad de que el usuario interactue con ella (a no ser que queramos que no sea así). Esta interacción puede ser de muchas formas: pulsaciones de teclas, eventos de ratón, eventos de sensores (si el dispositivo dispone de ellos)…
En el caso de la entrada de hoy, vamos a centrarnos en dispositivos con Android. Quiero avisar previamente de que esta entrada no va a hablar de cómo instalar el SDK de Android, ni cómo crear un proyecto, ni va a tratar de la teoría de qué es una actividad o un layout. Eso lo vamos a dejar reservado para un futuro no muy lejano, en el que explicaremos en sucesivas entradas el desarrollo de una aplicación desde cero (y cero significa sin el SDK instalado siquiera).
En el caso que ocupa a esta entrada y la siguiente, vamos a centrarnos en un objetivo claro: queremos pintar figuras en pantalla y poder moverlas con el dedo.
Vamos a utilizar para esta entrada un Canvas como elemento donde pintar las figuras. Dicho Canvas será el lienzo que recibirá las llamadas a la función de pintado, las cuales son propias de una vista y tendremos que elegir qué vista es la más apropiada para nosotros.
View y SurfaceView son nuestros candidatos. La principal diferencia entre ambas es que SurfaceView está diseñado para ser más eficiente con el renderizado de formas e imágenes. Así que vamos a usar un SurfaceView.
Lo primero que necesitamos (después de crear un nuevo proyecto de Android) es crear una nueva clase que va a ser nuestro SurfaceView. Además vamos a hacer que esta clase implemente la interfaz SurfaceHolder.Callback para poder recibir eventos relacionados con el SurfaceView (tales como creación, modificación y destrucción). El código tendrá la siguiente pinta:
package com.vidasconcurrentes.dragdropcanvas; import android.content.Context; import android.view.SurfaceHolder; import android.view.SurfaceView; public class DragAndDropView extends SurfaceView implements SurfaceHolder.Callback { public DragAndDropView(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) { } @Override public void surfaceDestroyed(SurfaceHolder arg0) { } }
Como vemos en este esqueleto, el constructor es obligado por SurfaceView, y las funciones surfaceChanged(), surfaceCreated() y surfaceDestroyed() vienen de la interfaz SurfaceHolder.Callback. Hay que añadir en el constructor la línea getHolder().addCallback(this) para que se use esta clase como el manejador.
Hagamos una ejecución temprana. Vamos a modificar la clase que crea el IDE por defecto de la siguiente forma:
package com.vidasconcurrentes.dragdropcanvas; import android.app.Activity; import android.os.Bundle; import android.view.Window; import android.view.WindowManager; public class DragAndDropCanvas extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new DragAndDropView(this)); } }
Lo que obtenemos con esto es:
Vamos a modificar el código anterior para usarlo a partir de ahora de modo que no mostremos la barra de notificaciones, pero tampoco el nombre de la actividad:
package com.vidasconcurrentes.dragdropcanvas; import android.app.Activity; import android.os.Bundle; import android.view.Window; import android.view.WindowManager; public class DragAndDropCanvas extends Activity { /** Called when the activity is first created. */ @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 DragAndDropView(this)); } }
Bien, ahora necesitamos hacer que esto pinte cosas. Vamos a crear un hilo de ejecución nuevo que sea el encargado de hacer el pintado, pues SurfaceView está preparado para que varios hilos hagan operaciones sobre él. De esta forma, creamos una clase que hereda de Thread:
package com.vidasconcurrentes.dragdropcanvas; import android.graphics.Canvas; import android.view.SurfaceHolder; public class DragAndDropThread extends Thread { private SurfaceHolder sh; private DragAndDropView view; private boolean run; public DragAndDropThread(SurfaceHolder sh, DragAndDropView 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); } } } }
Es decir, cuando creamos el hilo y decimos que empiece a ejecutar, mientras la variable run sea true va a pintar contínuamente. Ahora no vamos a discutir si esto derrocha recursos para mantener la explicación simple, así que símplemente tomamos esto como válido para el ejemplo. Es importante la sección del finally, pues si hay algún error deberíamos desbloquear el Canvas antes de continuar. Es decir, debemos devolverlo a un estado estable.
Aquí es cuando nos damos cuenta de que no hemos creado nuestra función onDraw() en el SurfaceView, y si hemos seguido los pasos tal y como hemos descrito, el propio IDE deberá decirnos que onDraw() no es visible. Efectivamente, no es que no exista, lo que pasa es que tenemos que redefinirlo. Es aquí donde de verdad vamos a pintar nuestras figuras.
De momento vamos a redefinir dicha función de la siguiente manera:
@Override public void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE); }
Si ahora mismo ejecutásemos de nuevo la aplicación, volveríamos a encontrar la pantalla en negro. Y claro, es que no hemos hecho que el hilo de pintado ejecute en ningún momento. Es momento de hacer que las funciones de nuestro SurfaceView contengan algo de código. Vamos por partes:
@Override public void surfaceCreated(SurfaceHolder arg0) { thread = new DragAndDropThread(getHolder(), this); thread.setRunning(true); thread.start(); }
Aquí vamos a crear el hilo en el momento en que creamos nuestro SurfaceView. Hemos de añadir esta variable thread como atributo de la clase, que no se nos olvide. Seguimos:
@Override public void surfaceDestroyed(SurfaceHolder arg0) { boolean retry = true; thread.setRunning(false); while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { } } }
Aquí lo que hacemos, básicamente, es parar el hilo para que se acabe y espere a que acabe el hilo principal.
Si con esto hacemos una ejecución, vamos a obtener lo siguiente:
Ahora vamos a comenzar a pintar las cosas de verdad. Por ejemplo, pintemos un cuadrado y un rectángulo. Para ello nos vamos a servir de las funciones que nos ofrecen los Canvas:
@Override public void onDraw(Canvas canvas) { Paint p = new Paint(); p.setColor(Color.BLACK); p.setAntiAlias(true); canvas.drawColor(Color.WHITE); canvas.drawCircle(200, 200, 100, p); canvas.drawRect(200, 500, 400, 700, p); }
Creamos un objeto de la clase Paint, que va a ser quien dé formato a las formas que pintemos. Después pintamos el color blanco del fondo, y luego un círculo y un cuadrado. Los parámetros del círculo son las coordenadas (x,y) del centro (y = 0 es la esquina superior izquierda) y el radio. Los parámetros del rectángulo son las coordenadas del punto de abajo a la izquierda y las del punto de arriba a la derecha. Una ejecución de esto nos dará lo siguiente:
Recordemos que la función onDraw() se va a llamar contínuamente, así que esto se va a estar pintando muchas veces por segundo. Si quisiéramos hacer una animación (pero esta entrada no trata de eso), tendríamos que modificar la posición de cada elemento y luego pintar.
En esta entrada hemos visto cómo crear una aplicación de prueba en la que pintar figuras primitivas como círculos o rectángulos. En la siguiente entrada veremos cómo moverlos con el dedo.
El código fuente completo usado para esta entrada se encuentra como siempre en nuestro repositorio.
Más información:
Android resources – Lunar Lander