Vous êtes sur la page 1sur 24

Taller Android: Aplicacin Bicicas

En este documento se va a describir los pasos para desarrollar de forma una iterativa una
sencilla aplicacin para consultar el listado de puntos bicicas de Castelln y poder mostrar en
tiempo real su disponibilidad. Los datos de los puntos y bicis se obtendrn mediante un XML
disponible en los servidores de http://www.cuatroochenta.com.
Las fases del desarrollo van a ser las siguientes:
1. Crear pantalla de Splash y Men
2. Crear lista de puntos
3. Crear mapa con puntos
4. Aadir recomendar a un amigo (envo de correo) y mejorar el rendimiento de la lista
de puntos bicicas



Fase 1. Pantalla de Splash y Men
Crear un proyecto Android utilizando como SDK base Android 1.6 con las APIs de Google para
poder utilizar el componente MapView.
Nombre de aplicacin Bicicas
Paquete com.cuatroochenta.bicicas
Activity SplashActivity

El proyecto en este momento tiene una nica actividad que muestra un Hello World. Vamos a
crear una carpeta res/drawable y vamos a aadir el fichero splash.png a esta carpeta.
Idealmente deberamos contar con tres iconos con tamaos adaptados para las tres gamas de
dpis de Android.
El siguiente paso va a ser crear un fichero de layout para la pgina de Splash. Vamos a borrar el
fichero res/layout/main.xml y en su lugar crearemos uno con nombre splash.xml y con el
contenido que hay a continuacin:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:background="#000000">

<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#000000"
android:layout_gravity="center">

<ImageView android:id="@+id/splash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/splash"
android:layout_gravity="center">
</ImageView>

<TextView
android:id="@+id/splashCargandoText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#FFFFFFFF"
android:layout_marginTop="80dip"
android:text="@string/cargando_aplicacion"
/>
</LinearLayout>
</LinearLayout>

Veremos que el proyecto nos muestra un error. En los ficheros de layout se puede referenciar
a otros recursos disponibles en el proyecto. Un ejemplo de ello es la imagen splash que hemos
copiado previamente (no se tiene en cuenta la extensin). Otra posibilidad es la de definir
cadenas de forma que se pueda localizar la aplicacin. El TextView del ejemplo requiere de una
cadena cargando_aplicacion. Estas cadenas se definen en el fichero
res/values/strings.xml. Vamos a irnos a la vista XML de este fichero y aadimos:
<string name="cargando_aplicacion">Cargando aplicacin\nPor favor espere...</string>
Borraremos la cadena hello pues no se va a utilizar en este ejemplo.
Por ltimo la actividad SplashActivity referencia al antiguo layout que hemos borrado. Sustituir
la lnea de error por
setContentView(R.layout.splash);
La aplicacin ya puede ser ejecutada en este punto.
El siguiente paso ser crear una segunda actividad que represente el men. Creamos una clase
MenuActivity al mismo nivel que SplashActivity con el siguiente contenido:
public class MenuActivity extends Activity implements OnClickListener {

private Button botonListaPuntos;
private Button botonMapaPuntos;
private Button botonRecomendarAmigo;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

//Cargo el layout del men de la aplicacin
setContentView(R.layout.menu);

//Registro los listeners para los tres botones
//El primer paso es obtenerlos por su id
botonListaPuntos = (Button)findViewById(R.id.boton_lista_puntos);
botonMapaPuntos = (Button) findViewById(R.id.boton_mapa_puntos);
botonRecomendarAmigo = (Button) findViewById(R.id.boton_recomendar_amigo);

botonListaPuntos.setOnClickListener(this);
botonMapaPuntos.setOnClickListener(this);
botonRecomendarAmigo.setOnClickListener(this);
}

@Override
public void onClick(View v) {
if (v == botonListaPuntos){

}
else if (v == botonMapaPuntos){

}
else if (v == botonRecomendarAmigo){

}
}
}

Nos har falta definir el fichero de layout. Lo creamos en res/layout/men.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>


<ImageView android:id="@+id/splash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/titulo_menu"
android:layout_marginTop="30dip"
android:layout_gravity="center">
</ImageView>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_subtitulo"
android:textSize="20dip"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="30dip"
/>

<Button
android:id="@+id/boton_lista_puntos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_lista_puntos"
android:layout_gravity="center_horizontal"
/>

<Button
android:id="@+id/boton_mapa_puntos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_mapa_puntos"
android:layout_gravity="center_horizontal"
/>

<Button
android:id="@+id/boton_recomendar_amigo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_recomendar_amigo"
android:layout_gravity="center_horizontal"
/>




