Desarrollando Android #6 Añadiendo listeners y ActionBar
Hoy toca un tema sencillo pero esencial para el buen funcionamiento de la aplicación. Hemos de añadir listeners. También añadiremos una action Bar a nuestra aplicación con un par de botones.
La Action Bar
La Action Bar en android es la típica barra superior que vemos en muchísimas aplicaciones. Añadirla es muy sencilla y depende totalmente de nuestros intereses. Como mi aplicación no tendrá muchos botones/opciones, voy a añadir un par de botones a esta barra que comentaré más al final. Veamos como implementarla.
AndroidManifest.xml
Para escoger el color y diseño de la barra rápidamente, en nuestro AndroidManifest.xml definiremos el tema de nuestra aplicación.
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.Holo.Light" >
Hay unas cuantas combinaciones: Holo Light (blanco), Holo Dark (negro) o Holo Light DarkActionBar (aplicación blanca con barra negra). Y para Android 3.0 o superior se puede incluso personalizar el color.
Main.xml
A continuación, en res/menu/ hemos de definir un fichero main.xml (o el nombre que queramos) donde definiremos el contenido de nuestra ActionBar
Como podéis ver, defino un primer item llamado action_reload que me permitirá recargar los datos en un futuro. El segundo item será un botón para acceder a los futuros ajustes. Con el tag: showAsAction="ifRoom" le decimos que nos muestre el icono si hay sitio en la barra, y si no, que muestre un texto en un menú desplegable. El icono lo definimos con el @drawable/XXXX. y poniendo dicha imagen en las carpetas res/drawableXXX.
MainActivity.java
Y finalmente, en nuestro programa hemos de añadir un par de cosillas. Básicamente, con el código siguiente le decimos que añada la barra con las opciones definidas en el Main.xml
Y ya tenemos nuestra ActionBar que de momento no hace nada.
Lógicamente hay más opciones para gestionar menús/links en una aplicación:
Si la ActionBar se os queda corta a nivel de personalización, probad SherlockActionBar
Otra opción que cada vez se ve con mayor asiduidad es la implementación de un menú lateral. Aquí más información al respecto.
Pasemos al siguiente punto.
Los Event Listeners
Un Event Listener, o listener a secas es un método o conjunto de acciones que esperan alguna acción para ser ejecutados. Esta acción puede ser un click del ratón, pulsar algún sitio en la pantalla, mover el teléfono y muchísimas cosas más. Nosotros tenemos muchos elementos en la pantalla en los cuales queremos que sucedan cosas:
Los botones de la ActionBar
Al pulsar encima de un marcador, nos lleve con el GPS hasta dicho lugar
Al pulsar encima de un parking de la lista, también nos lleve con el GPS.
Así pues, vamos a poner unos cuantos.
- Los botones de la ActionBar: Utilizaremos el método llamado onOptionsItemSelected, que detecta cuando seleccionamos un método de nuestra barra. A continuación detectamos qué botón hemos pulsado mirando su R.id. Y finalmente, le decimos qué tiene que hacer.
- Al pulsar un botón de la lista: utilizaremos el OnItemClickListener. Lo siguiente es un pelín complicado. Yo quería saber si el parking estaba libre o no, y por ello quiero recuperar una variable del objeto. Eso se hace llamando al adapter y recuperando el item en la posición que hemos pulsado. Y de todo ello recuperamos la variable que nos interesa. Luego podemos hacer lo que queramos con ella.
- Al pulsar en un marcador: con el setOnInfoWindowClickListener controlamos este caso. No tiene mucho secreto, con eventMarkerMap.get(marker) recuperamos el marcador y ya está. Lo interesante es el Intent que he añadido a continuación. Este trozo de código nos permite lanzar automáticamente el Google Navigation para que nos lleve con GPS a la dirección que digas.
- Finalmente, para hacerlo más sencillo y limpio, he recogido estos dos últimos listeners relacionado con los marcadores y la lista y los he puesto en un método llamado addListeners que invoco al añadir los parkings a la lista.
Como véis, el mundo de los listeners está lleno de posibilidades. Básicamente todos tienen la misma estructura: al realizar una acción se activa el método, recuperamos el valor o objeto que necesitamos y finalmente, lanzamos una acción.
En la próxima entrega os explicaré como hacer el menú de Opciones típico de Android, así que no os lo perdáis!
Os recuerdo que el código fuente del proyecto se encuentra en GitHub
¡Cualquier duda, sugerencia o cuestión me podéis encontrar en twitter!
Hoy vamos a tocar las listas. Antes de nada, muchísimas gracias a los artículos de Android Developers, Proyectosimio y de SGOliver que me han servido de inestimable ayuda.
Dividiendo la pantalla en dos
Bien, pues el siguiente paso para mejorar la aplicación es tener la lista de parkings también en un formato de lista para acceder rápidamente a los párkings. Lo ideal será dividir la pantalla en dos, por un lado dichos párkings en formato lista y por el otro en formato mapa.
Para ello vamos a tocar el layout. En res -> layout -> activity_main.xml vamos a cambiar lo que teníamos por lo siguiente.
Si nos fijamos, primero creamos un LinearLayout con un identificador @+id/top_half que va a rellenar todo. En el interior de este primer LinearLayout añadimos un ListView, lugar donde insertaremos nuestra lista. A continuación, en el siguiente LinearLayout insertamos el mapa tal y como lo teníamos hasta ahora.
Para conseguir que la pantalla se divida exactamente por la mitad utilizamos los weights (o pesos). Al poner la propiedad android:layout_weight="1" en ambos LinearLayout le estamos indicando que ambos tienen la misma importancia en la app, y por tanto, le da la mitad de espacio a cada uno. Si quisieramos que el mapa ocupase más, indicaríamos un peso mayor, por ejemplo "2". La regla que se sigue para distribuir el espacio es la siguiente:
Espacio que ocuparé = Peso / Suma de todos los pesos
Por ejemplo: Un linearlayout con peso 3 y otro con 1, ocuparán respectivamente: 3 / (3+1) = 75% y 1/ (3+1) = 25%. Si queréis más información sobre el tema os recomiendo este artículo y este otro.
Añadiendo la Lista
Diseño
Lo primero que hemos de hacer es definir correctamente el contenedor de esta lista en los layouts. ¿Pero eso no lo habíamos hecho ya? No. Para que lo entendamos, antes hemos divido en dos la pantalla diciendo que arriba vendría una lista. Ahora estamos definiendo el estilo de cada fila: su altura, espacios, los colores, el texto, etc..
Aquí me tiré varias horas para dar con el estilo que me gustase. Ya os aviso que es un infierno y es recomendable repasar las propiedades wrap_content y demás de los layout_width (aquí, aquí y aquí) y los pesos de los que hemos hablado. Básicamente, este diseño mío crea una lista en la que cada fila está compuesta de el Nombre del parking con el id LblTitle y un texto más pequeño indicando si está libre o no con el id LblStatus. Estos ids los utilizaremos más tarde y pueden tener el nombre que queráis..
Adapter
A continuación necesitamos un adapter (adaptador). ¿Por qué? Pues bien, nosotros tenemos una lista, sí. Pero, ¿de qué? Pues bien, se lo hemos de decir a Android, y eso lo haremos a través de este adapter. Para ello, en nuestra clase principal creamos una nueva clase que extiende el ArrayAdapter<nuestros_objetos>
Antes de nada, definimos el constructor por defecto que está compuesto de una Activity context y una lista de los objetos que recibimos, en mi caso los ParkingMarker. Y a continuación la clase que nos interesa: getView.
En getView vamos a definir los tipos de contenido que queremos que aparezca en nuestra lista y dónde queremos que estén. Así pues, lo primero que hacemos es definir un item de esta lista y le asignamos el diseño que hemos hecho antes con las dos primeras líneas.
A continuación, definimos los elementos de nuestro item:
asociamos al TextView lblTit (nombre inventado) el item del layouts.list_details.xml LblTitle que hemos definido en la etapa de diseño.
asociamos al TextView lblStat del mismo modo el item LblStatus.
además, miramos si el parking obtenido está libre o no, y en función le ponemos el color verde o rojo.
finalmente retornamos este item que hemos creado.
Añadiendo el código
Para implementar la lista, haremos lo siguiente.
1. Crearemos tres variables: lstOptions, adapter y parkings_data privadas a nivel de clase para que se puedan acceder desde cualquier subclase. La variable parkings ya existía de la entrega anterior.
//Creamos la lista de párkings
parkings = new ArrayList<ParkingMarker>();
//Añadimos nuestros parkings a la lista
parkings_data = new ArrayList<ParkingMarker>();
parkings_data = parkings;
adapter = new ParkingAdapter(MainActivity.this,parkings_data);
lstOptions = (ListView)findViewById(R.id.LstParkings);
lstOptions.setAdapter(adapter);
Esperando un tiempo
Y ahora viene una cuestión que hemos de tener en cuenta. Es importante fijarse que el parkings_data recoge la información de la variable parkings. Esta variable es la que tenemos compartida con los mapas que al parsear el archivos XML ha generado.
Así pues, si queremos asignárselo hemos de asegurarnos que la variable parkings esté llena. Pero como ésta se llena de forma asíncrona, hay que ir con cuidado: hemos de asegurarnos que la tarea de la clase asíncrona se ha acabado antes de llamar al adapter. Eso lo haremos utilizando el método get() de nuestra clase asíncrona. Después de la línea
Ahora toca una parte muy importante de el proyecto: parsear un XML. Según lo definido inicialmente, la gracia de toda esta aplicación es que yo recibiré un archivo .xml y a partir de aquí mostraré en el mapa las posiciones. Para ello, vamos a parsear este archivo primero y luego vamos a utilizar los datos para insertar marcadores en nuestro mapa.
Parsear un XML
Antes de nada, qué significa "parsear". El parseo es un proceso de análisis y posterior transformación de un bloque en un lenguaje/estructura determinada en otro bloqueo de lenguaje/estructura diferente. Básicamente lo que quiero es que mi programa sepa entender el archivo que le paso, y me detecte lo que son posiciones geográficas, lo que son títulos o el indicador de si está libre o no un parking.
Así que, antes de nada, he creado un archivo .xml que alamcena posiciones de parkings:
Este archivo no es nada más que un conjunto de objetos <parking> que contienen un nombre <name>, una latitud <lat>, una longitud <lng> y un indicador de si el parking está libre o no <free>. Ahora toca interpretarlo.
En Android se puede parsear un XML de muchas maneras: la que ellos proponen utilizando XMLPullParser, DOM o SAX entre muchos. Personalmente he probado de hacerlo de dos maneras diferentes, con el XMLPullParser y con SAX, y tras muchísimos muchísimos quebraderos de cabeza al final he optado por el SAX.
Nota: Muchísimas gracias a Salvador Gómez por su tutorial en el parseo con SAX, que ha sido de mucha utilidad.
Paso 1: La classe de objetos
El objetivo final del parseo es obtener una serie de objetos en el formato que queramos. Como yo quiero obtener parkings con los cuatro atributos mencionados anteriormente, lo primero que hay que hacer es crear la classe ParkingMarker:
Paso 2: El Handler
A medida que se parsea un documento se van produciendo eventos que se han de tratar. Estos eventos indican por ejemplo que se ha acabado una etiquetao que has llegado al final del documento entre otros.
Los principales eventos son:
startDocument(): comienza el documento XML.
endDocument(): termina el documento XML.
startElement(): comienza una etiqueta XML.
endElement(): termina una etiqueta XML.
characters(): fragmento de texto.
Así pues en nuestro código vamos a tratar todos ellos
Analicemos con detalle lo que hemos hecho. Primero hemos definido una lista de objetos parkings, un objeto parking y un objeto sbText. Este último sirve para ir almacenando el texto que se recupera del parseador.
En el startDocument, tan solo hay que instanciar los dos tipos de objeto y en startElement decimos que si nos encontramos con la etiqueta <parking> significa que empieza un nuevo objeto. La función characters tan solo va acumulando el texto para luego tratarlo.
Y la gracia de todo está en el endElement. Aquí definimos qué hay que hacer cuando llegas al final de un elemento. He definido en mi caso las siguientes situaciones:
Si estaba en la etiqueta <name>, almacena el contenido en el apartado name del ParkingMarker.
Si estaba en una de las etiquetas de <lat> o <lng> que lo almacene en su campo correspondiente
Si estaba en el campo <free>, que recoja el valor y si es un "1" entonces asignaremos el valor de TRUE al campo y FALSE si es el caso contrario.
He añadido un caso en el que si no viene ni un "1" o un "0" que indique que ha habido error en el parseo.
Paso 3. El Parser
Y finalmente el parseador. Esta clase recibe una URL con el XML y empieza a Parsearlo utilizando el ParkingHandler que hemos creado anteriormente.
Insertando el Parseador al proyecto y añadiendo puntos al mapa
Aquí hay un tema delicado que he aprendido a base de prueba y error. Desde la versión 3 de Android, no se permite a las aplicaciones ejecutar operaciones de larga duración en el hilo principal que puedan bloquear temporalmente la interfaz de usuario. Así pues, hay que crear una llamada asíncrona que lo hará en segundo plano.
En resumen, lo que toca hacer ahora es lo siguiente. En el método onCreate añadimos las siguientes líneas:
RetrieveFeed task = new RetrieveFeed();
task.execute("https://dl.dropboxusercontent.com/u/123539/parkingpositions.xml");
Y al final de la MainActivity.java, justo antes de cerrarla creamos una clase que extienda AsyncTask
Analicemos también con más detalle lo que pasa aquí. En el primer método doInBackground definimos lo que se realiza en segundo plano. En nuestro caso es crear un nuevo parseador con la URL dada y ejectuarlo.
Y en onPostExecute ponemos lo que se ha de hacer a continuación: tratamos la lista y para cada elemento (parking) de ella
recuperamos la latitud y la longitud
si el parking está libre definimos el color verde y si está ocupado el color rojo
finalmente añadimos un marcador en la posición recuperada, con el nombre (también recuperado) por título y el icono de un color u otro según su estado.
Como véis, no es excesivamente complicado pero os puedo asegurar que me estuve horas y horas para conseguir que funcionase. Ahora ya lo tenéis todo bien puesto y funcionando a la perfección.
Podéis ver el código del proyecto en gitHub
Ahora tenemos: Una aplicación que parsea un XML con posiciones y añade marcadores para cada una de ellas en un mapa.
En la próxima sesión: Dividir la pantalla en dos, peripecias.
Desarrollando Android #3 Centrando la localización y marcadores
Sigamos con el desarrollo de la aplicación. Ahora que ya hemos conseguido añadir un mapa hemos de pasar al siguiente punto: por un lado hay que conseguir centrar el mapa en mi posición y por el otro he de conseguir añadir marcadores encima.
Centrando el mapa
Hay diversas maneras de obtener una locaclización en Android. Pero hay que tener una cosa muy presente: la batería. Cuando se desarrolla una aplicación que implementa la localización del usuario hay que decidir cómo obtener dicha posición. Podemos utilizar el GPS y obtener una localización más exacta a cambio de gastar más batería o bien utilizar la posición por 3G, menos precisa pero más durarera.
Otro punto importante es que utilizamos los Google Services para mostrar el mapa, así pues sería interesante comprobar que están instalados en el teléfono y se puede conectar a ellos correctamente.
Finalmente, vamos a seguir los pasos sugeridos por Google y puntualizaciones realizadas por Javanator para establecer una correcta conexión a los Google Play Services y obtener nuestra prosición.
1. Especificando los permisos necesarios
Vamos a necesitar permisos uno o dos permisos en nuestra aplicación: ACCESS_COARSE_LOCATION o ACCESS_FINE_LOCATION en función de el grado de precisión que queremos tener. Pero básicamente, estos permisos ya los hemos añadido en la etapa anterior. En cualquier caso, en nuestro AndroidManifest.xml deberíamos tener el permiso:
2. Comprobando los Google Play Services
Ojo que vienen curvas. Vamos a comprobar el estado de los Google Services con un conjunto de métodos. Como seguramente otra aplicación o función de nuestra app quiera también comprobar este estado, vamos a encapsularlo todo en una método al que iremos llamando.
Este código va en la actividad principal de vuestro proyecto.
public class MainActivity extends FragmentActivity implements GooglePlayServicesClient.ConnectionCallbacks, GooglePlayServicesClient.OnConnectionFailedListener{ private SupportMapFragment mapFragment; private GoogleMap map; private LocationClient mLocationClient; /* * Define a request code to send to Google Play services * This code is returned in Activity.onActivityResult */ private final static int CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000; // Define a DialogFragment that displays the error dialog public static class ErrorDialogFragment extends DialogFragment { // Global field to contain the error dialog private Dialog mDialog; // Default constructor. Sets the dialog field to null public ErrorDialogFragment() { super(); mDialog = null; } // Set the dialog to display public void setDialog(Dialog dialog) { mDialog = dialog; } // Return a Dialog to the DialogFragment. @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return mDialog; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); mLocationClient = new LocationClient(this, this, this); mapFragment = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)); map = mapFragment.getMap(); map.setMyLocationEnabled(true); } /* * Called when the Activity becomes visible. */ @Override protected void onStart() { super.onStart(); // Connect the client. if(isGooglePlayServicesAvailable()){ mLocationClient.connect(); } } /* * Called when the Activity is no longer visible. */ @Override protected void onStop() { // Disconnecting the client invalidates it. mLocationClient.disconnect(); super.onStop(); } /* * Handle results returned to the FragmentActivity * by Google Play services */ @Override protected void onActivityResult( int requestCode, int resultCode, Intent data) { // Decide what to do based on the original request code switch (requestCode) { case CONNECTION_FAILURE_RESOLUTION_REQUEST: /* * If the result code is Activity.RESULT_OK, try * to connect again */ switch (resultCode) { case Activity.RESULT_OK: mLocationClient.connect(); break; } } } private boolean isGooglePlayServicesAvailable() { // Check that Google Play services is available int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); // If Google Play services is available if (ConnectionResult.SUCCESS == resultCode) { // In debug mode, log the status Log.d("Location Updates", "Google Play services is available."); return true; } else { // Get the error dialog from Google Play services Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog( resultCode, this, CONNECTION_FAILURE_RESOLUTION_REQUEST); // If Google Play services can provide an error dialog if (errorDialog != null) { // Create a new DialogFragment for the error dialog ErrorDialogFragment errorFragment = new ErrorDialogFragment(); errorFragment.setDialog(errorDialog); errorFragment.show(getSupportFragmentManager(), "Location Updates"); } return false; } } /* * Called by Location Services when the request to connect the * client finishes successfully. At this point, you can * request the current location or start periodic updates */ @Override public void onConnected(Bundle dataBundle) { // Display the connection status Toast.makeText(this, "Connected", Toast.LENGTH_SHORT).show(); Location location = mLocationClient.getLastLocation(); LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 17); map.animateCamera(cameraUpdate); } /* * Called by Location Services if the connection to the * location client drops because of an error. */ @Override public void onDisconnected() { // Display the connection status Toast.makeText(this, "Disconnected. Please re-connect.", Toast.LENGTH_SHORT).show(); } /* * Called by Location Services if the attempt to * Location Services fails. */ @Override public void onConnectionFailed(ConnectionResult connectionResult) { /* * Google Play services can resolve some errors it detects. * If the error has a resolution, try sending an Intent to * start a Google Play services activity that can resolve * error. */ if (connectionResult.hasResolution()) { try { // Start an Activity that tries to resolve the error connectionResult.startResolutionForResult( this, CONNECTION_FAILURE_RESOLUTION_REQUEST); /* * Thrown if Google Play services canceled the original * PendingIntent */ } catch (IntentSender.SendIntentException e) { // Log the error e.printStackTrace(); } } else { Toast.makeText(getApplicationContext(), "Sorry. Location services not available to you", Toast.LENGTH_LONG).show(); } } }
Vamos a analizar un poco lo que hemos puesto
Todo este código se introduce en la MainActivity. Primero añadimos unas cuantas variables que utilizaremos: el SupportMapFragment que ya vimos en la etapa anterior, un objeto GoogleMap y un objeto LocationClient. Además definimos un tiempo máximo que vamos a esperar a la conexión (CONNECTION_FAILURE_RESOLUTION_REQUEST). Pasado este tiempo, daremos por perdida la conexión y dejaremos de intentarlo. El bloque [public static class ErrorDialogFragment] nos muestra un diálogo, un mensaje informando de que no se ha podido establecer dicha conexión.
En el onCreate básicamente instanciamos al objeto LocationClient y activamos la locaclización del mapa. Cuando la actividad pasa a ser visible se llama al método onStart y le decimos que intente conectarse si los servicios de GooglePlay están disponibles. Del mismo modo, le decimos que se desconecte en el onStop.
En onActivityResult básicamente le diremos que intente conectarse si nos han comunicado que los Google Services funcionan correctamente. Información que llega del método isGooglePlayServicesAvailable que no tocaremos.
Finalmente, en el onConnected le decimos qué ha de hacer nuestro programa una vez hemos establecido la conexión. En nuestro caso le decimos que muestre un mensaje por pantalla indicando que se ha conectado (con el Toast) y a continuación estamos centrando el mapa en nuestra posición. Así que básicamente, el trozo de código que centra nuestra vista en un mapa es:
Obetenemos nuestra posición y la almacenamos en location
Creamos un objeto tipo LatLng con dicha posición
Creamos una orden de actualización de la cámara (CameraUpdate) y le decimos que se vaya a nuestra posición con un zoom de 17.
Finalmente ejecutamos esta orden, haciendo que se mueva la vista del mapa hasta nuestra situación actual.
Es interesante ver todas las cosas que podemos hacer con la cámara en Android.¡Podemos cambiar la posición, el zoom, la orientación e incluso el ángulo! Mucha más información al respecto en Changing the View.
Ahora que tenemos centrado nuestro mapa, vamos a añadir un marcador.
Añadir un marcador en el mapa
Muy muy sencillo. El código necesario para añadir un marcador es el siguiente:
mMap.addMarker (new MarkerOptions()
.position(new LatLng(lat, lng))
.title("El titulo que quieras"));
Así pues, basta con añadir este trozo de código en nuestro método onCreate para añadir un marcador. Por descontado, estos marcadores se pueden personalizar, rotar, cambiar de color y millones de cosas más. Pasaos por Markers en Android para saber más sobre el tema.
Ahora tenemos: Un mapa que se carga, se centra en nuestra posición y muestra un marcador.
En la próxima sesión: Parseo de un .xml para añadir varios marcadores a un mapa.