</LinearLayout>

De nuevo obtendremos varios errores. Por un lado nos falta una imagen y por otro cuatro
strings. Aadimos el fichero titulo_menu.png a la carpeta res/drawable y al final del fichero
res/values/strings.xml agregamos cuatro nuevas cadenas de texto:
<!-- Pantalla de men -->
<string name="menu_subtitulo">En bicicleta...</string>
<string name="menu_lista_puntos">Lista de puntos</string>
<string name="menu_mapa_puntos">Mapa de puntos</string>
<string name="menu_recomendar_amigo">Recomendar a un amigo</string>

Por ltimo faltara que desde Splash se invocara a la pantalla de men. En posteriores fases
esto se producir una vez recuperados los datos del servidor. Por el momento un Thread.sleep
simular la comunicacin.
public class SplashActivity extends Activity {

//El handler es necesario para que desde un Thread
//fuera del loop de pintado se pueda actualizar la interfaz de usuario
//o invocar nuevos Intents
private Handler handler;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

handler = new Handler();
//Cargo el layout del men de la aplicacin
setContentView(R.layout.splash);

Thread cargaPuntosThread = new CargaPuntosThread();
cargaPuntosThread.start();

}

class CargaPuntosThread extends Thread{

@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}

handler.post(new Runnable() {

@Override
public void run() {
Intent mainIntent = new
Intent(SplashActivity.this,MenuActivity.class);
mainIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
SplashActivity.this.startActivity(mainIntent);
SplashActivity.this.finish();
}
});
}
}


}

La aplicacin se podr ejecutar ya pero


Vamos a buscar la pestaa LogCat en Eclipse y el error indica lo siguiente:
10-29 07:50:37.930: ERROR/AndroidRuntime(304):
android.content.ActivityNotFoundException: Unable to find explicit activity class
{com.cuatroochenta.bicicas/com.cuatroochenta.bicicas.MenuActivity}; have you declared
this activity in your AndroidManifest.xml?

En efecto no hemos dado de alta la Activity en el AndroidManifest.xml. Android requiere que
todas las actividades que se utilicen en una aplicacin se declaren en este fichero. Doble click
en el fichero y nos vamos a la pestaa AndroidManifest.xml dentro del editor y aadimos lo
siguiente justo debajo de la nica Activity declarada
<activity android:name=".activity.MenuActivity" android:label="@string/app_name">
</activity>

Volvemos a ejecutar y despus de mostrarse el Splash durante tres segundos nos mostrar la
pantalla de men:


Fin de la fase 1

Fase 2: Crear un listado de puntos
En esta segunda fase vamos a crear el modelo de datos, bsicamente una clase PuntoBicicas.
Inicialmente cargaremos el listado desde un fichero XML local y posteriormente lo
descargaremos desde Internet. Para visualizar la lista emplearemos un tipo especial de Activity
llamada ListActivity que muestra un listado de filas partiendo de un layout que tambin
definiremos. Manos a la obra
El XML con el listado de puntos ser como sigue:
<?xml version="1.0" encoding="UTF-8"?>
<puntos>
<punto>
<nombre>Universidad Jaime I</nombre>
<numero>1</numero>
<descripcion></descripcion>
<longitud>-0.069804</longitud>
<latitud>39.994492</latitud>
<disponibilidad>1111111101110000110111111111</disponibilidad>
</punto>
<punto>
<nombre>Estacin de Ferrocarril y Autobuses</nombre>
<numero>2</numero>
<descripcion></descripcion>
<longitud>-0.052770</longitud>
<latitud>39.987465</latitud>
<disponibilidad>1100001101010100010110111111</disponibilidad>
</punto>

</puntos>

La disponibilidad de cada bici se marca con un 1 (disponible) o un 0 (no disponible).
Para una lectura ms eficiente del fichero XML vamos a utilizar SAX. Nos evitar no recargar la
memoria del dispositivo levantando todo el fichero mediante un rbol DOM.
Crear la carpeta res/raw y copiar el fichero puntos.xml a esta carpeta.
Crear un paquete model dentro del proyecto y una clase PuntoBicicas.java con el siguiente
contenido:


public class PuntoBicicas {

private String nombre;
private String descripcion;
private int numeroPunto;
private float latitud;
private float longitud;
private boolean enlinea;
private boolean [] disponibilidad;

public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getDescripcion() {
return descripcion;
}
public void setDescripcion(String descripcion) {
this.descripcion = descripcion;
}
public int getNumeroPunto() {
return numeroPunto;
}
public void setNumeroPunto(int numeroPunto) {
this.numeroPunto = numeroPunto;
}
public float getLatitud() {
return latitud;
}
public void setLatitud(float latitud) {
this.latitud = latitud;
}
public float getLongitud() {
return longitud;
}
public void setLongitud(float longitud) {
this.longitud = longitud;
}
public boolean isEnlinea() {
return enlinea;
}
public void setEnlinea(boolean enlinea) {
this.enlinea = enlinea;
}
public boolean[] getDisponibilidad() {
return disponibilidad;
}
public void setDisponibilidad(boolean[] disponibilidad) {
this.disponibilidad = disponibilidad;
}

public int getNumeroBicicletasLibres() {
int i=0;
for (boolean libre:disponibilidad){
if (libre)
i++;
}
return i;
}
public int getNumeroBicicletasTotales() {
return disponibilidad.length;
}



}

Crear un paquete parser dentro del proyecto donde aadiremos una clase PuntosParser con el
siguiente cdigo:
public class PuntosParser extends DefaultHandler {

private static final String NODE_PUNTOS = "puntos";
private static final String NODE_PUNTO = "punto";
private static final String NODE_PUNTO_NOMBRE = "nombre";
private static final String NODE_PUNTO_NUMERO = "numero";
private static final String NODE_PUNTO_DESCRIPCION = "descripcion";
private static final String NODE_PUNTO_LONGITUD = "longitud";
private static final String NODE_PUNTO_LATITUD = "latitud";
private static final String NODE_PUNTO_DISPONIBILIDAD = "disponibilidad";


private String text;
private List<PuntoBicicas> puntos;
private PuntoBicicas currentPunto;

/**
* Mtodo esttico que parsea el XML pasado como parmetro
* y devuelve un array de puntos bicicas
* Para ello se utiliza SAX
* @param is
* @return
*/
public static List<PuntoBicicas> parsePuntos(InputStream is){

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);

SAXParser saxParser;
try {
saxParser = factory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
PuntosParser puntosParser = new PuntosParser();
xmlReader.setContentHandler(puntosParser);
xmlReader.parse(new InputSource(is));

return puntosParser.getPuntos();
} catch (Exception e) {
//e.printStackTrace();
return null;
}
}

private List<PuntoBicicas> getPuntos() {
return this.puntos;
}

public void startDocument() throws SAXException { this.puntos = new
ArrayList<PuntoBicicas>(); }
public void endDocument() throws SAXException { }

public void startElement(String namespaceURI, String localName, String qName,
Attributes atts) throws SAXException {
if (localName.equals(PuntosParser.NODE_PUNTOS)) {
}
else if (localName.equals(PuntosParser.NODE_PUNTO)) {
currentPunto = new PuntoBicicas();
puntos.add(currentPunto);
}
}


public void endElement(String namespaceURI, String localName, String qName)
throws SAXException {
if (localName.equals(NODE_PUNTO_NOMBRE))
currentPunto.setNombre(text);
else if (localName.equals(NODE_PUNTO_NUMERO))
currentPunto.setNumeroPunto(Integer.parseInt(text));
else if (localName.equals(NODE_PUNTO_DESCRIPCION))
currentPunto.setDescripcion(text);
else if (localName.equals(NODE_PUNTO_LONGITUD))
currentPunto.setLongitud(Float.parseFloat(text));
else if (localName.equals(NODE_PUNTO_LATITUD))
currentPunto.setLatitud(Float.parseFloat(text));
else if (localName.equals(NODE_PUNTO_DISPONIBILIDAD)){
boolean [] disponibilidad = new boolean[text.length()];
for (int i=0;i<text.length();i++){
disponibilidad[i]=text.charAt(i)=='1';
}
currentPunto.setDisponibilidad(disponibilidad);
}

}

public void characters(char ch[], int start, int length) {
text = new String(ch, start, length).trim();
}


}

Esta clase tiene un mtodo esttico al que se le pasa un InputStream con el XML a parsear y
devuelve un List<PuntoBicicas>
Todas las aplicaciones Android tienen una clase Application donde poder guardar informacin
global. No es necesario crear una pues el runtime la crea internamente por ti. En este caso nos
interesa guardar el listado de puntos para que sea visible tanto desde el listado como desde el
futuro mapa de puntos. Creamos la clase BicicasApplication en el paquete principal del
proyecto con el siguiente cdigo:

public class BicicasApplication extends Application {

private List<PuntoBicicas> puntosBicicas;
public boolean puntosCargados = false;
private boolean USE_LOCAL_FILE = true;

public List<PuntoBicicas> getPuntosBicicas() {
return puntosBicicas;
}

@Override
public void onCreate() {

}

public void loadPuntos() {
if (USE_LOCAL_FILE){
//Recupero el InputStream de los recursos
InputStream is = getResources().openRawResource(R.raw.puntos);
//Y lo parseo obteniendo un List<Punto>
puntosBicicas = PuntosParser.parsePuntos(is);

puntosCargados = true;
}
else{
//...
}

}


public boolean haCargadoPuntos() {
return puntosCargados;
}

}

Hemos incluido en la propia clase application un mtodo que realiza la carga de los puntos. De
momento la implementacin con la que se cuenta lo lee desde local.
Vamos a modificar por tanto la clase Splash para que invoque al mtodo loadPuntos de la clase
BicicasApplication y haga un while hasta que haya cargado los puntos Vamos a sustituir el
cdigo que haba en el mtodo run del Thread por lo siguiente:
BicicasApplication application = (BicicasApplication)getApplication();
try{
application.loadPuntos();

while (!application.haCargadoPuntos()){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}

handler.post(new Runnable() {
@Override
public void run() {
Intent mainIntent = new
Intent(SplashActivity.this,MenuActivity.class);
mainIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
SplashActivity.this.startActivity(mainIntent);
SplashActivity.this.finish();
}
});

}
catch(Exception e){
e.printStackTrace();
handler.post(new Runnable() {
@Override
public void run() {
AlertDialog.Builder dialog = new
AlertDialog.Builder(SplashActivity.this);
dialog.setTitle("Error");
dialog.setMessage("Se ha producido un error al inicializar la
aplicacin. Pulse Ok para salir de la aplicacin");
dialog.setPositiveButton("Ok", new
DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int
which) {
SplashActivity.this.finish();
}
});
dialog.show();
}
}
);
}

Ejecutamos la aplicacin y de nuevo

Vemos de nuevo el LogCat y obtenemos el siguiente mensaje:
10-29 08:13:56.780: ERROR/AndroidRuntime(363): java.lang.ClassCastException:
android.app.Application
10-29 08:13:56.780: ERROR/AndroidRuntime(363): at
com.cuatroochenta.bicicas.SplashActivity$CargaPuntosThread.run(SplashActivity.java:34)

Lo que ocurre es que no hemos indicado en el AndroidManifest que queremos utilizar nuestra
propia clase aplicacin por lo que en el Thread nos devuelve una instancia del standard
android.app.Application.
Modificamos el AndroidManifest.xml aadiendo el atributo
android:name=".BicicasApplication" al nodo <applicatio.
Llegado a este punto ya tenemos todos los puntos cargados y guardados en nuestra clase
BicicasApplication. Vamos a crear ahora la nueva activity ListaPuntosActivity. A diferencia de
Splash y Menu va a descender de ListActivity y no vamos a definir el layout de la actividad
globalmente sino que vamos a tener que definir el layout para cada una de las filas. Para que
Android popule la actividad con filas se provee a la actividad de un ListAdapter. Esta clase
es la responsable de indicar a Android:
el nmero de elementos que tiene la lista
el objeto (PuntoBicicas) que representa a cada uno de ellos
y de devolver un objeto View por cada elemento de la Lista
Antes de crear el ListActivity copiar a la carpeta res/drawable los ficheros con_bicicletas.png y
sin_bicicletas.png.
Ahora vamos a crear el layout que va a definir a cada una de las filas de la tabla. Para ello crear
el fichero res/layout/lista_punto_row.xml con el siguiente contenido:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="6dip">

<ImageView
android:id="@+id/row_icono_punto"

android:layout_width="wrap_content"
android:layout_height="fill_parent"

android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:layout_marginRight="6dip"
android:src="@drawable/con_bicicletas"
/>

<TextView
android:id="@+id/row_disponibilidad_punto"

android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"

android:textSize="10dip"
android:layout_toRightOf="@id/row_icono_punto"
android:layout_alignParentBottom="true"
android:text="DISPONIBILIDAD DEL PUNTO" />

<TextView
android:id="@+id/row_nombre_punto"
android:layout_width="fill_parent"
android:layout_height="wrap_content"

android:textSize="14dip"
android:layout_toRightOf="@id/row_icono_punto"
android:layout_marginTop="5dip"
android:text="NOMBRE DEL PUNTO"
/>

</RelativeLayout>

Cada fila va a tener un punto verde o rojo indicando si hay bicis disponibles, el nmero de
puntos y el nmero de bicis libres/bicis totales.


La ltima pieza necesaria ser crear el cdigo del ListAdapter:
public class PuntosListAdapter extends BaseAdapter {

private List<PuntoBicicas> puntos;
private LayoutInflater layoutInflater;

public PuntosListAdapter(Context context, List<PuntoBicicas> puntos) {
super();
this.puntos = puntos;
this.layoutInflater =
(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
PuntoBicicas punto = puntos.get(position);

// Forma ineficiente de pintar la lista
View row=layoutInflater.inflate(R.layout.lista_punto_row, null);
TextView
nombrePuntoLabel=(TextView)row.findViewById(R.id.row_nombre_punto);
TextView
disponibilidadPuntoLabel=(TextView)row.findViewById(R.id.row_disponibilidad_punto);
ImageView icon=(ImageView)row.findViewById(R.id.row_icono_punto);
nombrePuntoLabel.setText(punto.getNombre());

disponibilidadPuntoLabel.setText(String.format("%1$d/%2$d",punto.getNumeroBicicle
tasLibres(),punto.getNumeroBicicletasTotales()));
if (punto.getNumeroBicicletasLibres() == 0 ){
icon.setImageResource(R.drawable.sin_bicicletas);
}
else{
icon.setImageResource(R.drawable.con_bicicletas);
}
return row; }

@Override
public int getCount() {
return puntos.size();
}

@Override
public Object getItem(int position) {
return puntos.get(position);
}

@Override
public long getItemId(int position) {
return ((PuntoBicicas)getItem(position)).getNumeroPunto();
}


}
Antes de ejecutar la aplicacin no olvidar aadir la actividad al AndroidManifest.xml
<activity android:name=".activity.ListaPuntosActivity"
android:label="@string/lista_puntos_activity_title">
</activity>
Aadir el recurso lista_puntos_activity_title a res/values/strings.xml
<string name="lista_puntos_activity_title">Lista de puntos bicicas</string>

E invocar a la actividad desde MenuActivity.
Intent i = new Intent(this, ListaPuntosActivity.class);
startActivity(i);

Fase 3: Crear mapa con puntos bicicas
En esta ltima fase se va aadir un mapa con todos los puntos bicicas de Castelln.
Todava estamos recogiendo los datos de local por lo que vamos a modificar la clase
BicicasApplication para que los recupere del servidor.
Vamos a establecer a false el valor de la variable USE_LOCAL_FILE y en el cdigo del
mtodo loadPuntos vamos a incluir las invocaciones a la clase HttpClient que ser la que se
utilizar para recuperar el fichero xml desde Internet.
HttpClient client = new DefaultHttpClient();

String url = "http://www.cuatroochenta.com/demos/bicicas/puntos.php?format=XML";
HttpGet get = new HttpGet(url);
try {
HttpResponse response = client.execute(get);
InputStream is = response.getEntity().getContent();

puntosBicicas = PuntosParser.parsePuntos(is);
puntosCargados = true;
} catch (IOException e) {
throw new RuntimeException("Error inesperado en la carga de los puntos",e);
}

Ya recuperamos el mapa desde remoto. Tratamos de ejecutar la aplicacin y

Y ahora?
Este mensaje de error se muestra en el Splash cuando se ha producido un error en la carga. Lo
que realmente enmascara es un problema de permisos. Si se quiere acceder a Internet es
necesario indicarlo en el AndroidManifest.xml. Cuando se instala una aplicacin de Android
desde el Android Market aparece un listado con todos aquellos permisos especiales que
requiere cada aplicacin.
Para solucionarla aadismos entre el tag <manifest y el tag <application la siguiente lnea:
<uses-permission android:name="android.permission.INTERNET" />

Para poder utilizar Google Maps en el emulador es necesario disponer de una clave de API.
Para obtenerla hay que entrar en la pgina http://code.google.com/intl/es-ES/android/add-
ons/google-apis/maps-api-signup.html y pegar el fingerprint del certificado de debug que se
gener de forma automtica cuando se instal el SDK. Para obtener el fingerprint de debug
consultar el siguiente enlace: http://code.google.com/intl/es-ES/android/add-ons/google-
apis/mapkey.html#getdebugfingerprint

Una vez obtenida la clave de Google Maps ser necesario crear el fichero de layout del
MapActivity que vamos a crear. Este cdigo es muy sencillo:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mainlayout"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >

<com.google.android.maps.MapView
android:id="@+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey="AQUI_TU_CLAVE
/>

</RelativeLayout>

Vamos a aadir dos nuevas imgenes necesarias para pintar los puntos bicicas. Estas son
bici.png y bici_seleccionada.png. Las copiaremos a res/drawable
El cdigo de la activity es el siguiente:
public class MapaPuntosActivity extends MapActivity{

public static final String EXTRA_PUNTO_BICICAS_ID = "EXTRA_PUNTO_BICICAS_ID";

private MapView mapView;

private List<Overlay> mapOverlays;
private Drawable drawableBiciNormal;
private Drawable drawableBiciSeleccionada;
private MyItemizedOverlay itemizedOverlay;
private LocationManager locationManager;
private List<PuntoBicicas> puntosBicicas;

private PuntoBicicas selectedPuntoBicicas;

private GestureDetector detector;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.map);

//Recupero el mapa de la jerarqua de vistas
mapView = (MapView)findViewById(R.id.mapview);

//Creo una clase para detectar gestos. Bsicamente para hacer zoom con el double
tap
detector = new GestureDetector(this,new MySimpleGestureListener());

//Digo al componente que incluya los componentes de zoom que tiene por defecto
mapView.setBuiltInZoomControls(true);

//El detector necesita que le pasemos los eventos de touch para que los detecte
mapView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return detector.onTouchEvent(event);
}
});

//Recuperamos el listado de puntos bicicas
puntosBicicas = ((BicicasApplication)getApplication()).getPuntosBicicas();

//Inicializamos la clase que nos permitira obtener nuestra posicin GPS
locationManager =
(LocationManager)getSystemService(Context.LOCATION_SERVICE);

//Obtenemos la referencia al objeto overlays del mapa que nos permitir
//aadir capas encima
mapOverlays = mapView.getOverlays();

//Recuperamos las dos imgenes que vamos a utilizar
drawableBiciNormal = this.getResources().getDrawable(R.drawable.bici);
drawableBiciSeleccionada =
this.getResources().getDrawable(R.drawable.bici_seleccionada);

//Si se nos ha pasado un punto por parmetro lo marcamos como seleccionado
if (getIntent().hasExtra(EXTRA_PUNTO_BICICAS_ID)){
selectedPuntoBicicas =
puntosBicicas.get(getIntent().getIntExtra(EXTRA_PUNTO_BICICAS_ID, -1));
}

//Me creo mi propia capa donde aadir todos las puntos
itemizedOverlay = new MyItemizedOverlay(drawableBiciNormal);

//Para calcular el punto central sumar todas las latitudes y longitudes y las
dividir por el nmero de puntos
long sumaLatitudes = 0;
long sumaLongitudes = 0;
for (PuntoBicicas puntoBicicas: puntosBicicas){

GeoPoint point =
toGeoPoint(puntoBicicas.getLatitud(),puntoBicicas.getLongitud());

if (selectedPuntoBicicas==null){
sumaLatitudes+=point.getLatitudeE6();
sumaLongitudes+=point.getLongitudeE6();
}

//Aado un item a la capa
OverlayItem overlayitem = new OverlayItem(point, puntoBicicas.getNombre(),
puntoBicicas.getDescripcion());
//Si el punto actual es el seleccionado cambio la imagen por defecto por la
de bicicleta seleccionada
if (selectedPuntoBicicas!=null &&
selectedPuntoBicicas.equals(puntoBicicas)){

overlayitem.setMarker(itemizedOverlay.boundCenterBottomAux(drawableBiciSelecciona
da));
}
itemizedOverlay.addOverlay(overlayitem);


}
//Indico que el mapa puede ser clicable
mapView.setClickable(true);

//Y por ltimo aado la capa con todos los puntos
mapOverlays.add(itemizedOverlay);

//Si hay una bicicleta seleccionada la establezco como centro del mapa y hago un
poco ms de zoom
if (selectedPuntoBicicas==null){
mapView.getController().setCenter(new
GeoPoint((int)(sumaLatitudes/puntosBicicas.size()),(int)sumaLongitudes/puntosBicicas.siz
e()));
mapView.getController().setZoom(17);
}
else{

mapView.getController().setCenter(toGeoPoint(selectedPuntoBicicas.getLatitud(),se
lectedPuntoBicicas.getLongitud()));
mapView.getController().setZoom(16);
}

}

public static final long TO_E6 = 1000000l;
public static GeoPoint toGeoPoint(float latitud, float longitud) {
return new GeoPoint((int)(latitud*TO_E6), (int)(longitud*TO_E6));
}

@Override
protected boolean isRouteDisplayed() {
return false;
}

//La implementacin de mi capa
public class MyItemizedOverlay extends ItemizedOverlay<OverlayItem> {

public MyItemizedOverlay(Drawable defaultMarker) {
super(boundCenterBottom(defaultMarker));
}

private ArrayList<OverlayItem> mOverlays = new ArrayList<OverlayItem>();

@Override
protected OverlayItem createItem(int i) {
return mOverlays.get(i);
}

@Override
public int size() {
return mOverlays.size();
}

public void addOverlay(OverlayItem overlay) {
mOverlays.add(overlay);

populate();
}

//De nuevo paso los eventos al detector
@Override
public boolean onTouchEvent(MotionEvent event, MapView mapView) {
return detector.onTouchEvent(event);
}

//Dado una imagen la ajusta para que el punto 0,0 de ste est en el
centro de la parte inferior
public Drawable boundCenterBottomAux(Drawable marker){
return boundCenterBottom(marker);
}

//Cuando se hace tap en un punto muestro un Alert
@Override
protected boolean onTap(int index) {
PuntoBicicas punto = puntosBicicas.get(index);

// OverlayItem item = mOverlays.get(index);
AlertDialog.Builder dialog = new
AlertDialog.Builder(MapaPuntosActivity.this);
dialog.setTitle("Punto seleccionado");
dialog.setMessage(punto.getNombre());
dialog.show();
return true;
}





}

//Mi GestureListener desciende de SimpleOnGestureListener y slo sobreescribo el
mtodo onTap
//Hago un zoom de la misma forma que lo hace la aplicacin de Google Maps nativa
class MySimpleGestureListener extends SimpleOnGestureListener{

@Override
public boolean onDoubleTap(MotionEvent e) {
mapView.getController().zoomInFixing((int)e.getX(),(int)e.getY());
return true;
}
}

}

En el cdigo se puede ver que es posible que te enven una bici seleccionada cuando se inicie la
actividad. Esto lo podremos hacer desde ListaPuntosActivity. Para ello sobreescribimos el
mtodo onListItemClick desde donde invocaremos a MapaPuntosActivity aadiendo un Extra
en el Intent que arranca la actividad.
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
Intent intent = new Intent(this, MapaPuntosActivity.class);
intent.putExtra(MapaPuntosActivity.EXTRA_PUNTO_BICICAS_ID, position);
startActivity(intent);
}

Lo ltimo que quedara sera dar de alta la actividad en el manifest, aadir al strings.xml una
nueva cadena y desde MenuActivity invocar a la nueva pantalla
<activity android:name=".MapaPuntosActivity"
android:label="@string/mapa_puntos_activity_title">
</activity>

<string name="mapa_puntos_activity_title">Mapa de puntos Bicicas</string>

Intent i = new Intent(this, MapaPuntosActivity.class);
startActivity(i);

y

Analizamos de nuevo LogCat y nos encontramos con lo siguiente:
10-29 09:26:09.335: ERROR/AndroidRuntime(467): java.lang.NoClassDefFoundError:
com.cuatroochenta.bicicas.MapaPuntosActivity

El NoClassDefFoundError es ligeramente distinto al ClassNotFoundException que se provoca
cuando no se encuentra una clase. NoClassDefFoundError indica que no se ha podido levantar
esta clase pues depende de alguna clase que no la tenemos disponible. Aunque el proyecto lo
hayamos creado indicando que queremos las Google APIs en el Manifest hay que indicar
expresamente que se quiere utilizar esta librera. De no ser as no la incluye en el classpath de
ejecucin de nuestra aplicacin.
Aadimos por lo tanto al final del tag <application, justo antes del cierre:
<uses-library android:name="com.google.android.maps" />

Y ya tenemos nuestra aplicacin funcionando:

Fase 4: Recomendar a un amigo y mejorar rendimiento de la lista
de puntos
Recomendar a un amigo
Para aadir el recomendar a un amigo slo sera necesario incluir el siguiente cdigo en
MenuActivity:
Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.setType("message/rfc822");
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
getString(R.string.asunto_recomendar_amigo));
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT,
getString(R.string.cuerpo_recomendar_amigo));
this.startActivity(Intent.createChooser(emailIntent,
getString(R.string.enviar_email_chooser)));

Lo que hace este cdigo es crear un intent del tipo ACTION_SEND, en el campo tipo indicar que
se quiere enviar un email (message/rfc822) y mediante los Extras standard de android se
incluye el asunto y el cuerpo del mensaje. Como es posible que Android tenga varias
aplicaciones que gestionen el envo de correo mediante Intent.createChooser se indica el ttulo
que aparecer en el cuadro de dilogo donde permite escoger una de ellas.
Tendremos tambin que crear las siguientes cadenas en el strings.xml:
<string name="asunto_recomendar_amigo">Recomendar aplicacin</string>
<string name="cuerpo_recomendar_amigo">Quera recomendarte que asistas al prximo
decharlas de la Uji</string>
<string name="enviar_email_chooser">Enviar email con:</string>

Optimizar ListaPuntosActivity
El ltimo paso de la aplicacin ser mejorar el rendimiento de la clase ListaPuntosActivity. Con
la implementacin actual (en PuntosListAdapter) se crea una vista (View) por cada punto
Bicicas. Imaginemos que tenemos 100 puntos Android permite cachear estas filas visuales de
forma que si se visualizan 6 filas a la vez internamente es posible que slo instancie (6,7, u 8)
Una primera aproximacin para mejorar el rendimiento sera la siguiente:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//Si no nos han pasado una instancia existente la creamos
if (convertView == null) {
convertView = layoutInflater.inflate(R.layout.lista_punto_row, null);
}

//Buscamos todos los componentes visuales
TextView nombrePuntoLabel = (TextView)
convertView.findViewById(R.id.row_nombre_punto);
TextView disponibilidadPuntoLabel = (TextView)
convertView.findViewById(R.id.row_disponibilidad_punto);
ImageView biciIcon = (ImageView) convertView.findViewById(R.id.row_icono_punto);

//Recuperamos el punto
PuntoBicicas punto = puntos.get(position);

//Y establecemos las propiedades de los componentes visuales
nombrePuntoLabel.setText(String.format("%1$s -
%2$s",punto.getNumeroPunto(),punto.getNombre()));
disponibilidadPuntoLabel.setText(String.format("%1$d/%2$d",punto.getNumeroBicicle
tasLibres(),punto.getNumeroBicicletasTotales()));
if (punto.getNumeroBicicletasLibres() == 0 ){
biciIcon.setImageResource(R.drawable.sin_bicicletas);
}
else{
biciIcon.setImageResource(R.drawable.con_bicicletas);
}

return convertView;
}

Esta implementacin es muchsimo mejor que la que tenamos hasta ahora. Sin embargo
todava queda algo de margen para aliviar a nuestro dispositivo. Si se observa el cdigo cada
vez que se repinta una fila se busca dentro de la vista cada uno de los componentes. Lo que
vamos a hacer es crearnos una clase dentro del adapter para guardarnos la referencia a cada
uno de ellos:
static class ViewHolder {
TextView nombrePuntoLabel;
TextView disponibilidadPuntoLabel;
ImageView biciIcon;
}

Cada objeto que hereda de View tiene una propiedad Tag que es un Object y que nos permite
guardarnos cualquier objeto asociado. Esto va a ser precisamente lo que vamos a hacer.
Asociar a cada View de la lista sus componentes visuales para poderlos recuperar de forma
ms eficiente:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//En este view holder vamos a guardar/recuperar las referencias a los componentes
visuales
ViewHolder holder;

//Si no nos da una vista reutilizable la creamos nueva
if (convertView == null) {
convertView = layoutInflater.inflate(R.layout.lista_punto_row, null);

//Metemos en el ViewHolder todos los componentes visuales
holder = new ViewHolder();
holder.nombrePuntoLabel = (TextView)
convertView.findViewById(R.id.row_nombre_punto);
holder.disponibilidadPuntoLabel = (TextView)
convertView.findViewById(R.id.row_disponibilidad_punto);
holder.biciIcon = (ImageView) convertView.findViewById(R.id.row_icono_punto);

//Y por ltimo asociamos el holder a la vista
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}

//Recuperamos el punto que representa la fila
PuntoBicicas punto = puntos.get(position);

//Y por ltimo asignamos los textos, iconos a los componentes visuales que los
recuperamos del holder
holder.nombrePuntoLabel.setText(String.format("%1$s -
%2$s",punto.getNumeroPunto(),punto.getNombre()));
holder.disponibilidadPuntoLabel.setText(String.format("%1$d/%2$d",punto.getNumero
BicicletasLibres(),punto.getNumeroBicicletasTotales()));
if (punto.getNumeroBicicletasLibres() == 0 ){
holder.biciIcon.setImageResource(R.drawable.sin_bicicletas);
}
else{
holder.biciIcon.setImageResource(R.drawable.con_bicicletas);
}

return convertView;
}