Está en la página 1de 164

Curso Programación avanzada

de dispositivos móviles
IFC02CM15

Cuadernillo de prácticas

Pedro Pablo Gómez Martín

Julio, 2015
Documento maquetado con TEXiS v.1.0.

Este documento está preparado para ser imprimido a doble cara.


Curso Programación avanzada
de dispositivos móviles
IFC02CM15

Cuadernillo de prácticas

Julio, 2015
Copyright
c Pedro Pablo Gómez Martín
Índice

1. Listas 1
Pr. 1.1. Hola mundo . . . . . . . . . . . . . . . . . . . . . . . . . 1

Pr. 1.2. Cambiando el color . . . . . . . . . . . . . . . . . . . . . 3

Pr. 1.3. Reloj emergente . . . . . . . . . . . . . . . . . . . . . . . 3

Pr. 1.4. Mensaje de alerta desde un layout . . . . . . . . . . . . . 4

Pr. 1.5. Lista básica de elementos . . . . . . . . . . . . . . . . . . 6

Pr. 1.6. ListView con dos etiquetas . . . . . . . . . . . . . . . . . 8

Pr. 1.7. El patrón View Holder . . . . . . . . . . . . . . . . . . . 12

Pr. 1.8. Diferentes tipos de item . . . . . . . . . . . . . . . . . . . 13

Pr. 1.9. Pulsación sobre elementos . . . . . . . . . . . . . . . . . 15

Pr. 1.10. Borrado de elementos . . . . . . . . . . . . . . . . . . . . 16

Pr. 1.11. Identicadores estables de los elementos . . . . . . . . . . 18

Pr. 1.12. Ordenación de los elementos . . . . . . . . . . . . . . . . 19

Pr. 1.13. Vista especíca para la lista vacía . . . . . . . . . . . . . 21

Pr. 1.14. ViewStub: optimizando las vistas . . . . . . . . . . . . . . 22

Pr. 1.15. ListActivity: ahorrándonos trabajo . . . . . . . . . . . 24

Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

2. Fragments 27
Pr. 2.1. Dos actividades: detalles de los libros . . . . . . . . . . . 27

Pr. 2.2. Repasando el ciclo de vida . . . . . . . . . . . . . . . . . 30

Pr. 2.3. Fragmentos: aprovechando la pantalla de las tablets . . . 34

Pr. 2.4. Fragmentos en actividades diferentes . . . . . . . . . . . 45

Pr. 2.5. Fragmentos: ciclo de vida . . . . . . . . . . . . . . . . . . 47

Pr. 2.6. Conguración diferente en horizontal y vertical . . . . . . 50

Pr. 2.7. Dos fragmentos dinámicos . . . . . . . . . . . . . . . . . 52

Pr. 2.8. Fragmentos estáticos estables . . . . . . . . . . . . . . . . 59

Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 60

3. Hebras y tareas asíncronas 61


Pr. 3.1. Actividades, aplicaciones y procesos . . . . . . . . . . . . 61

v
vi Índice

Pr. 3.2. La hebra principal y ANR . . . . . . . . . . . . . . . . . 62

Pr. 3.3. Hebras trabajadoras . . . . . . . . . . . . . . . . . . . . . 63

Pr. 3.4. Hebras trabajadoras e interfaz . . . . . . . . . . . . . . . 64

Pr. 3.5. Ejecución en la hebra principal . . . . . . . . . . . . . . . 66

Pr. 3.6. AsyncTask . . . . . . . . . . . . . . . . . . . . . . . . . . 67

Pr. 3.7. AsyncTask cancelable . . . . . . . . . . . . . . . . . . . . 69

Pr. 3.8. Progreso de la AsyncTask . . . . . . . . . . . . . . . . . . 71

Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 73

4. Servicios 75
Pr. 4.1. Servicio básico . . . . . . . . . . . . . . . . . . . . . . . . 75

Pr. 4.2. Servicios y la hebra principal . . . . . . . . . . . . . . . . 77

Pr. 4.3. IntentService . . . . . . . . . . . . . . . . . . . . . . . 78

Pr. 4.4. BroadcastReceiver: comunicación con la actividad . . . 82

Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 85

5. Bluetooth 87
Pr. 5.1. Activar bluetooth . . . . . . . . . . . . . . . . . . . . . . 87

Pr. 5.2. Detectar cambios de estado . . . . . . . . . . . . . . . . . 89

Pr. 5.3. Visibilidad del dispositivo . . . . . . . . . . . . . . . . . . 92

Pr. 5.4. Dispositivos emparejados . . . . . . . . . . . . . . . . . . 96

Pr. 5.5. Servidor bluetooth . . . . . . . . . . . . . . . . . . . . . . 98

Pr. 5.6. Cliente bluetooth . . . . . . . . . . . . . . . . . . . . . . 101

Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

6. Conexión por red 105


Pr. 6.1. Estado de la red . . . . . . . . . . . . . . . . . . . . . . . 105

Pr. 6.2. Nocaciones del cambio de estado . . . . . . . . . . . . . 108

Pr. 6.3. Cliente de sumas . . . . . . . . . . . . . . . . . . . . . . . 109

Pr. 6.4. Servidor de sumas . . . . . . . . . . . . . . . . . . . . . . 114

Pr. 6.5. Viabicing . . . . . . . . . . . . . . . . . . . . . . . . . . . 116

Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 127

7. Animaciones 129
Pr. 7.1. Animaciones básicas entre fragmentos . . . . . . . . . . . 129

Pr. 7.2. Animaciones personalizadas entre fragmentos . . . . . . . 130

Pr. 7.3. Animaciones tween: escalado . . . . . . . . . . . . . . . 131

Pr. 7.4. Animaciones tween: traslación . . . . . . . . . . . . . . 133

Pr. 7.5. Animaciones tween: rotación . . . . . . . . . . . . . . . 133

Pr. 7.6. Animaciones tween: alfa . . . . . . . . . . . . . . . . . . 134

Pr. 7.7. Animaciones tween: animaciones simultáneas . . . . . . 134

Pr. 7.8. Animaciones tween: animaciones en secuencia . . . . . . 135


Índice vii

Pr. 7.9. Animaciones tween: fillAfter y fillBefore . . . . . . 135

Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

8. Audio y grácos para juegos 137


Pr. 8.1. Streams de salida de audio . . . . . . . . . . . . . . . . . 137

Pr. 8.2. Efectos de sonido . . . . . . . . . . . . . . . . . . . . . . 138

Pr. 8.3. Música de fondo . . . . . . . . . . . . . . . . . . . . . . . 140

Pr. 8.4. Renderizado continuo . . . . . . . . . . . . . . . . . . . . 142

Pr. 8.5. SurfaceView: renderizado continuo en otra hebra . . . . 145

Pr. 8.6. Una entidad . . . . . . . . . . . . . . . . . . . . . . . . . 148

Pr. 8.7. Detalles nales . . . . . . . . . . . . . . . . . . . . . . . . 151

Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 152


Capítulo 1

Listas
Resumen: En este capítulo veremos la manera de conseguir mostrar
listas en Android. Es un tipo de control omnipresente en las aplicacio-
nes, que no es sencillo de utilizar. Antes de introducirlo, daremos unos
primeros pasos con las noticaciones, que nos servirán de repaso y de
preparación para los conceptos que se necesitarán con las listas.

Práctica 1.1: Hola mundo

Para ir calentando, empezaremos con una práctica sencilla en la que


haremos una actividad con un botón que, al ser pulsado, mostrará un mensaje
(toast ) saludando.

1. Lanza el entorno de desarrollo y crea un proyecto nuevo:

Nombre de la aplicación: Hola mundo

Dominio de la compañía: azul.libro


Mínimo SDK: API 10 (Android 2.3.3)

Una blank activity, con la conguración por defecto.

2. Revisa el código que el asistente ha creado por nosotros y asegúrate de


que lo entiendes todo.

3. Lanza la aplicación en un AVD o en tu móvil.

La actividad predenida no es muy emocionante. Vamos a cambiarla para


añadir un poco de interactividad:

1. Elimina la etiqueta del layout.

1
2 Capítulo 1. Listas

2. Añade el botón:

<Button android:text="@string/pulsame"
android:layout_width="match_parent"
android:layout_height="match_parent" />

3. La cadena @string/hello_world (de la etiqueta) ya no se utiliza. Eli-


mínala de strings.xml.

4. Añade una nueva cadena con identicador pulsame y texto ½Púlsame!

5. Lanza la aplicación y comprueba el cambio.

6. De momento, el botón no hace nada. Añade un evento:

<Button ...
android:onClick="onPulsame" />

7. Añade en la clase de la actividad el método que recibirá la noticación.

public void onPulsame(View view) {


Toast toast = Toast.makeText(
getApplicationContext(),
R.string.saludo,
Toast.LENGTH_SHORT);
toast.show();
}

8. Añade en strings.xml una nueva cadena con el identicador saludo


y texto ½Hola, mundo!.

9. Lanza la aplicación y comprueba que, al pulsar el botón, aparece el


mensaje.

Puedes limpiar la aplicación eliminando el menú que añadió el asistente


y que no estamos utilizando para nada:

1. Elimina de la clase principal los métodos onCreateOptionsMenu() y


onOptionsItemSelected() (y retira los import's que ya no se necesi-
ten).

2. Elimina el recurso del menú, menu_main.xml.

3. Elimina la cadena action_settings de strings.xml.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
3

Práctica 1.2: Cambiando el color

Utilizando el método factoría Toast.makeText() conseguimos un objeto


Toast cómodamente. Podemos modicar la posición de la noticación usan-
do los métodos setGravity() y setMargin() antes de mostrarla, e incluso
renar el texto con setText().

Si queremos hacer cambios más radicales, tendremos que acceder a la


vista (widget ) interna del toast y manipularla. Por ejemplo, para que el texto
salga de color rojo, antes de hacer visible la noticación podemos obtener su
vista interna, acceder a la etiqueta y cambiarla el color.

1. Haz una copia del proyecto.

2. Añade, antes de la línea toast.show() el siguiente código:

View v = toast.getView();
TextView tv = (TextView) v.findViewById(android.R.id.message);
tv.setTextColor(Color.RED);

Práctica 1.3: Reloj emergente

En la práctica anterior, utilizamos el método factoría para conseguir un


toast inicializado, y accedimos a su contenido para manipularlo.
En lugar de eso, podemos construir manualmente el toast (con new) y
establecerle por código la vista que queramos que contenga. La clase Toast
se encargará de superponer nuestra vista sobre la actividad actual, y de
hacerla desaparecer un tiempo después.

En esta práctica, pondremos como vista el widget AnalogClock, que pinta


un reloj de agujas.

1. Haz una copia del proyecto de la práctica anterior.

2. Renombra el paquete principal a libro.azul.relojemergente.

3. Modica la cadena app_name y pon Reloj emergente. Borra la cadena


saludo, que ya no utilizaremos.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
4 Capítulo 1. Listas

4. Quita el código del evento asociado al botón y crea y congura la


noticación manualmente. En concreto, construye un nuevo objeto con
new, y luego establecele como vista un android.widget.AnalogClock.

public void onPulsame(View view) {


Context c = getApplicationContext();
Toast toast = new Toast(c);
toast.setDuration(Toast.LENGTH_LONG);
AnalogClock clock = new AnalogClock(c);
toast.setView(clock);
toast.show();
}

Práctica 1.4: Mensaje de alerta desde un layout

Si quisiéramos poner noticaciones más elaboradas (por ejemplo, un


icono y un texto), podríamos construir vistas más complejas igual que hemos
hecho en la práctica anterior. Sin embargo, construir el interfaz por código
va en contra de la losofía de desarrollo para Android. Cuando sea posible,
es preferible utilizar cheros de recursos de tipo layout para ello. En esta
práctica, crearemos un layout que no estará directamente asociado a una
actividad, sino que lo usaremos para construir a partir de él el contenido del
toast.

1. Haz una copia del proyecto de la práctica 1.1 y renombra el paquete


principal a libro.azul.toastconlayout.
2. Cambia el título de la actividad principal a Toast con layout.

3. Crea un nuevo chero de layout :

Llámalo mitoast.xml
Usa un LinearLayout.
Añade una imagen. Para no tener que buscar una y añadirla
en el proyecto, utilizaremos una de las predenidas de Android,
@android:drawable/ic_dialog_info.
Añade una etiqueta con la cadena @string/saludo que denimos
en la práctica original.

Ponle a la imagen un pequeño margen a la derecha para que se


separe del texto (8dp bastarán).

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
5

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:src="@android:drawable/ic_dialog_info"
/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/saludo"
android:layout_gravity="center_vertical"
/>

</LinearLayout>

4. Modica el código del método onPulsame() para que se cree una no-
ticación manualmente, como hicimos en la práctica 1.3.

5. Obtén el objeto que es capaz de generar una vista a partir del identi-
cador de un layout.

6. Pídele que construya la vista a partir del layout anterior.

7. Establécelo como vista del toast que acabas de crear

8. Muestra el toast.

public void onPulsame(View view) {


Context c = getApplicationContext();
Toast toast = new Toast(c);
toast.setDuration(Toast.LENGTH_LONG);

LayoutInflater inflater = getLayoutInflater();


View v = inflater.inflate(R.layout.mitoast, null, false);

toast.setView(v);
toast.show();
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
6 Capítulo 1. Listas

1. Modica el layout y añade un AnalogClock.

2. Lanza la aplicación.

3. Pulsa el botón y cierra la aplicación antes de que el toast desaparezca.

4. ¾Qué ocurre? ¾Por qué? ¾Cómo lo solucionarías?

Práctica 1.5: Lista básica de elementos

Las listas resultan ser un control muy habitual en las aplicaciones de


Android, pero requieren una programación algo más compleja de lo habitual.
Hay varias cuestiones que hay que tener en cuenta:

El objetivo de las listas es mostrar múltiples elementos (items).

Cada elemento se muestra a través de un componte visual diferente.


Por tanto, las listas son contenedores de otros controles: un botón es
un widget independiente, pero una lista tendrá dentro a otros.

La visualización de cada elemento de la lista puede requerir más de un


elemento, mezclando así múltiples etiquetas, imágenes, etcétera.

En un determinado momento, no todos los items serán visibles, por lo


que podríamos querer que la creación de los componentes de cada item
no se construyan hasta que no sea imprescindible.

Los items de una lista pueden provenir de datos con estructuras com-
plejas (más allá de un mero texto) por lo que se desea independizar el
componente visual (ListView) de los propios datos y su origen.

Todo lo anterior hace que, formalmente, las ListView se consideren la-


youts, que son construídas a partir de lo que Android denomina adaptado-
res. Un adaptador es un objeto que implementa un determinado interfaz
(Adapter), y a través del cual la ListView es capaz de obtener cada uno de
los componentes grácos que debe mostrar. El método más importante es
getView(), que devuelve la vista (control) que mostrará el elemento i-ésimo
de la vista.

Android proporciona varios adaptadores. En esta práctica, utilizaremos


ArrayAdapter que recibe un array de objetos de tipo T (es una clase genérica)
y para cada uno construye un TextView en el que muestra el resultado de
ejecutar toString() sobre él. Por tanto, al conectar un ListView con un

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
7

ArrayAdapter veremos la lista de los elementos del array que le hayamos


dado al adaptador.

1. Crea un nuevo proyecto y llámalo ListViewBasico. Como título pon


ListView básico, y ponlo en el paquete libro.azul.listviewbasico.

2. Modica el layout principal para eliminar la etiqueta añadida por el


asistente y pon como único widget un ListView. Fíjate que aquí no
establecemos su contenido.

<RelativeLayout ... >

<ListView android:id="@+id/listView"
android:layout_width="math_parent"
android:layout_height="math_parent" />

</RelativeLayout>

3. La clase ArrayAdapter necesita saber cómo queremos que se muestre la


etiqueta que creará para cada item. Espera que lo hagamos indicando
un layout que esté formado por un TextView. En el método getView()
inará el layout y establecerá el texto del elemento correspondiente.
Crea un layout de nombre basiclistitemview con un TextView:

<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</TextView>

4. En el onCreate(), tras establecer la vista con el layout :

a ) Crea un array de 100 Integer, y añade los números del 1 al 10.

b ) Crea un ArrayAdapter<Integer>, y pásale en el constructor el


array anterior.

c ) Busca la ListView en la actividad.

d ) Establece como adaptador el ArrayAdapter creado.

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Integer[] datos = new Integer[100];


for (int i = 0; i < 100; ++i) {

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
8 Capítulo 1. Listas

datos[i] = i + 1;
}

ArrayAdapter<Integer> aa;
aa = new ArrayAdapter<Integer>(this,
R.layout.basiclistitemview,
datos);
ListView lv = (ListView) findViewById(R.id.listView);
lv.setAdapter(aa);

} // onCreate

5. Prueba la aplicación.

El constructor de ArrayAdapter recibe tres parámetros:

1. El objeto que se usará como contexto para la creación de las vistas.

2. El layout que se inará para cada item. Debe ser un TextView.


3. El array de elementos de tipo T (o, alternativamente, un List<T>) del
que se obtendrán los items.

Observa que los elementos aparecen muy juntos, y su tamaño no sería


práctico si quisiéramos permitir al usuario que seleccionara alguno. Además,
tener que crear el layout incluso para unos items tan simples como un mero
texto resulta laborioso.

Android proporciona un layout predenido que ahorra la creación del


layout, y que además deja espacio suciente para poder seleccionar cada
elemento en función del tipo y conguración del dispositivo.

1. Modica la llamada al constructor delArrayAdapter y en el segun-


do parámetro pasa android.R.layout.simple_list_item_1. Es un
layout predenido que contiene un TextView congurado para dejar
margen suciente como para poder seleccionarse.

2. Elimina el layout que hiciste manualmente, basiclistitemview.


3. Lanza la aplicación y observa las diferencias.

Práctica 1.6: ListView con dos etiquetas

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
9

Como hemos visto, Android proporciona layout predenidos . En es-


1

ta ocasión, usaremos android.R.layout.simple_list_item_2, que incluye


dos etiquetas.

1. Haz una copia de la práctica anterior:

a ) Llámala ListViewConDosEtiquetas.
b ) Renombra el paquete a libro.azul.listviewdosetiquetas.
2. Modica la llamada al constructor de ArrayAdapter para que utilice
el nuevo layout predenido.

3. Ejecuta la práctica. ¾Qué ocurre?

simple_list_item_2 es un RelativeLayout que tiene dentro


El layout
los dos TextView. Cuando el ArrayAdapter ina el layout para mostrar
un item, no se encuentra un TextView, sino el RelativeLayout, y no puede
establecerle el texto, haciendo saltar una excepción.

1. En la invocación al constructor del ArrayAdapter, antes del último


parámetro con el array pasa como parámetro android.R.id.text1.

2. Ejecuta la práctica de nuevo. Observa que vuelve a funcionar, pero el


aspecto es ligeramente diferente respecto a la práctica anterior.

Como es la clase ArrayAdapter quien se preocupa de crear la vista de


cada item, aunque hayamos pasado un layout que es capaz de mostrar dos
etiquetas no podemos a priori especicar qué queremos que se muestre en la
segunda.

Para conseguirlo, necesitaremos hacer una subclase de ArrayAdapter,


sobreescribir el método getView() y construir nosotros la vista del item.

1. Crea una clase Libro que tenga dos atributos de tipo String, titulo
y autor.
2. Crea una clase MiArrayAdapter que herede de ArrayAdapter<Libro>:
a ) Crea dos constructores que reciban un contexto, y o bien un array
de libros, o bien un java.util.List.
b ) Sobreescribe el método getView(). En él, construye una nueva
vista a partir del layout predenido, obtén el libro que hay que
mostrar en la posición solicitada, y accede a las etiquetas del
layout para poner el título y el autor (android.R.id.text1 y
text2).
1
Puedes verlos en http://developer.android.com/reference/android/R.layout.
html

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
10 Capítulo 1. Listas

class MiArrayAdapter extends ArrayAdapter<Libro> {

public MiArrayAdapter(Context context, Libro[] libros) {


super(context, 0, libros);
}

public MiArrayAdapter(Context context,


java.util.List<Libro> libros) {
super(context, 0, libros);
}

@Override
public View getView(int position, View convertView,
ViewGroup parent) {
Libro l = getItem(position);
View v;
v = LayoutInflater.from(getContext()).inflate(
android.R.layout.simple_list_item_2,
parent,
false);
TextView tv;
tv = (TextView) v.findViewById(android.R.id.text1);
tv.setText(l.titulo);
if (l.autor != null) {
tv = (TextView) v.findViewById(android.R.id.text2);
tv.setText(l.autor);
}
return v;
} // getView

} // class MiArrayAdapter

3. Modica el onCreate() para crear una lista con varios libros, y aso-
ciarselo a un MiArrayAdapter que asocies al ListView.

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

java.util.ArrayList<Libro> libros;
libros = new java.util.ArrayList<Libro>();
libros.add(new Libro("Don Quijote de la Mancha",
"Miguel de Cervantes"));
libros.add(new Libro("La Celestina", "Fernando de Rojas"));

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
11

libros.add(new Libro("Rinconete y Cortadillo",


"Miguel de Cervantes"));
libros.add(new Libro("El Lazarillo de Tormes", null));
libros.add(new Libro("La Galatea", "Miguel de Cervantes"));

MiArrayAdapter aa;
aa = new MiArrayAdapter(this, libros);
ListView lv = (ListView) findViewById(R.id.listView);
lv.setAdapter(aa);
} // onCreate

En el getView() hemos ignorado el segundo parámetro, convertView.


Internamente, ListView puede reutilizar vistas. Si detecta que la vista que
se construyó para un determinado item ya no es necesaria, y necesita una
vista nueva para un item diferente, puede llamar a getView() pasando como
segundo parámetro la vista antigua para ser reciclada con el nuevo item.
Por eciencia, es conveniente por tanto comprobar si el segundo parámetro
no es null. En ese caso, en lugar de crear una vista nueva (inándola desde
el layout ) se debería utilizar esa, accediendo a sus elementos y manipulando
su contenido. Al nal, devolveremos como resultado del método el mismo
objeto que hemos recibido en el segundo parámetro.

public View getView(int position, View convertView,


ViewGroup parent) {
Libro l = getItem(position);
View v;
if (convertView == null)
v = LayoutInflater.from(getContext()).inflate(
android.R.layout.simple_list_item_2,
parent,
false);
else
v = convertView;
TextView tv;
tv = (TextView) v.findViewById(android.R.id.text1);
tv.setText(l.titulo);
tv = (TextView) v.findViewById(android.R.id.text2);
if (l.autor != null)
tv.setText(l.autor);
else
tv.setText("");
return v;
} // getView

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
12 Capítulo 1. Listas

Práctica 1.7: El patrón View Holder


En la práctica anterior, nos preocupamos de reciclar la vista anterior
que pudiera habernos llegado en el segundo parámetro de getView(). Sin
embargo, tenemos que seguir utilizando findViewById() para acceder a sus
elementos internos. Una solución es utilizar el patrón ViewHolder . Consiste
2

en aprovechar que la clase View es capaz de guardar un objeto arbitrario


(con setTag() y getTag() que pueda ser utilizado a discreción por el pro-
gramador. La idea es crear una nueva clase ViewHolder en el que guardemos
los dos TextView. Cuando nos veamos obligados a crear una nueva vista,
buscaremos los TextView con findViewById() y los guardaremos en un ob-
jeto de nuestra clase ViewHolder que conservaremos en la vista a través del
setTag(). En futuras invocaciones a getView(), si convertView no es null,
en lugar de utilizar el lento findViewById() accederemos con getTag() al
ViewHolder y recogeremos de ahí los elementos. A costa de un pequeño
consumo adicional de memoria mejoramos el rendimiento sensiblemente.

1. Haz una copia de la práctica anterior.

2. Añade una clase interna a MiArrayAdapter:


a ) Llámala ViewHolder.
b ) Añade dos atributos públicos de tipo TextView y llámalos titulo
y autor.
3. Modica el método getView() para que:

a ) SiconvertView es null, se cree un objeto ViewHolder, se inicia-


lice con losTextView de la nueva vista recién creada y se guarde
en ella con setTag().

b ) Si se está reciclando una vista anterior, se acceda con getTag()


al objeto ViewHolder y se evite así buscar los TextView por id.

public View getView(int position, View convertView,


ViewGroup parent) {
Libro l = getItem(position);
View v;
ViewHolder vh;
if (convertView == null) {
2
http://developer.android.com/training/improving-layouts/
smooth-scrolling.html#ViewHolder

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
13

v = LayoutInflater.from(getContext()).inflate(
android.R.layout.simple_list_item_2,
parent,
false);
vh = new ViewHolder();
vh.titulo = (TextView) v.findViewById(android.R.id.text1);
vh.autor = (TextView) v.findViewById(android.R.id.text2);
v.setTag(vh);
}
else {
v = convertView;
vh = (ViewHolder) v.getTag();
}

vh.titulo.setText(l.titulo);
if (l.autor != null)
vh.autor.setText(l.autor);
else
vh.autor.setText("");
return v;
} // getView

Práctica 1.8: Diferentes tipos de item

En la práctica anterior, los libros anónimos utilizan una vista en la que


hay dos etiquetas, aunque una, la del autor, no se utilice. Podríamos querer
usar para ellos la vista con una única etiqueta que usamos en la prácti-
getView() decidiremos
ca 1.5. En ese caso, al crear la vista del item en el
si inamos el layout android.R.layout.simple_list_item_1 (en los li-
bros anónimos) o el android.R.layout.simple_list_item_2 (en todos los
demás).

El problema que surge es que las vistas las reutilizamos. Si Android nos
pide que creemos la vista para un libro con autor conocido y nos da una vista
para reutilizar, ésta tendrá que ser del tipo simple_list_item_2 o no nos
valdrá y se perderá la oportunidad de reciclaje. La solución pasa por indicar
a Android que tenemos varios tipos de vista, y permitirle saber de qué tipo
es cada una.

En concreto, sobreescribiremos los siguientes métodos del adaptador:

getViewTypeCount(): devuelve el número de tipos de vista que po-

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
14 Capítulo 1. Listas

dría mostrar la lista. En nuestro caso devolveremos dos.

getItemViewType(int position): debe devolver un número entre 0


y getViewTypeCount() - 1 indicando el tipo de vista que se utiliza
para mostrar el item situado en la posición del parámetro. En nuestro
caso, devolveremos 0 para indicar que es una vista normal (de un libro
con autor) y 1 si es la de un libro anónimo.

Luego, en getView() tendremos, obviamente, que crear la vista que co-


rresponda cuando convertView sea null. Lo importante es que, cuando no
lo sea, sabremos que el tipo de vista que nos han dado es precisamente el que
necesitamos. En este caso eso signica que no hay que preocuparse más de
borrar la etiqueta del autor cuando vayamos a mostrar un usuario anónimo.

1. Haz una copia de la práctica anterior.

MiArrayAdapter los métodos getViewTypeCount() y


2. Añade a la clase
getItemViewType() tal y como se ha descrito antes.

3. Modica getView() para construir el tipo de vista que corresponda


dependiendo de si convertView es null. Elimina el else del if donde
se establecía el autor, dado que ahora sabremos que la segunda etiqueta
nunca existirá cuando el libro sea anónimo.

public int getViewTypeCount() {


return 2;
}

public int getItemViewType(int position) {


return (getItem(position).autor == null)?1:0;
}

public View getView(int position, View convertView,


ViewGroup parent) {
Libro l = getItem(position);
View v;
ViewHolder vh;
if (convertView == null) {
int layoutId;
if (l.autor == null)
layoutId = android.R.layout.simple_list_item_1;
else
layoutId = android.R.layout.simple_list_item_2;
v = LayoutInflater.from(getContext()).inflate(
layoutId,

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
15

parent,
false);
vh = new ViewHolder();
vh.titulo = (TextView) v.findViewById(android.R.id.text1);
if (l.autor != null)
vh.autor = (TextView) v.findViewById(android.R.id.text2);
v.setTag(vh);
}
else {
v = convertView;
vh = (ViewHolder) v.getTag();
}
vh.titulo.setText(l.titulo);
tv = (TextView) v.findViewById(android.R.id.text2);
if (l.autor != null)
vh.autor.setText(l.autor);
// Si el autor es null no tenemos que preocuparnos, porque
// la vista tendrá siempre una única etiqueta.
return v;
} // getView

Práctica 1.9: Pulsación sobre elementos

Si en las prácticas anteriores pulsas sobre cualquier elemento verás un


pequeño efecto que proporciona feedback, pero nada más.

Es posible añadir código para reaccionar ante las pulsaciones de los even-
tos. En este caso, se hace a través del ListView, no del adaptador como he-
mos hecho en las prácticas anteriores. Ten en cuenta que el adaptador sirve
para hacer de puente entre las vistas y los datos (un array o una lista, en el
caso del ArrayAdapter), pero es responsabilidad del ListView preocuparse
de la interacción.

La clase ListView proporciona métodos del tipo setOn*Listener(), pa-


ra añadir oyentes a diferentes eventos que pueden ocurrir sobre la lista. Los
que nos interesan aquí son:

setOnItemClickListener(): recibe un OnItemClickListener (inter-


AdapterVew, superclase de ListView). El interfaz fuerza
faz interno de
a la implementación deun único método, onItemClick(), que recibe
información sobre el evento.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
16 Capítulo 1. Listas

setOnItemLongClickListener(): recibe un OnItemLongClickListener.


En este caso el método a implementar se llama onItemLongClick(),
que debe devolver un booleano indicando si ha consumido o no el even-
to.

1. Haz una copia de la práctica anterior.

2. En el onCreate() añade código al nal del todo para registrarte de los


eventos de OnItemClick y OnItemLongClick de la lista.

3. En ambos casos, haz que se muestre un toast con la posición e identi-


cador del elemento seleccionado.

[...]
lv.setAdapter(aa);
lv.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Toast.makeText(getApplicationContext(),
"Pulsado " + position + ", " + id,
Toast.LENGTH_SHORT).show();
}
});
lv.setOnItemLongClickListener(
new AdapterView.OnItemLongClickListener() {
public boolean onItemLongClick(AdapterView<?> parent,
View view,
int position, long id) {
Toast.makeText(getApplicationContext(),
"Pulsado largo " + position + ", " + id,
Toast.LENGTH_SHORT).show();
return true;
}
});
[...]

¾Ves algo mejorable en este código?

Práctica 1.10: Borrado de elementos

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
17

A lo largo de su uso, la lista puede sufrir modicaciones, con elementos


que se añaden, se borran, o se modican. En esta práctica vamos a ver
un pequeño ejemplo de borrado. En concreto, borraremos un elemento si
hacemos un long click sobre él.

1. Haz una copia de la práctica anterior.

2. Modica el evento de la pulsación simple para que el toast indique que


si se mantiene pulsado se borrará el elemento.

3. Modica el evento de pulsación larga para que se elimine. Para eso,


utiliza el método remove(int) de la lista de libros. Como estamos
utilizando una clase anónima, podrás acceder a la variable local del
método onCreate(), aunque tendrás que declararla final3 .

[...]
lv.setAdapter(aa);
lv.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Toast.makeText(getApplicationContext(),
"Mantén pulsado para borrar",
Toast.LENGTH_SHORT).show();
}
});
lv.setOnItemLongClickListener(
new AdapterView.OnItemLongClickListener() {
public boolean onItemLongClick(AdapterView<?> parent,
View view,
int position, long id) {
Libro l = aa.getItem(position);
Toast.makeText(getApplicationContext(),
"Borrado '" + l.titulo +
"' (" + position + ", " + id + ")",
Toast.LENGTH_SHORT).show();
libros.remove(position);
return true;
}
});
[...]

4. Ejecuta la aplicación y comprueba que no funciona. ¾Qué crees que


está ocurriendo?
3
Si se utilizara Java 8, esto último no sería necesario.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
18 Capítulo 1. Listas

5. La ListView no es consciente del cambio en los datos, y por tanto no


se actualiza. Tras la eliminación, fuerza la propagación del aviso del
cambio desde el adaptador, que tendrás que hacer también final:

aa.notifyDataSetChanged();

6. Lanza la aplicación y comprueba que ahora sí funciona.

7. Tener que noticar el cambio es propenso a olvidos. Para evitarlo,


ArrayAdapter proporciona métodos para manipular la lista de datos
subyacente. Elimina las dos últimas líneas y pide al adaptador el bo-
rrado:

aa.remove(l);

Práctica 1.11: Identicadores estables de los elementos

En la práctica anterior hemos visto que ante un cambio en el modelo


es necesario noticar a la vista que algo ha cambiado. Esa noticación es
global : no se especica qué ha pasado. Por tanto, ListView debe recrear
todo su contenido.
Por desgracia, las vistas las obtiene del adaptador a partir de la posición
de cada item, posiciones que podrían haber cambiado entre actualizaciones.
Por tanto, ListView no tiene, a priori, una forma de hacer un seguimiento
de las vistas que ya tiene y de sus items para evitar recrear muchas vistas.

Para conseguirlo, el adaptador tiene un método en el que se pide el iden-


ticador de un item. Además, el adaptador puede informar de que los iden-
ticadores son estables ante cambios, es decir se devolverá el mismo identi-
cador para un elemento independientemente de su posición. En ese caso, la
ListView podría hacer un seguimiento de los identicadores de los elementos
y sus vistas, y evitar reconstruir las vistas incluso aunque se hayan producido
cambios drásticos.

1. Haz una copia de la práctica anterior.

2. Implementa en el adaptador el método hasStableIds() que devuelva


siempre true.

getItemId(int position) que devuelva


3. Implementa también método
getItem(position).titulo.hashCode();

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
19

4. Ejecuta la aplicación y comprueba que sigue funcionando.

Práctica 1.12: Ordenación de los elementos

Dado que ArrayAdapter guarda los elementos que se muestran, podemos


utilizarlo para que nos los ordene por algún criterio, y luego notique a
la vista el cambio. Para eso, se utiliza su método sort() que recibe un
java.util.Comparable que compara dos elementos de la lista (Libro en
nuestros ejemplos).

1. Haz una copia de la práctica anterior.

2. Modica el layout para añadir dos botones en la parte de abajo, con el


texto Por título y Por autor , situado uno junto al otro. Dene las
cadenas en el chero de recursos, y establece como eventos los métodos
onPorTitulo y onPorAutor.

<RelativeLayout
...>

<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_above="@+id/botones"
/>

<LinearLayout
android:id="@+id/botones"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/porTitulo"
android:onClick="onPorTitulo"
/>

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
20 Capítulo 1. Listas

<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/porAutor"
android:onClick="onPorAutor"
/>
</LinearLayout>

</RelativeLayout>

3. Desde los métodos de los eventos necesitaremos acceder al adaptador,


que ahora es una variable local (y nal) del método onCreate(). Con-
vierte la variable en un atributo.

4. Implementa el código del evento onPorTitulo(). Tendrás que llamar


al método sort() del adaptador. Espera recibir un Comparator que
reciba dos libros y diga cuál va antes. Devuelve un valor para que se
ordene por título y, en caso de empate, por autor.

5. Implementa el código del evento onPorAutor() de una manera equi-


valente a la anterior.

public void onPorTitulo(View v) {


aa.sort(new java.util.Comparator<Libro>() {
@Override
public int compare(Libro lhs, Libro rhs) {
int resultTitulo;
resultTitulo = lhs.titulo.compareToIgnoreCase((rhs.titulo));
if (resultTitulo != 0)
return resultTitulo;
else
// Por simplicidad, asumo que no hay dos libros
// anónimos con el mismo autor.
return lhs.autor.compareToIgnoreCase(rhs.autor);
}
});
} // onPorTitulo

public void onPorAutor(View v) {


aa.sort(new java.util.Comparator<Libro>() {
@Override
public int compare(Libro lhs, Libro rhs) {
if (lhs.autor == null)
if (rhs.autor == null)

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
21

return lhs.titulo.compareTo(rhs.titulo);
else
return -1;
else if (rhs.autor == null)
return 1;
int resultAutor;
resultAutor = lhs.autor.compareToIgnoreCase((rhs.autor));
if (resultAutor != 0)
return resultAutor;
else
return lhs.titulo.compareToIgnoreCase(rhs.titulo);
}
});
} // onPorAutor

Práctica 1.13: Vista especíca para la lista vacía

Si ejecutas la aplicación y borras todos los elementos, la lista queda-


rá completamente vacía, algo que puede resultar confuso para el usuario.
ListView permite que se le establezca una vista para ser mostrada cuando
la lista quede vacía. La vista debe ser hermana del ListView en el layout.
ListView se encargará de hacerse visible a sí misma o a la vista alternativa
en función de si tiene o no elementos.

1. Haz una copia de la práctica anterior.

2. Aunque no es necesario, para que te resulte más cómodo el siguiente


paso pon android:visibility="gone" en la ListView de la actividad.

3. Añade los componentes grácos necesarios para conseguir el resultado


de la gura 1.1.

El icono de aviso es el recurso @android:drawable/ic_dialog_alert.

4. Quita, si quieres, el android:visibility="gone" de la lista.

5. En el onCreate(), congura la lista para establecer como vista especial


para el estado vacío la vista que acabas de crear:

lv.setEmptyView(findViewById(R.id.<id>));

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
22 Capítulo 1. Listas

Figura 1.1: Ejemplo de vista vacía

6. Prueba la aplicación. Elimina todos los libros y observa que la vista


cambia.

Como referencia, el layout podría ser algo así:

<RelativeLayout
android:id="@+id/emptyListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_above="@+id/botones">
<ImageView
android:id="@+id/alertIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@android:drawable/ic_dialog_alert"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/alertIcon"
android:layout_centerHorizontal="true"
android:text="@string/listaVacia"
android:textSize="24sp"
/>
</RelativeLayout>

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
23

Práctica 1.14: ViewStub: optimizando las vistas

En la práctica anterior, el layout contiene una vista que es posible que


no llegue a mostrarse nunca. Si esa vista resultara ser costosa de construir
estaríamos desperdiciando recursos.

Cuando ocurre esto, un modo muy sencillo de mejorar el rendimiento


de nuestras vistas es utilizar ViewStub's. Es un contenedor dummy que no
consume apenas recursos y que en el momento de ser inado (o ser hecho
visible) ocasiona la carga de un layout secundario y se sustituye a sí mismo
por él.

Haz una copia de la práctica anterior.

Crea un nuevo layout y llámalo emptylistlayout.xml. Copia en él el


contenido del layout usado cuando la lista estaba vacía en la práctica
anterior.

Elimina del layout principal de la actividad el de la vista vacía y sus-


titúyelo por un ViewStub. Es importante que pongas su visibilidad en
gone desde el principio, para que no se resuelva durante la inicializa-
ción de la actividad, antes de que la ListView entre en juego. Además,
tendrás que poner el atributo android:layout con el identicador del
layout secundario que se cargará.

<ViewStub
android:id="@+id/emptyListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
< otros android:layout_* que pudieras tener >
android:layout="@layout/emptylistlayout"/>

1. Ejecuta la aplicación. No deberías observar ninguna diferencia.

2. Para comprobar que, efectivamente, estamos retrasando la carga del la-


yout secundario, modifícalo para que sea incorrecto y la aplicación falle
durante su inado. Elimina, por ejemplo, el atributo layout_width.

3. Lanza de nuevo la aplicación. Comprueba a priori que todo funciona


correctamente.

4. Elimina los items de la lista. Nota que en el momento en el que se debe


mostrar la vista vacía la aplicación falla.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
24 Capítulo 1. Listas

Práctica 1.15: ListActivity: ahorrándonos trabajo

1. Haz una copia de la práctica anterior.

2. Haz que la clase MainActivity herede de ListActivity.


3. Modica el layout para que el identicador de la lista sea el que espera
laListActivity, @android:id/list. Haz lo mismo con el de la vista
para la lista vacía @android:id/empty.

4. Elimina de onCreate() la línea en la que establecíamos la vista vacía


(llamando a setEmptyView() de la ListView).

5. Implementa el método protegido onListItemClick(...) con los mis-


mos parámetros y cuerpo que el del listener que ya tienes que mostraba
un toast indicando que se mantuviera pulsado para borrar. Elimina de
onCreate() el establecimiento del listener.

6. Por desgracia, ListActivity no proporciona un método equivalente


para las pulsaciones largas y tendremos que mantener el listener engan-
chado de manera artesanal. No obstante, en el código de onCreate(),
en lugar de buscar la ListView manualmente, utiliza el nuevo método
getListView(). Fíjate que ya no necesitarás la conversión de tipos.

7. Para establecer el adaptador, utiliza el método setListAdapter() de


la clase padre, en lugar de hacerlo a través de la vista que has obtenido
para establecer el listener.

@Override
protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

java.util.ArrayList<Libro> libros;
libros = new java.util.ArrayList<Libro>();
// [ .... ]

aa = new MiArrayAdapter(this, libros);


setListAdapter(aa);

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Notas bibliográcas 25

ListView lv = getListView();
lv.setOnItemLongClickListener(
new AdapterView.OnItemLongClickListener() {
public boolean onItemLongClick(AdapterView<?> parent,
View view,
int position, long id) {
Libro l = aa.getItem(position);
Toast.makeText(getApplicationContext(),
"Borrado '" + l.titulo +
"' (" + position + ", " + id + ")",
Toast.LENGTH_SHORT).show();
libros.remove(position);
return true;
}
});

} // onCreate

@Override
protected void onListItemClick(ListView l, View v,
int position, long id) {
Toast.makeText(getApplicationContext(),
"Mantén pulsado para borrar",
Toast.LENGTH_SHORT).show();
}


Notas bibliográcas
http://developer.android.com/guide/topics/ui/notifiers/toasts.
html
http://developer.android.com/guide/topics/ui/declaring-layout.
html#AdapterViews
http://developer.android.com/guide/topics/ui/layout/listview.
html
http://developer.android.com/reference/android/view/ViewStub.
html
http://developer.android.com/reference/android/app/ListActivity.
html

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Capítulo 2

Fragments
Resumen: En este capítulo veremos los fragments. Aparecidos en
Android 3.0, proporcionan la funcionalidad necesaria para poder crear
interfaces de usuario que se adaptan a pantallas pequeñas (móviles) y
grandes (tabletas).

Práctica 2.1: Dos actividades: detalles de los libros

En esta práctica vamos a añadir una segunda actividad a nuestra apli-


cación de la lista de libros, de manera que si el usuario pulsa sobre un libro
se muestre, en una segunda actividad, su resumen. Seguiremos sin preocu-
parnos de la fuente de esos datos, y cablearemos los resúmenes también en
el código fuente.

1. Haz una copia de la práctica 1.14.

2. Renombra el paquete principal a libro.azul.mislibros.

3. Cambia el título de la aplicación en el chero de cadenas por Mis libros.

4. Elimina el evento que asociamos para borrar los libros.

5. Modica la clase Libro para que guarde una tercera cadena con el
supuesto resumen del libro.

class Libro {

public Libro(String t, String a, String r) {


titulo = t;
autor = a;

27
28 Capítulo 2. Fragments

resumen = r;
}

public String titulo;


public String autor;
public String resumen;

} // class Libro

6. No es necesario modicar el patrón ViewHolder, dado que el resumen


no lo vamos a mostrar en la lista.

7. Modica la inicialización del ArrayList para añadir un resumen (val-


drá añadir cualquier cosa que te parezca).

8. Crea una actividad nueva (usando la plantilla Blank activity). Lláma-


la ResumenLibroActivity, asociada al layout activity_resumen_libro.
Como título pon Resumen del libro. No la marques como actividad
para el lanzador.

9. Comprueba que en el maniesto se ha incluído la declaración de la


nueva actividad.

10. Elimina la parte de conguración del menú, borrando los métodos, el


XML del menú y las constantes de tipo cadena. Elimina también la
cadena de hello_world que no usaremos.

11. Modica el layout para que incluya tres etiquetas, una para el títu-
lo del libro, otra para el autor, y una tercera para el resumen. Pon
identicadores en las tres para poder acceder a ellas posteriormente.
Por comodidad, utiliza un LinearLayout en vertical en lugar de un
RelativeLayout. Haz que las tres etiquetas ocupen todo el espacio a
lo ancho, y que la del resumen ocupe todo el espacio a lo alto. Pon, co-
mo ayuda, un texto  place holder  en cada etiqueta para ver un ejemplo
de texto y que sea más fácil intuir el resultado.

<LinearLayout
...
android:orientation="vertical">

<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tituloLibro"
style="@style/Base.TextAppearance.AppCompat.Title"
android:text="TituloPlaceHolder"/>

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
29

<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/autorLibro"
android:text="AutorPlaceHolder"
style="@style/Base.TextAppearance.AppCompat.Subhead"/>

<TextView android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/resumenLibro"
android:text="Resumen" />

</LinearLayout>

12. Añade un método (protegido) en la actividad setLibro() que reciba


las tres cadenas, y congure las tres etiquetas.

protected void setLibro(String titulo, String autor,


String resumen) {
setLabel(R.id.tituloLibro, titulo);
setLabel(R.id.autorLibro, autor);
setLabel(R.id.resumenLibro, resumen);
} // setLibro

protected void setLabel(int id, String str) {


TextView tv;
tv = (TextView) findViewById(id);
if (str != null)
tv.setText(str);
else
tv.setText("");
} // setLabel

13. En la clase MainActivity, modica el evento asociado a la pulsación de


un elemento de la lista. Hasta ahora mostrábamos un aviso para indicar
que se podía quedar pulsando para borrarlo. Ahora lo que queremos es
lanzar la segunda actividad para ver el resumen del libro. Necesitamos
un Intent que haga una invocación explícita a la carga de la segunda
actividad, indicando directamente su clase:

lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Intent intent = new Intent(getApplicationContext(),
ResumenLibroActivity.class);

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
30 Capítulo 2. Fragments

startActivity(intent);
}
});

14. Desde aquí querríamos enviar los datos del libro seleccionado a la se-
gunda actividad. Por desgracia no somos nosotros quienes construi-
mos el objeto de la clase, por lo que no podemos llamar al método
setLibro() que hicimos antes. Hay que utilizar el bundle del intent.
Añade, antes de la llamada a startActivity(intent):

Libro l = aa.getItem(position);
intent.putExtra("titulo", l.titulo);
intent.putExtra("autor", l.autor);
intent.putExtra("resumen", l.resumen);

15. Lo que hemos hecho es meter en la tabla hash de comunicación con
la segunda actividad el título, el autor y el resumen utilizando como
titulo, autor y resumen. En la clase de la actividad
índice las cadenas
secundaria, modica el métodoonCreate(...) y añade después del
setContentView(...):

Bundle extras = getIntent().getExtras();


if (extras != null) {
setLibro(extras.getString("titulo"),
extras.getString("autor"),
extras.getString("resumen"));
}

16. Prueba la aplicación. Comprueba que si pulsas sobre un libro, se abre


la segunda ventana con los detalles del libro.

17. Haber utilizado directamente cadenas como claves en el bundle no es


ResumenLibroActivity tres constantes (public
muy limpio. Declara en
static final) de tipo String, y utilizalas tanto en los putExtra()
como en el getString().

Práctica 2.2: Repasando el ciclo de vida

Antes de seguir, vamos a repasar el ciclo de vida de las actividades. Vamos


a crear una nueva actividad manualmente (sin layout ) en la que sobreescri-
biremos todos los métodos del ciclo de vida para mostrar un mensaje de log.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
31

Luego haremos que las dos actividades que hicimos en la práctica anterior
hereden de ella, de modo que nos informen en cada evolución de su estado.
Probaremos luego la aplicación para observar cuando suceden.

Las guras muestran el ciclo de vida de las actividades tal y como se


describe en la documentación de Android.

Figura 2.1: Bucles del ciclo de vida de una actividad

1. Haz una copia del proyecto de la práctica anterior.

2. Haz una nueva clase Java y llámala SpyActivity. No utilices el asis-


tente de creación de actividades: no queremos que el IDE nos cree
automáticamente el XML del layout, ni añada cadenas, o modique el
chero de maniesto.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
32 Capítulo 2. Fragments

Figura 2.2: Diagrama de estados de una actividad

3. Haz que la nueva clase herede de ActionBarActivity, la superclase de


nuestras actividades.

4. Captura todos los eventos del ciclo de vida, llama al método de la


superclase y escribe en el log una nota de la invocación.

public class SpyActivity extends ActionBarActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

if (savedInstanceState == null)
android.util.Log.i(TAG, "onCreate()");
else
android.util.Log.i(TAG, "onCreate(" +
savedInstanceState + ")");

} // onCreate

@Override
protected void onStart() {
android.util.Log.i(TAG, "onStart()");
super.onStart();
}

@Override
protected void onRestart() {

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
33

android.util.Log.i(TAG, "onRestart()");
super.onRestart();
}

@Override
protected void onResume() {
android.util.Log.i(TAG, "onResume()");
super.onResume();
}

@Override
protected void onPause() {
android.util.Log.i(TAG, "onPause()");
super.onPause();
}

@Override
protected void onStop() {
android.util.Log.i(TAG, "onStop()");
super.onStop();
}

@Override
protected void onDestroy() {
android.util.Log.i(TAG, "onDestroy()");
super.onDestroy();
}

//---------------------------------------------

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
android.util.Log.i(TAG, "onSaveInstanceState(" +
savedInstanceState + ")");
super.onSaveInstanceState(savedInstanceState);
}

@Override
protected void onRestoreInstanceState
(Bundle savedInstanceState) {
android.util.Log.i(TAG, "onRestoreInstanceState(" +
savedInstanceState + ")");
super.onRestoreInstanceState(savedInstanceState);
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
34 Capítulo 2. Fragments

//---------------------------------------------

private static final String TAG = "MainActivity";

} // MainActivity

Lee cada una de las siguientes pruebas y trata de predecir qué mensajes
se van a generar. Luego comprueba si has acertado y trata de explicar las
discrepancias.

5. Lanza la aplicación.

6. Pulsa el botón de volver (escape en el teclado).

7. Busca la aplicación instalada y vuelve a lanzarla.

8. Pulsa sobre el botón para ir al escritorio (no el botón de volver).

9. Busca la aplicación instalada y vuelve a lanzarla.

10. Gira el dispositivo. En un AVD se consigue, por ejemplo, con el nú-


mero 7 del teclado numérico si no está activado, o con la combinación
Ctrl + F11 (o Ctrl + F12).

11. Pulsa sobre cualquier libro.

12. Pulsa la tecla Volver.

13. Vuelve a pulsar sobre cualquier libro.

14. Gira de nuevo el dispositivo.

15. Pulsa Volver.

16. Pulsa de nuevo sobre cualquier libro.

17. Gira el dispositivo dos veces.

18. Pulsa Volver.

19. Cierra la aplicación pulsando una última vez Volver.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
35

Práctica 2.3: Fragmentos: aprovechando la pantalla de las


tablets

El patrón de actividad con una lista y actividad con los detalles de un


elemento es muy habitual en Android. Pero no queda bien en pantallas gran-
des.

1. Crea un nuevo AVD de tipo tablet. Por eciencia, procura evitar reso-
luciones excesivamente grandes como mucha densidad de pantalla. Por
ejemplo, crea una tablet antigua, de 7"WSVGA (600x1024, mdpi).

2. Lanza el AVD, y ponlo en orientación apaisado.

3. Lanza la aplicación de la práctica anterior sobre el nuevo AVD, y ob-


serva el aparente desperdicio de espacio. En el lado derecho podríamos
perfectamente ver el resumen del libro seleccionado.

Para resolver este problema aparecieron los fragmentos, que no es más


que una porción de un interfaz de usuario. La ventaja de los fragments es
la versatilidad: podemos crear un fragmento y decidir en ejecución dónde
incrustarlo, y en qué circunstancias.

En esta práctica vamos a hacer uso de fragments para tener un inter-


faz modulable. De momento asumiremos que nuestra aplicación se ejecutará
siempre un una tablet apaisada, de modo que no nos preocuparemos de con-
seguir que funcione bien en móviles.

Lo primero que vamos a hacer es convertir a la actividad secundaria en


un fragmento, y la incorporaremos en la otra, temporalmente, para ver el
resultado.

1. Haz una copia del proyecto de la práctica anterior.

2. Renombra la clase ResumenLibroActivity por ResumenLibroFragment.

3. Renombra el layout del chero activity_resumen_libro.xml y llá-


malo fragment_resumen_libro.xml. Observa cómo se actualiza auto-
máticamente el uso del recurso R en el onCreate().

4. Modica la superclase para que en lugar de heredar de SpyActivity


herede de Fragment. Dado que esta clase apareció en Android 3.0 por
primera vez, si queremos que nuestra aplicación funcione en versiones
anteriores tendremos que heredar de la reimplementación en la librería
de compatibilidad, en el paquete android.support.v4.app..

5. Aunque no es importante, crea un constructor por defecto (sin pará-


metros) y sin código. Siempre debe haber un constructor por defecto
(al que llamará Android por nosotros).

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
36 Capítulo 2. Fragments

6. Elimina del chero de maniesto (AndroidManifest.xml) la entrada


de la actividad.

Los fragmentos tienen su propio ciclo de vida, integrado en el de la acti-


vidad en el que se conectan. La vista que contienen deben devolverla como
resultado de la ejecución de su método onCreateView(...), en lugar de esta-
blecerla en el onCreate() como tenemos ahora. El método onCreateView()
tiene varios parámetros:

LayoutInflater: un objeto capaz de construir una vista a partir de un


recurso de tipo layout. Nos lo pasan como parámetro ya congurado
para que no tengamos que conseguirlo nosotros. Los fragmentos no son
contextos (como las actividades).

ViewGroup: contenedor donde nos incluirán. Lo usaremos al llamar al


método inflate para que pueda obtener información sobre el espacio
disponible.

Bundle: información persistente para reconstruir el contenido.

Fíjate que el último parámetro no es el que utilizábamos en la actividad


con getIntent().getExtras(), dado que ese era el que se había estable-
cido al hacer la invocación explícita. Ahora no van a poder lanzarnos así
(ya no somos una actividad), por lo que de momento nos olvidamos de la
inicialización del contenido.

1. Elimina el onCreate() y escribe el nuevo método, onCreateView(). Es


importante que no incluyas la vista que construyas en el padre, porque
lo hará Android por nosotros.

public View onCreateView(LayoutInflater inflater,


ViewGroup container,
Bundle savedInstanceState) {

return inflater.inflate(R.layout.fragment_resumen_libro,
container, false);
}

2. Los fragmentos no tienen el método findViewById(), por lo que el mé-


todo setLabel() tendrá un error. El TextView tenemos que buscarlo
en nuestra vista. Modica la linea del error:

tv = (TextView) getView().findViewById(id);

3. Ejecuta la aplicación. Pulsa sobre cualquier elemento de la lista y com-


prueba que la aplicación falla. ¾Por qué? ¾Por qué no lo hizo antes?

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
37

Lo que queremos ahora es incluir el fragmento que acabamos de hacer


dentro de la ventana de la lista. Este paso lo desharemos más adelante, pero
nos sirve para comprender y probar la losofía de los fragmentos. Actual-
mente el layout de la actividad contiene un RelativeLayout en la raíz con
la lista y los dos botones. Lo que queremos es que ese RelativeLayout sea
un elemento más, que tenga a su derecha al fragmento. Vamos a hacer que
la lista utilice un tercio de la pantalla, y el fragmento del resumen utilice los
otros dos tercios.

1. Abre el chero activity_main, utilizando la vista del XML.

2. Encierra el contenido que aparece dentro de un LinearLayout que


será el nuevo nodo raíz. Mueve las deniciones de los namespaces que
tiene el RelativeLayout (atributos xmlns:android y xmlns:tools al
nuevo nodo raíz.

3. Congura el LinearLayout para que cupe todo el espacio disponible y


tenga una disposición (android:orientation) horizontal.

4. Modica la conguración del RelativeLayout para poner como ancho


0dp y como layout_weight un 1. Para que el peso se aplique el ancho
debe ser 0. El peso se sumará con el peso del resto de nodos hermanos,
y se repartirá en función de su tamaño relativo.

5. Añade un nuevo nodo, hermano del RelativeLayout original que sea


el fragment. Pon también como ancho 0dp, y como layout_weight
un 2 (el doble que para la lista). Establece la clase que contiene el
fragmento.

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">

<RelativeLayout
android:layout_weight="1"
android:layout_width="0dp"
....>

[ ... CONTENIDO ORIGINAL ... ]

</RelativeLayout>

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
38 Capítulo 2. Fragments

<fragment
android:id="@+id/resumenFragment"
android:layout_width="0dp" android:layout_weight="2"
android:layout_height="match_parent"
android:name="libro.azul.mislibros.ResumenLibroFragment"/>

</LinearLayout>

6. Lanza la aplicación en la tablet. Observa el resultado.

Si ejecutas la aplicación en un móvil, verás que la disposición se mantiene,


pero al haber mucho menos hueco el resultado es agobiante. Si pones el móvil
en apaisado, el aspecto es algo más razonable.

Si queremos que reaccione ante la pulsación de un libro, ya no podemos


usar un intent, porque no tenemos una actvidad. Además, tampoco podemos
utilizarfindViewById() porque eso nos daría una vista, y no queremos tener
setLibro()
que lidiar nosotros con ella. Lo que queremos es llamar al método
del objeto de la clase ResumenLibroFragment que se ha creado para nuestro
fragmento.

Para eso, cada actividad tiene un gestor de fragmentos que almacena los
fragmentos que se están mostrando. Si estás utilizando una versión posterior
al API level 10, la actividad tendrá el método getFragmentManager() que
nos lo devuelve.

Sin embargo, aquí estamos utilizando todo el tiempo la librería de com-


patibilidad, y las clases nativas no son intercambiables con ellas. Nues-
tro fragmento no es un android.app.Fragment, sino un objeto de la cla-
se android.support.v4.app.Fragment, y por tanto no es gestionado por
el gestor devuelto por getFragmentManager(), sino por el devuelto por
getSupportFragmentManager().
En el código de la pulsación de un elemento de la lista pon el código
siguiente:

lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Libro l = aa.getItem(position);
ResumenLibroFragment rlf;
rlf = (ResumenLibroFragment) getSupportFragmentManager().
findFragmentById(R.id.resumenFragment);
rlf.setLibro(l.titulo, l.autor, l.resumen);
}
});

Ahora toca convertir en un fragmento también la lista, y luego tendremos


una actividad con un layout nuevo que tendrá dos los fragmentos.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
39

1. Deshaz los cambios en el layout de la actividad principal, para que no


tenga el fragmento del resumen del libro, ni el LinearLayout. Quita
también ellayout_weight del RelativeLayout, y vuelve a poner en
match_parent el ancho.

2. Dado que ahora el layout de la actividad ya no tiene el fragmento, el


código en el evento de la pulsación de un item ha dejado de compilar.
Quita el código.

3. Renombra el chero del layout a fragment_lista_libros.xml. Ob-


serva cómo el IDE nos ha cambiado automáticamente la referencia a
él en el onCreate() de la, de momento, actividad.

4. Renombra la clase MainActivity por ListaLibrosFragment. Observa


cómo cambia, entre otras muchas cosas, el chero de maniesto.

5. Haz que ListaLibrosFragment herede de Fragment. De nuevo, utili-


zaremos android.support.v4.app.Fragment de la librería de compa-
tibilidad.

6. En el chero de maniesto, elimina la referencia a la actividad, que


ya no es tal. Habremos dejado a la aplicación sin ninguna actividad.

7. Como antes, ahora tenemos que construir la vista sobreescribiendo el


método onCreateView(), no en el onCreate(). Además, los fragmen-
tos no son contextos, por lo que no podemos usarlo para construir el
adaptador:

@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {

// Creamos la lista de libros.


// [ ... código original de inicialización de datos ... ]

View result = inflater.inflate(R.layout.fragment_lista_libros,


container, false);

aa = new MiArrayAdapter(getActivity(), libros);


ListView lv = (ListView) result.findViewById(R.id.listView);
lv.setAdapter(aa);
lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Libro l = aa.getItem(position);

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
40 Capítulo 2. Fragments

// PENDIENTE!
}
});
lv.setEmptyView(findViewById(R.id.emptyListView));
return result;

} // onCreateView

Con todos estos cambios hemos conseguido que lo que antes eran activi-
dades, ahora sean fragmentos. Lo que nos falta es tener una actividad que
tenga ambos fragmentos.

1. Crea una nueva actividad. Vuelve a utilizar el nombre MainActivity


que hemos hecho desaparecer al convertirla en un fragment. Asegúrate
de que la marcas como actividad para el lanzador (Launcher activity ).

2. Elimina las cosas habituales que nos mete el asistente: métodos de


gestión del menú, menu_main.xml y las dos cadenas.

3. Comprueba que hemos vuelto a tener una actividad en el chero de


maniesto.

4. Modica el layout de la actividad para que incluya los dos fragmentos.


Utiliza un LinearLayout en horizontal como hicimos antes, ajustando
los pesos.

5. Elimina los android:padding_* de los layout de los dos fragmentos,


para que no quede tanto espacio. Los incluyó el asistente cuando hizo
los layouts pensando en que fueran usados en una actividad; al ser
fragmentos, el espacio debe ser responsabilidad de la actividad que lo
incluya. De hecho, en el layout de la actividad principal añadiremos
margen entre los dos fragmentos.

<LinearLayout
... >

<fragment
android:id="@+id/libroFragment"
android:layout_width="0dp" android:layout_weight="1"
android:layout_height="match_parent"
android:layout_marginRight="12dp"
android:name="libro.azul.mislibros.ListaLibrosFragment">
</fragment>

<fragment

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
41

android:id="@+id/resumenFragment"
android:layout_width="0dp" android:layout_weight="2"
android:layout_height="match_parent"
android:name="libro.azul.mislibros.ResumenLibroFragment">
</fragment>

</LinearLayout>

6. Ejecuta la práctica. Comprueba que al seleccionar los diferentes ele-


mentos de la lista el fragmento del resumen no se actualiza.

7. Pulsa sobre alguno de los botones de la ordenación. Verás que la apli-


cación acaba con error.

La detección de la pulsación de los botones estaba hecha directamente en


el layout usando android:onClick. Android busca los métodos indicados en
el XML en la actividad en la que está el botón. Sin embargo, esos métodos
los tenemos en el fragmento.

Podríamos moverlos a la actividad pero no es buena idea. Los fragmen-


tos se utilizan para encapsular en una clase (y un layout ) una porción del
interfaz; es preferible que toda la lógica del fragmento la gestione él mismo.

La única forma de controlar los eventos de los botones en un fragmento


es capturando el evento explícitamente. Es decir, tendremos que registrar un
listener a mano por código para que llame a nuestros métodos.

1. Abre fragment_lista_libros y quita los atributos onClick de los


dos botones, y añadeleandroid:id para poder referirnos a ellos desde
código (por ejemplo @+id/botonPorTitulo y @+id/botonPorAutor.

2. Para capturar los eventos, añade en el onCreateView() el código de


registro en los listener :

Button b = (Button) result.findViewById(R.id.botonPorTitulo);


b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onPorTitulo(v);
}
});
b = (Button) result.findViewById(R.id.botonPorAutor);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onPorAutor(v);
}
});

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
42 Capítulo 2. Fragments

Fíjate que ese es el código que nos había ahorrado Android hasta ahora
al permitir poner el nombre del método en el layout.

Ya sólo nos falta que al pulsar sobre un elemento de la lista se actualice


el resumen. La detección de la pulsación la detectamos en el fragmento, y
lo que queremos es modicar el fragmento hermano. Podríamos, desde el
fragmento de la lista, acceder a la actividad, y buscar en él el fragmento
del resumen para manipularlo. Pero eso añade una dependencia entre los
fragmentos, lo que diculta la reutilización.

El esquema habitual es que el propio fragmento tenga un listener al que


avise de que se ha seleccionado un elemento nuevo, y que la actividad se
registre en él, y sea la actividad la que decida qué hacer.
De hecho, normalmente el fragmento asume que la actividad que lo está
incluyendo implementa el interfaz y la registra como oyente automáticamen-
te.

1. Añade en ListaLibrosFragment un interfaz interno público.

public interface OnLibroSeleccionadoListener {


public void onLibroSeleccionado(Libro l);
}

2. Añade en ListaLibrosFragment un atributo nuevo que guarde un ob-


jeto que implemente ese interfaz. Llámalo_listener.
3. Haz que la actividad principal lo implemente. Deja de momento el
código vacío.

public class MainActivity extends ActionBarActivity implements


ListaLibrosFragment.OnLibroSeleccionadoListener {

// [ ... onCreate ... ]

@Override
public void onLibroSeleccionado(ListaLibrosFragment.Libro l) {
}

4. En el código del evento de la pulsación de un elemento de la lista que


dejamos pendiente en el onCreateView, mira si el listener no es null,
y llama al método del oyente en ese caso.

lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
43

Libro l = aa.getItem(position);
if (_listener != null)
_listener.onLibroSeleccionado(l);
}
});

Ahora lo que tenemos que hacer es que la actividad se registre para que
reciba las noticaciones. En lugar de meter métodos para registro y desregis-
tro y que la actividad se registre manualmente, aprovechamos dos métodos
del ciclo de vida de los fragments y registramos a la actividad automática-
mente. Añade al fragmento el siguiente código:

@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
_listener = (OnLibroSeleccionadoListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " debe implementar OnLibroSeleccionadoListener");
}
}

@Override
public void onDetach() {
super.onDetach();
_listener = null;
}

El método onAttach() es llamado automáticamente cuando un frag-


mento se engancha en una actividad. Lo que hacemos es coger esa
actividad y registrarla automáticamente como nuestro oyente. Si la
actividad no implementa el interfaz, generamos una excepción que de-
tendrá la aplicación. Esto signica que nuestro fragmento exige que se
implemente el interfaz siempre (¾para qué si no nos han incluído?)

El método onDetach() es llamado cuando un fragmento se va a desaco-


plar de la actividad que lo contiene. Aprovechamos para desregistrar
a la actividad como oyente. Fíjate que como un fragmento sólo puede
estar conectado con una actividad no necesitamos una lista de oyentes.

Ya sólo falta reaccionar ante el evento. En la clase MainActivity mete


un código similar al que ya usamos antes, pidiendo al gestor de fragmentos
que nos dé el del resumen, y llamando a su método setLibro().

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
44 Capítulo 2. Fragments

@Override
public void onLibroSeleccionado(Libro l) {

ResumenLibroFragment rlf;
rlf = (ResumenLibroFragment) getSupportFragmentManager().
findFragmentById(R.id.resumenFragment);
rlf.setLibro(l.titulo, l.autor, l.resumen);

Lanza la aplicación, y comprueba que funciona. No obstante, al lanzar la


aplicación en el fragmento de los detalles se ven los place holders.

1. Modica el layout del fragmento de los detalles para que el nodo raíz
sea un FrameLayout que tenga dentro al LinearLayout actual.

2. Añade, fuera del LinearLayout, un TextView centrado en la ventana,


con el texto  No has seleccionado ningún libro

3. Pon en el LinearLayout la visibilidad en gone.

<FrameLayout ...>

<TextView
android:id="@+id/tvLibroNoSeleccionado"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/libroNoSeleccionado"
style="@style/Base.TextAppearance.AppCompat.Headline"/>

<LinearLayout
android:id="@+id/panelLibro"
...
android:visibility="gone">

[ ... Contenido original ... ]

</LinearLayout>
</FrameLayout>

4. Modica el método setLibro() para que se asegure de hacer visible el


LinearLayout TextView.
e invisible el

public void setLibro(String titulo, String autor,

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
45

String resumen) {

setVisibility(R.id.tvLibroNoSeleccionado, View.GONE);
setVisibility(R.id.panelLibro, View.VISIBLE);
setLabel(R.id.tituloLibro, titulo);
...

protected void setVisibility(int id, int visibility) {


getView().findViewById(id).setVisibility(visibility);
}

Práctica 2.4: Fragmentos en actividades diferentes

Si la práctica anterior la ejecutas en un dispositivo con una pantalla


pequeña, sufrirás evidentes problemas de espacio. Para resolverlo lo que que-
rríamos es que se mostrara únicamente la lista de libros, y al pulsar sobre
uno de ellos se mostrara su resumen. Vamos a modicar la práctica anterior
para conseguirlo.

1. Haz una copia de la práctica anterior.

2. Crea un nuevo chero de recurso, activity_main. Ponle el modicador


de tamaño (Size ) en Large.

3. Observa que ahora tendrás dos versiones del mismo chero, uno de
ellos marcado como large/.
4. Copia el contenido del activity_main anterior a la nueva versión.

5. Modica la versión predenida (para pantallas pequeñas y norma-


les) y quita el segundo fragment. Dado que ahora sóplo tenemos un
elemento, modica el LinearLayout para que sea un FrameLayout. Re-
visa el fragment para poner match_parent en el ancho (y no usar el
peso) y quita el margen.

6. Lanza un AVD pequeño y prueba la práctica sobre él. Observa que se


utiliza el nuevo layout. Si pulsas sobre cualquier elemento la aplicación
fallará. ¾Por qué?

7. Crea una actividad nueva. Llámala ResumenActivity.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
46 Capítulo 2. Fragments

8. Modica el layout de la nueva actividad para que sea similar a la


de la actividad principal, pero utilizando el fragmento del resumen.
Puedes copiar la denición del fragmento de la versión large del layout.
Además, de nuevo por eciencia, utiliza un FrameLayout (no pongas
0dp en el ancho, y no pongas peso en el fragmento).

9. En la actividad principal, cuando se pulse sobre cualquier elemento


tenemos ahora que decidir qué hacer. Si el layout que se ha cargado
tiene el fragmento del resumen, lo utilizaremos para mostrarlo. Si no,
necesitaremos lanzar la actividad secundaria. El código para hacerlo
será equivalente al que utilizamos en la práctica 2.1.

@Override
public void onLibroSeleccionado(Libro l) {

ResumenLibroFragment rlf;
rlf = (ResumenLibroFragment) getSupportFragmentManager().
findFragmentById(R.id.resumenFragment);
if (rlf != null)
// Estamos en una disposición con los dos fragmentos.
// Actualizamos el contenido.
rlf.setLibro(l.titulo, l.autor, l.resumen);
else {
// Tenemos que lanzar la segunda actividad.
Intent intent = new Intent(getApplicationContext(),
ResumenActivity.class);
intent.putExtra(ResumenLibroFragment.TITULO, l.titulo);
intent.putExtra(ResumenLibroFragment.AUTOR, l.autor);
intent.putExtra(ResumenLibroFragment.RESUMEN, l.resumen);
startActivity(intent);
}

} // onLibroSeleccionado

10. Lanza la aplicación en el móvil. Al elegir ahora un elemento, se abre la


actividad secundaria con el fragmento de los detalles. Por el momento,
sin embargo, no aparece ninguno.

11. En el onCreate() de la segunda actividad, recupera los datos extra


del intent, y utiliza un código similar al de la actividad principal para
obtener el fragment y llamar a su método setLibro() con los datos.

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

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
47

setContentView(R.layout.activity_resumen);

ResumenLibroFragment rlf;
rlf = (ResumenLibroFragment) getSupportFragmentManager().
findFragmentById(R.id.resumenFragment);

Bundle extras = getIntent().getExtras();


if (extras != null) {
rlf.setLibro(
extras.getString(ResumenLibroFragment.TITULO),
extras.getString(ResumenLibroFragment.AUTOR),
extras.getString(ResumenLibroFragment.RESUMEN));
} // if hay extras

} // onCreate

Práctica 2.5: Fragmentos: ciclo de vida

Los fragmentos también tienen su propio ciclo de vida, gestionado por


la actividad. No es Android el que se encarga de llamarla, sino que las acti-
vidades se enriquecieron (con el gestor de fragmentos) para ir informando a
los fragmentos de los eventos importantes.

Los fragmentos tienen los mismos métodos del ciclo de vida que las activi-
onRestart() y onRestoreInstanceState() que pasa a llamarse
dades salvo
onViewStateRestored(). Se añaden además métodos nuevos, algunos de los
cuales ya hemos visto.

1. Haz una copia de la práctica anterior.

2. Crea una nueva clase Java y llámala SpyFragment. Haz que herede de
android.support.v4.app.Fragment.

3. Para evitar escribir mucho, copia el código de todos los métodos de la


clase SpyActivity, dado que muchos se comparten. Copia también la
denición del atributo TAG.

4. Convierte a públicos todos los métodos que acabas de copiar.

5. Borra el método onRestart(), que no existe en los fragmentos.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
48 Capítulo 2. Fragments

Figura 2.3: Ciclo de vida de un fragment

6. Renombra el método onRestoreInstanceState para que tenga el nom-


bre usado en los fragmentos,onViewStateRestored, con el mismo pa-
rámetro. Ajusta el mensaje de log convenientemente.

7. Sobreescribe los métodos especícos del ciclo de vida de los fragmentos.


Crea además un constructor por defecto y escribe un mensaje en en
log.

public SpyFragment() {
android.util.Log.i(TAG, "<constructor>");
}

@Override
public void onAttach(Activity activity) {
android.util.Log.i(TAG, "onAttach(" + activity + ")");

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
49

Figura 2.4: Emparejamiento de cada evento del ciclo de vida

super.onAttach(activity);
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
android.util.Log.i(TAG, "onCreateView()");
return super.onCreateView(inflater,
container, savedInstanceState);
}

@Override
public void onActivityCreated(
@Nullable Bundle savedInstanceState) {
if (savedInstanceState == null)
android.util.Log.i(TAG, "onActivityCreated()");
else
android.util.Log.i(TAG, "onActivityCreated(" +
savedInstanceState + ")");
super.onActivityCreated(savedInstanceState);
}

@Override
public void onDestroyView() {
android.util.Log.i(TAG, "onDestroyView()");
super.onDestroyView();

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
50 Capítulo 2. Fragments

@Override
public void onDetach() {
android.util.Log.i(TAG, "onDetach()");
super.onDetach();
}

8. Modica los dos fragmentos para que hereden de nuestra nueva clase.
Modica también la actividad principal para que herede de SpyActivity,
si no lo hacía ya.

9. Ejecuta la aplicación y ciérrala. Observa cómo los eventos principales


ocurren primero en la actividad e inmediatamente después en los frag-
mentos. Además, algunas veces los eventos adicionales de un mismo
fragmento van juntos y otras veces no.

Práctica 2.6: Conguración diferente en horizontal y vertical

En la práctica 2.4 conseguimos tener dos fragmentos en dispositivos de


tamaño grande, y dos actividades en dispositivos más pequeños. Sin embargo,
podríamos querer mostrar los dos fragmentos en móviles apaisados, y dejar
el funcionamiento de dos actividades únicamente para móviles en vertical
(retrato).

Para eso, tendremos que revisar nuestros layouts. Ahora mismo tenemos
como layout predenido el que muestra un único fragmento, que sobrees-
cribimos para pantallas grandes con la versión de dos. Lo que queremos es
que esa misma versión se utilice también en pantallas pequeñas en apaisado.
Podríamos cambiar el esquema y poner como predenido la versión con dos
fragmentos, y sobreescribirlo con la versión de uno en móviles pequeños en
modo retrato.

En lugar de hacer eso, vamos a aprovechar para ver cómo utilizar un


mismo recurso en dos conguraciones diferentes a la predenida usando un
alias para evitar tener el mismo chero dos veces.

1. Haz una copia de la práctica anterior.

2. Entra en el directorio layout-large de los recursos, y renombra el -


chero activity_main.xml por activity_main_twopanes.xml y mue-
velo al directorio layout. Ten en cuenta que cualquier nombre servirá.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
51

3. Crea un nuevo activity_main para los dispositivos de pantalla grande,


y crea un alias al layout que acabas de renombrar:

<?xml version="1.0" encoding="utf-8"?>


<merge>
<include layout="@layout/activity_main_twopanes"/>
</merge>
4. Crea un directorio layout-land y copia el mismo chero con el alias.

5. Lanza la aplicación en un móvil, y comprueba que al cambiar la orien-


tación, se pasa de una representación a otra.

6. Observa los mensajes del ciclo de vida. Dependiendo de cuántos frag-


mentos haya en la orientación destino, se pasa por el ciclo en el de la
lista únicamente, o en los dos.

7. Cierra la aplicación en el móvil, y vuelve a lanzarla con el móvil en


vertical (modo retrato).

8. Pulsa sobre cualquier libro, y observa que salta la actividad correcta-


mente.

9. Pulsa Volver para volver a la actividad principal, y rota el móvil.


Verás la versión con los dos fragmentos.

10. Pulsa sobre cualquier libro y verás su resumen.

11. Rota el móvil, y volverás a la lista.

12. Pulsa sobre cualquier libro. ¾Qué ocurre y por qué?

13. Los fragmentos que se han creado en algún momento se recrean auto-
máticamente, aunque luego puede que no se asocien a ningún lugar de
la disposición. En la actividad principal, al seleccionar un libro mirá-
bamos si existía el fragmento del resumen a partir de su identicador,
y en ese caso establecíamos el libro. Ahora el fragmento existe, y con
ese identicador, pero no tiene la vista creada (no se ha lanzado su
ciclo de vida), por lo que falla. Tenemos que mejorar esa condición:

public void onLibroSeleccionado(Libro l) {

// ...
if ((rlf != null) && rlf.isInLayout())
rlf.setLibro(l.titulo, l.autor, l.resumen);
else {
...
}
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
52 Capítulo 2. Fragments

14. Lanza de nuevo la aplicación y comprueba que ahora funciona bien.

15. Con el móvil en modo retrato, selecciona un libro para ver su resumen.

16. Gira el móvil para verlo en apaisado. ¾Es razonable lo que ves? Pulsa
Volver. ¾Qué ocurre ahora? ¾Cómo podemos evitarlo?

17. La actividad que muestra el resumen no queremos que sea visible nunca
en modo apaisado. La solución es ajustar el método onCreate() para
que si detecta durante el onCreate() que la conguración es apaisada,
se nalice a sí misma.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getResources().getConfiguration().orientation ==
Configuration.ORIENTATION_LANDSCAPE) {
finish();
return;
}

setContentView(R.layout.activity_resumen);

...

} // onCreate

Práctica 2.7: Dos fragmentos dinámicos

En las prácticas anteriores hemos hecho un uso estático de los fragments,


dado que en los cheros de layout está integrado directamente la posición de
cada uno, y la clase con la lógica que hay que instanciar.

Una aproximación diferente es hacer uso de fragmentos dinámicos. En


los layout colocamos ViewGroup's como contenedores vacíos, sobre los que
desplegaremos los fragmentos en ejecución.

Vamos a empezar otra vez, cconstruyendo la aplicación, asumiendo que


sólo queremos que se utilice en pantallas grandes donde mostraremos los dos
fragmentos, tal y como hicimos inicialmente en la práctica 2.3.

1. Haz una copia de la práctica anterior.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
53

2. Renombra el paquete principal por libro.azul.mislibrosdinamicos,


dado que vamos a hacer cambios drásticos en la losofía de funciona-
miento. Modica la cadena app_name para hacer más explícita la dife-
rencia y poder distinguir ambas versiones instaladas en el dispositivo.
Pon, por ejemplo, Mis libros (fragments dinámicos).

3. Dado que vamos a asumir siempre que tenemos visibles los dos pa-
neles, elimina los alias del layout de pantallas grandes y apaisadas.
Elimina la versión predenida con un solo panel, y renombra el chero
activity_main_twopanes.xml para que sea activity_main.xml.

4. Modica el layout para convertir los elementos <fragment> en elemen-


tos<FrameLayout> que serán nuestros contenedores de los fragmentos.
Elimina en ambos el atributo android:name que ya no se utiliza y
mueve el marginRight del panel izquierdo al derecho, convirtiéndolo
en un marginLeft.

5. Ejecuta la aplicación. Comprueba que no aparece nada.

Dado que no hemos metido los fragments en el layout, tenemos que me-
terlos por código en el método onCreate(). La gestión de los fragments de
la actividad la realiza la clase FragmentManager, que ya hemos utilizado en
algún momento para buscar fragmentos en la actividad. Si queremos aña-
dir, eliminar o sustituir fragmentos, tendremos que hacer uso también de esa
clase.

La modicación de los fragmentos se realiza a través de transacciones.


Para añadir un nuevo fragmento, le diremos al FragmentManager que que-
remos iniciar una transacción, luego indicaremos los cambios que queremos
hacer, y nalmente indicaremos que hemos terminado y deben ejecutarse
nuestros cambios.

1. En el código del método onCreate() de la actividad principal, pide


al gestor de los fragmentos una nueva transacción, indica que deseas
añadir un fragmento de tipo lista en el panel izquierdo, y consolida la
transacción.

ListaLibrosFragment llf = new ListaLibrosFragment();


FragmentTransaction transaction;
transaction = getSupportFragmentManager().beginTransaction();
transaction.add(R.id.contenedorListaLibros, llf);
transaction.commit();

2. Ejecuta la aplicación y comprueba que aparece nuestro fragment. Pulsa


Volver para cerrarla.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
54 Capítulo 2. Fragments

3. El uso de transacciones permite al sistema mantener una pila de vuel-


ta, de modo que le podemos pedir que recuerde cada transacción que
ha realizado, y la deshaga cuando se pulse el botón Volver. Aunque
en principio en este momento no tenga ningún sentido dentro de la
aplicación, antes de consolidar la transacción llamando a commit pide
al sistema que añada la transacción actual a la pila y la recuerde:

transaction.addToBackStack(null);

1. Ejecuta la aplicación y comprueba que no parece haber cambiado nada.

2. Pulsa el botón Volver. Observa que el fragmento desaparece al des-


hacerse la última transacción.

3. Vuelve a pulsar Volver. La aplicación se cierra.

4. Lanza de nuevo la aplicación. Rota el dispositivo.

5. Pulsa Volver. No parecerá ocurrir nada. Pulsa una segunda vez y


observa que, ahora sí, el fragmento desaparece.

6. Relanza la aplicación, rota el dispositivo dos veces, e intenta cerrar el


programa pulsando Volver. ¾Qué está ocurriendo?

7. Lo que ocurre es que el gestor de fragmentos conserva el estado entre


reinicios de la actividad. Al rotarlo una vez, la actividad se cierra y
reconstruye, y el gestor de fragmentos nos añade automáticamente el
fragmento que añadimos inicialmente. Nosotros en el onCreate() aña-
dimos siempre el fragmento, lo que signica que se añadirá dos veces (o
más, si has rotado varias veces el dispositivo). Modica el onCreate()
para añadir el fragmento sólo si es la primera ejecución:

protected void onCreate(Bundle savedInstanceState) {


...

if (savedInstanceState == null) {
ListaLibrosFragment ...
...
}

} // onCreate

8. Vuelve a lanzar la aplicación y comprueba que ya no se acumulan


múltiples copias del fragmento.

9. No tiene ningún sentido el comportamiento actual con el botón Vol-


ver. Quita la llamada a addToBackStack(null).

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
55

10. Vamos a convertir la vista que avisaba de que no se había elegido nin-
gún libro en un fragment. Para eso, lo primero es independizarlo. Crea
un layout nuevo y llámalo fragment_sinlibroseleccionado.xml.
11. Copia el TextView original del layout fragment_resumen_libro. No
hace falta que dejes el layout. Podemos poner directamente el TextView
como raíz.

12. Dado que ya no tenemos el mensaje en el layout del fragmento del resu-
men, elimina el FrameLayout que metimos antes y deja el LinearLayout
como nodo raíz. Quita también el android:visibility="gone". Por
último, modica la clase ResumenLibroFragment para que no se ma-
nipule la visibilidad de los dos elementos en el método setLibro().

13. Crea una nueva clase de Java (no uses el asistente de fragmentos) y
llámala SinLibroSeleccionadoFragment.
14. Haz que herede de Fragment.
15. Implementa el método onCreateView(...), que expanda el layout an-
terior:

public class SinLibroSeleccionadoFragment extends Fragment {

@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {

return inflater.inflate(
R.layout.fragment_sinlibroseleccionado,
container, false);
}

} // SinLibroSeleccionadoFragment

16. Modica el onCreate de la actividad principal para que en la transac-


ción que ya hay se añada también el fragmento en el segundo contene-
dor:

if (savedInstanceState == null) {
ListaLibrosFragment llf = new ListaLibrosFragment();
SinLibroSeleccionadoFragment slsf =
new SinLibroSeleccionadoFragment();
FragmentTransaction transaction;
transaction = getSupportFragmentManager().beginTransaction();

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
56 Capítulo 2. Fragments

transaction.add(R.id.contenedorListaLibros, llf);
transaction.add(R.id.contenedorResumenLibros, slsf);
transaction.commit();
}

17. Para que al pulsar cualquier elemento de la lista se muesten sus deta-
lles, tenemos que modicar el método onLibroSeleccionado(), cons-
truyendo un fragmento y realizando una transacción con él. Ahora no
queremos añadir el fragmento, sino que queremos reemplazar el que ya
hay con el nuevo.

public void onLibroSeleccionado(Libro l) {

ResumenLibroFragment rlf;
rlf = new ResumenLibroFragment();
rlf.setLibro(l.titulo, l.autor, l.resumen);
FragmentTransaction transaction;
transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.contenedorResumenLibros, rlf);
transaction.addToBackStack(null);
transaction.commit();

18. Lanza la aplicación y pulsa sobre algún libro. ¾Qué ocurre?

La aplicación falla porque hemos llamado a setLibro() antes de que el


fragmento se inicialice, y no es capaz de encontrar los elementos de la vista.
Lo que tenemos que hacer es conseguir que el fragmento guarde las cadenas
temporalmente, y cuando expanda la vista en el onCreateView() las utilice
para establecerlas en las etiquetas.

Hay un problema adicional y es que si el dispositivo se rota, hemos visto


que es Android quién se encarga de reconstruir nuestros fragmentos; en el
onCreate() de la actividad no estamos haciendo nada con los fragmentos si
nos estamos reconstruyendo, porque Android lo hará por nosotros.

Pero Android llama al constructor por defecto de los fragmentos pero


no llamará al método setLibro() para que el objeto recupere las cadenas.
Podríamos capturar los eventos del ciclo de vida del fragmento para conservar
su estado, y luego usarlo para reconstruirlo, pero hay una opción mejor.

Dado que el problema de la inicialización de los fragmentos por código


como estamos haciendo aquí es similar al problema de la reconstrucción de
los fragmentos de manera automática, Android añade en los fragmentos un
bundle adicional, que se guarda automáticamente sin que tengamos que preo-
cuparnos nosotros en los métodos del ciclo de vida. Es decir, cualquier cosa

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
57

que guardemos en el bundle obtenido con getArguments() de un fragmento


sobrevivirá a cambios de conguración de manera automática.

El esquema de programación de los fragmentos lo que hace es aprovechar


esto también para la inicialización de los fragmentos por código. En concreto,
en lugar de tener nuestro setLibro() que recibe los parámetros y los guarda,
tal y como proponíamos en sus atributos, lo que hacemos es guardarlos en
el bundle de guardado automático, para saber que los tendremos ahí la
próxima vez que nos reconstruyan. En el onCreateView() sacamos siempre
los parámetros de ese bundle, y así no tenemos que diferenciar si los cogemos
de nuestros supuestos atributos de construcción en dos pasos, o del bundle
recibido como parámetro en el onCreateView().
Además, la construcción habitual es crear un método factoría (estático)
llamado newInstance() que reciba los parámetros de inicialización y que
haga todo el trabajo por nosotros.

Modica la clase ResumenLibroFragment para que quede algo así:

public class ResumenLibroFragment extends SpyFragment {

public static final String TITULO = "titulo";


public static final String AUTOR = "autor";
public static final String RESUMEN = "resumen";

public static ResumenLibroFragment newInstance(String titulo,


String autor,
String resumen) {
ResumenLibroFragment fragment = new ResumenLibroFragment();
Bundle args = new Bundle();
args.putString(TITULO, titulo);
args.putString(AUTOR, autor);
args.putString(RESUMEN, resumen);
fragment.setArguments(args);
return fragment;
}

public ResumenLibroFragment() {
}

@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {

View result = inflater.inflate(R.layout.fragment_resumen_libro,

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
58 Capítulo 2. Fragments

container, false);
if (getArguments() != null) {
Bundle args = getArguments();
setLabel(result, R.id.tituloLibro, args.getString(TITULO));
setLabel(result, R.id.autorLibro, args.getString(AUTOR));
setLabel(result, R.id.resumenLibro, args.getString(RESUMEN));
}
return result;

protected void setLabel(int id, String str) {

setLabel(getView(), id, str);

} // setLabel

protected void setLabel(View v, int id, String str) {

TextView tv;
tv = (TextView) v.findViewById(id);
if (str != null)
tv.setText(str);
else
tv.setText("");

} // setLabel

} // ResumenLibroFragment

1. Ejecuta el programa.

2. Selecciona algún libro.

3. Rota el dispositivo y comprueba que aunque la actividad se ha recons-


truído, se sigue viendo el contenido.

4. Selecciona varios libros, y vuelve a rotar el dispositivo.

5. Ve pulsando Volver y comprueba que todo funciona como debería.

6. Antes de terminar, elimina la llamada a addToBackStack(null) en


MainActivity para que no se conserven las transacciones. Es antina-
tural que el botón volver tenga este signicado aquí.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
59

Práctica 2.8: Fragmentos estáticos estables

Vamos a recuperar la última versión de la práctica usando fragments está-


ticos para utilizar en el fragmento con el resumen del libro el mismo esquema
que hemos utilizado en la práctica anterior. De ese modo, conseguiremos que
se mantenga el resumen del último libro cuando rotemos el dispositivo.

1. Haz una copia de la práctica 2.6.

2. Ejecútala sobre un dispositivo con pantalla grande. Selecciona un libro


y rota la pantalla. Comprueba que el resumen se pierde. Es debido
a que la invocación a setLibro() que se hace desde el evento de la
actividad principal hace cambios no persistentes en el fragmento, y no
nos hemos preocupado de grabarlo.

3. En lugar de grabarlo usando el ciclo de vida del fragmento, nos aprove-


charemos de los argumentos del fragmento. Copia la nueva versión de
setLabel() que hemos puesto en la práctica anterior para que reciba
la vista en la que buscar el TextView en lugar de hacerlo en la vista
del fragmento.

4. Modica onCreateView() para que, si getArguments() no es null,


extraiga del bundle los campos del título, autor y resumen y los es-
tablezca en las etiquetas correspondientes, tal y como hicimos antes.
Recuerda, además, hacer visible el LinearLayout y ocultar el TextView
con el aviso, tal y como tenemos en setLibro().
5. Modica el método setLibro() para que, además de cambiar las eti-
quetas, modique el getArguments() estableciendo los nuevos valores
en los parámetros. Así garantizaremos que se conservarán entre reini-
cios.

public void setLibro(String titulo, String autor, String resumen) {

...

Bundle args = getArguments();


args.putString(TITULO, titulo);
args.putString(AUTOR, autor);
args.putString(RESUMEN, resumen);

} // setLibro

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
60 Capítulo 2. Fragments

6. Ejecuta la aplicación. Fallará, porque getArguments() es null.


7. No podemos establecer un bundle como parámetro del fragmento una
vez que éste está ya en marcha, por lo que no podemos hacer el new
directamente ahí. En el constructor, añade:

setArguments(new Bundle());

8. Vuelve a ejecutar la aplicación. Comprueba que ahora ya sí funciona.

Notas bibliográcas
http://developer.android.com/training/basics/fragments/index.
html
http://developer.android.com/guide/components/fragments.html
http://developer.android.com/reference/android/app/Fragment.
html
http://developer.android.com/guide/topics/resources/accessing-resources.
html

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Capítulo 3

Hebras y tareas asíncronas


Resumen: En este capítulo veremos el concepto de hebra principal
así como la creación de hebras de usuario y su relación con el ciclo de
vida de la actividad. También se exploran las posibilidades de la clase
AsyncTask.

Práctica 3.1: Actividades, aplicaciones y procesos

En la práctica 2.2 vimos el ciclo de vida de las actividades que es controla-


da por Android. Las actividades reciben noticaciones a través de diferentes
métodos, que podemos sobreescribir para actuar en consecuencia.

Las actividades son un tipo de componente, como lo son los los servicios,
los proveedores de contenido o los broadcast receivers. En Android, no se
lanzan aplicaciones, sino que se lanzan componentes.

Los componentes de una aplicación se declaran en el chero de maniesto


del APK que los contiene y, salvo que se indique lo contrario, todos los
componentes se ejecutarán en el mismo proceso, y serán atendidos por la
misma hebra.
Así, cuando lanzamos una actividad, Android comprueba si el proceso
asociado a esa aplicación (APK) está o no lanzado ya. Si no lo está, lo
lanza e inicializa, y le pide que construya y ejecute la actividad, iniciando su
ciclo de vida. Si la actividad se cierra, Android normalmente no eliminará el
proceso, dejándolo en suspendido por si llegara alguna solicitud nueva para
él de lanzar un componente.

1. Crea un proyecto nuevo y llámalo ProcesosYComponentes.

2. Recupera la clase SpyActivity de la práctica 2.2 e inclúyela en el


proyecto. Cambia el nombre del paquete para que pertenezca también

61
62 Capítulo 3. Hebras y tareas asíncronas

a libro.azul.procesosycomponentes como el resto de la aplicación.


Haz que la actividad principal herede de ella.

3. Añade en el proyecto una nueva actividad. Llámala SecondaryActivity.


Marca que quieres que sea una actividad para el lanzador Launcher ac-
tivity para que podamos lanzar ambas. Como título, pon Proc. y comp.
2

4. Lanza la aplicación (se lanzará la primera actividad) y comprueba la


aparición de los mensajes de log indicando el ciclo de vida.

5. Ve al directorio donde tengas instaladas las SDK de Android. Entra


en el directorio platform-tools y ejecuta adb para abrir una línea de
1
comandos en el dispositivo .

6. Ejecuta ps y busca el proceso libro.azul.procesosycomponentes,


que es el que está a cargo de nuestra ejecución.

7. Mira su identicador del proceso. Utiliza ps -t <id> para listar sus


hebras.

8. Pulsa Volver en la actividad. Observa los mensajes del ciclo de vida,


indicando que la actividad ha sido destruída.

9. Comprueba de nuevo con ps en la línea de comandos que el proceso


sigue en ejecución.

10. Desde el lanzador, busca la segunda actividad y ejecútala. Con ps,


comprueba que el proceso se mantiene y tiene el mismo identicador.

11. Cierra la actividad, comprueba que se han llamado a los métodos de


su ciclo de vida, y que el proceso sigue vivo.

Práctica 3.2: La hebra principal y ANR

Aunque hemos visto que había varias hebras en el proceso, la gestión de


todas las actividades de la aplicación es realizada desde la misma, la llamada
hebra principal o hebra de UI (de nombre main en los listados).

1. Haz una copia de la práctica anterior.

1
Dependiendo de cuántos AVD tengas lanzados y dispositivos físicos tengas conectados
necesitarás unos parámetros u otros. Por ejemplo, con un único dispositivo bastará con
adb shell. Si tienes más de uno, tendrás que usar los parámetros -e, -d o -s <id>

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
63

2. Elimina la segunda actividad, dado que no la utilizaremos más.

3. Modica el layout de la actividad principal para poner un botón que


ocupe toda la pantalla, asocia un evento y crea el método.

4. Pon como código del evento un bucle innito.

public void onPulsame(View v) {


while(true)
;
}

5. Lanza la aplicación y pulsa el botón. ¾Qué ocurre?

Práctica 3.3: Hebras trabajadoras

Si queremos hacer tareas que lleven mucho tiempo, es preferible utilizar


hebras.

1. Haz una copia de la práctica anterior.

2. Modica el código del evento para que el bucle innito se ejecute en


una hebra nueva.

public void onPulsame(View v) {

Thread t;
t = new Thread(new Runnable() {
@Override
public void run() {
while(true);
}
});
t.start();

} // onPulsame

3. Lanza la aplicación. No pulses el botón aún.

4. En el shell con el dispositivo a través de adb, utiliza ps -t para ver


las hebras. También puedes verlas con el Android Device Monitor.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
64 Capítulo 3. Hebras y tareas asíncronas

5. Pulsa el botón. Ahora que la aplicación responde correctamente. Ob-


serva con ps que aparece una hebra nueva.

6. Pulsa Volver para cerrar la actividad. Comprueba que la hebra se


mantiene.

7. Para matar la hebra, puedes usar kill desde el shell (sólo si si era
un AVD), el administrador de aplicaciones del móvil, o, sencillamen-
te, volver a lanzar la ejecución de la aplicación desde el entorno de
desarrollo. Ésto ocasiona la reinstalación de la aplicación, y Android
detendrá antes el proceso asociado a ella.

Si, en lugar de un bucle innito, quieres hacer algo más apreciable, puedes
generar un pequeño beep cada segundo:

t = new Thread(new Runnable() {


@Override
public void run() {
int i = 0;
ToneGenerator tg;
tg = new ToneGenerator(AudioManager.STREAM_ALARM, 100);
while(true) {
tg.startTone(ToneGenerator.TONE_CDMA_ALERT_INCALL_LITE, 20);
android.util.Log.i("TAG", "MainActivity: " + i);
++i;
try {
Thread.sleep(2000);
} catch(InterruptedException ie) {}
}
}
});

Práctica 3.4: Hebras trabajadoras e interfaz

Vamos a modicar la práctica anterior para que en lugar de un bucle in-


nito, tengamos una hebra trabajadora que calcule algo, y que dé el resultado
al terminar.

1. Haz una copia de la práctica anterior. Refactoriza el paquete para que


se llamelibro.azul.cuantosprimos y cambia el texto de la cadena
app_name para que sea Cuantos primos.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
65

2. Añade en la actividad dos métodos estáticos, uno para decidir si un


número es o no primo, y otro para contar cuántos primos hay entre 1
y un número.

public static boolean esPrimo(long i) {


if (i == 1) return false;
if (i < 4) return true;
if ((i % 2) == 0 || (i % 3) == 0) return false;
if (i < 9) return true;
long n = 5;
while (n*n <= i && i % n != 0 && i % (n + 2) != 0)
n += 6;
return (n*n > i);
}

public static long cuantosPrimos(long limite) {


long result = 0;
for (long i = 1; i <= limite; ++i)
if (esPrimo(i))
++result;
return result;
}

3. Modica el layout para meter una etiqueta (TextView) encima del


botón. Seguramente quieras cambiar a LinearLayout.

<LinearLayout
...
android:orientation="vertical">

<TextView android:id="@+id/tvResultado"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<Button ... />

</LinearLayout>

4. Modica el código asociado a la hebra secundaria para que llame al


método cuantosPrimos().

t = new Thread(new Runnable() {


@Override
public void run() {

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
66 Capítulo 3. Hebras y tareas asíncronas

long r = cuantosPrimos(1000000); // 10^6


TextView tv = (TextView) findViewById(R.id.tvResultado);
tv.setText("" + r);
}
});

1. Lanza la aplicación. ¾Qué ocurre y por qué?

Práctica 3.5: Ejecución en la hebra principal

El problema de la práctica anterior es que la hebra principal no es reen-


trante (segura ante hebras). No podemos manipular nada del interfaz desde
una hebra que no sea la principal, porque, por eciencia, no se utilizan mé-
todos sincronizados.

Para manipular el interfaz desde una hebra trabajadora, necesitamos


crear un Runnable que encapsule el código que queremos ejecutar, y enco-
larlo para que la hebra principal lo ejecute en cuanto le sea posible hacerlo
de manera segura.

1. Haz una copia de la práctica anterior.

2. Encierra el código que manipula el interfaz en un objeto Runnable, y


pásalo al método runOnUiThread() de la actividad.

t = new Thread(new Runnable() {


@Override
public void run() {
final long r = cuantosPrimos(1000000); // 10^6
runOnUiThread(new Runnable() {
@Override
public void run() {
TextView tv = (TextView) findViewById(R.id.tvResultado);
tv.setText("" + r);
}
});
}
});

3. Ejecuta la aplicación. Comprueba que, ahora sí, funciona bien.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
67

Práctica 3.6: AsyncTask

El código de la práctica anterior es un poco farragoso, y no es cómodo de


escribir. Android proporciona, desde el API level 3 (1.5), la clase AsyncTask
que encapsula una hebra y facilita la programación de tareas largas que se
deben realizar en segundo plano.

Los dos métodos más importantes de la clase, que habrá que sobreescribir,
son:

protected RESULT doInBackground(PARAMS... params): contendrá


el código que se ejecutará en una hebra secundaria. Recibe una lis-
ta de parámetros de tipo PARAMS.

protected void onPostExecute(RESULT result): será llamado des-


de la hebra principal cuando el método anterior termine. El parámetro
será el valor devuelto por doInBackground().

Como puedes ver, AsyncTask es una clase genérica, con tres argumentos,
uno de los cuales aún no hemos visto:

PARAMS: tipo de los parámetros recibidos al lanzar la tarea.

PROGRESS: tipo de indicación del progreso, del que hablaremos en la


práctica 3.8.

RESULT: tipo del resultado que genera.

Para lanzar una tarea asíncrona, basta con crear un objeto y llamar,
desde la hebra principal, al método execute(PARAMS... params), que se
encargará de que el método doInBackground() se ejecute con los parámetros
indicados en una hebra secundaria.

1. Haz una copia de la práctica anterior.

2. Crea una clase interna CalcularPrimos que herede de AsyncTask. El


tipo PARAMS Void, que se utiliza para indicar que no tenemos
será
parámetros. El tipoRESULT será Long, y el tipo PROGRESS será, de
momento, también Void. Mueve a ella los métodos estáticos del cálculo
de primos (aunque ahora no podrán ser estáticos).

3. En el método doInBackground() llama a cuantosPrimos().

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
68 Capítulo 3. Hebras y tareas asíncronas

4. En el método onPostExecute() actualiza la etiqueta.

class CalcularPrimos extends AsyncTask<Void, Void, Long> {

public /*static*/ boolean esPrimo(long i) {


if (i == 1) return false;
if (i < 4) return true;
if ((i % 2) == 0 || (i % 3) == 0) return false;
if (i < 9) return true;
long n = 5;
while (n*n <= i && i % n != 0 && i % (n + 2) != 0)
n += 6;
return (n*n > i);
}

public /*static*/ long cuantosPrimos(long limite) {


long result = 0;
for (long i = 1; i <= limite; ++i)
if (esPrimo(i))
++result;
return result;
}

@Override
protected Long doInBackground(Void... params) {

return cuantosPrimos(10000000); // 10^7

@Override
protected void onPostExecute(Long resultado) {

TextView tv = (TextView) findViewById(R.id.tvResultado);


tv.setText(resultado.toString());
}

} // CalcularPrimos

5. Modica el método del evento del ratón, crea un objeto de la clase, y


lanza su ejecución.

public void onPulsame(View v) {

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
69

new CalcularPrimos().execute();

} // onPulsame

6. Prueba la aplicación.

7. Aumenta el límite en la búsqueda para que se cuenten más primos y el


proceso tarde más. En el método doInBackground() pon, al principio,
una noticación en el log:

protected Long doInBackground(Void... params) {


android.util.Log.d("CLICK", "Empiezo!");
return cuantosPrimos(10000000); // 10^7
}

8. Pulsa el botón dos veces. ¾Qué ocurre? ¾Cómo lo explicas?

Práctica 3.7: AsyncTask cancelable

Es posible cancelar una tarea asíncrona llamando a su método cancel().


La tarea no se detendrá por sí misma. Es responsabilidad nuestra compro-
bar periódicamente durante el trabajo en segundo plano si nos han pedido
que paremos, llamando al método isCancelled(), y terminar en ese caso.
Cuando una tarea termina habiendo sido cancelada, no se llamará al método
onPostExecute(), sino al método onCancelled(RESULT), también desde la
hebra principal.

1. Haz una copia de la práctica anterior.

2. Vamos a querer cancelar la tarea asíncrona cuando se pulse el botón


una segunda vez. Para poder hacerlo, crea un atributo en la clase.

3. Cambia el código del evento del ratón para que cancele la tarea si se
pulsó una segunda vez:

CalcularPrimos cp;

public void onPulsame(View v) {

if (cp == null) {
cp = new CalcularPrimos();

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
70 Capítulo 3. Hebras y tareas asíncronas

cp.execute();
}
else
cp.cancel(true);

} // onPulsame
4. Modica la clase asíncrona para que compruebe de vez en cuando si se
ha pedido que se termine. Para no comprobarlo contínuamente, pode-
mos hacerlo únicamente cuando encontremos un número primo.

public long cuantosPrimos(long limite) {


long result = 0;
for (long i = 1; i <= limite; ++i)
if (esPrimo(i)) {
++result;
// Miramos si nos han cancelado. Sólo si es primo
// para mirarlo menos veces.
if (isCancelled())
break;
} // if esPrimo

return result;
}
5. Sobreescribe el método onCancelled(), refactorizando el código de
onPostExecute() para no repetir.

@Override
protected void onPostExecute(Long resultado) {
terminar(resultado.toString());
}

@Override
protected void onCancelled(Long resultadoParcial) {
terminar("½Cancelado! Llevaba " + resultadoParcial);
}

protected void terminar(String s) {


TextView tv = (TextView) findViewById(R.id.tvResultado);
tv.setText(s);
cp = null;
}
6. Prueba la aplicación. Pulsa el botón mientras se están contando primos
y comprueba que la operación se cancela.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
71

Práctica 3.8: Progreso de la AsyncTask

Para poder mostrar una barra de desplazamiento, AsyncTask proporciona


dos métodos más:

void publishProgress(PROGRESS... values): recibe una lista de pa-


rámetros del tipo PROGRESS, el tercer argumento del tipo genérico
que dejamos pendiente en la práctica 3.6. Debe ser llamado desde
doInBackground() cuando queremos informar de un avance en el pro-
greso.

onProgressUpdate(PROGRESS... values): se ejecuta en la hebra prin-


cipal cada vez que desde la hebra trabajadora se llama al método
publishProgress(...). Los parámetros recibidos son los que se pasa-
ran a éste.

Por completar, AsyncTask tiene un último método, onPreExecute(), que


puede ser sobreescrito y que será llamado desde la hebra principal en el
momento de ir a lanzar el procesado.

1. Haz una copia de la práctica anterior.

2. Modica el layout para añadir una barra de progreso horizontal debajo


de la etiqueta, y déjala oculta.

<ProgressBar
android:id="@+id/pbCalcularPrimos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible"
android:max="100"
style="?android:attr/progressBarStyleHorizontal"/>

3. Modica la clase interna CalcularPrimos para que tenga como segun-


do argumento el tipo Integer (será el que hemos llamado PROGRESS).

4. Añade como atributo una ProgressBar que nos evite tener que bus-
carla en la vista continuamente.

5. Sobreescribe el método onPreExecute() para que se incialice su valor


(usando findViewById()), y se haga visible.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
72 Capítulo 3. Hebras y tareas asíncronas

6. Sobreescribe el método onProgressBarUpdated(Integer...) para que


se establezca como progreso en la barra el valor del parámetro. Ten en
cuenta que al ser una lista de parámetros opcionales, lo recibirás como
un array, que será de un solo elemento.

7. Modica el método cuantosPrimos() para que se actualice la barra de


progreso. Evita llamar a publishProgress() con el mismo valor para
no forzar a repintados y sincronizaciones superuas.

8. Modica el método terminar() para que se oculte la barra de progreso.

protected ProgressBar pb;

public long cuantosPrimos(long limite) {


long result = 0;
long lenStep = limite / 100;
int prevProgress = -1;
for (long i = 1; i <= limite; ++i) {
if (esPrimo(i)) {
++result;
if (isCancelled())
break;
if (prevProgress != i / lenStep) {
prevProgress = (int)(i / lenStep);
publishProgress(prevProgress);
}
} // if esPrimo
} // for
return result;
} // cuantosPrimos

@Override
protected void onPreExecute() {
pb = (ProgressBar) findViewById(R.id.pbCalcularPrimos);
pb.setVisibility(View.VISIBLE);
}

[ ... ]

protected void terminar(String s) {


TextView tv = (TextView) findViewById(R.id.tvResultado);
tv.setText(s);
pb.setVisibility(View.INVISIBLE);
cp = null;
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Notas bibliográcas 73

@Override
protected void onProgressUpdate(Integer... values) {
pb.setProgress(values[0]);
}

Notas bibliográcas
http://developer.android.com/guide/components/processes-and-threads.
html
http://developer.android.com/training/articles/perf-anr.html
http://developer.android.com/reference/android/os/AsyncTask.
html

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Capítulo 4

Servicios
Resumen: En este capítulo veremos los servicios como solución para
la realización de tareas largas en segundo plano. Veremos un ejemplo
sencillo en el que se independiza el ciclo de vida de la actividad y del
servicio y, aun así, el servicio puede comunicarse con la actividad de
manera segura.

Práctica 4.1: Servicio básico

Un servicio es un tipo de componente pensado para ejecutar tareas en


segundo plano. Los servicios tienen más prioridad en el sistema que las acti-
vidades detenidas (no visibles), por lo que proporcionan un mecanismo ideal
para realizar operaciones de larga duración que no queramos que se detengan
con la actividad, como descargar un chero o reproducir música.

Para lanzar o detener servicios se utilizan métodos de la clase Context


e intents. En el caso de la llamada, el intent puede contener información
extra que el servicio podrá recuperar. Cuando se lanza un servicio, éste no
se destruye cuando la solicitud es terminada.
Los servicios pueden implementar los métodos siguientes:

onCreate(): llamado cuando el servicio se arranca.

onStartCommand(Intent, int, int): llamado por cada solicitud rea-


lizada al servicio.

onBind(): llamado cuando se realiza una actividad de enlace con el


servicio para realizar llamadas entre procesos.

onDestroy(): llamado cuando el servicio es detenido por el sistema


por falta de recursos, o cuando se pide explícitamente su terminación.

75
76 Capítulo 4. Servicios

En contra de lo que ocurre en las actividades, no es necesario llamar a


los métodos de la superclase.

1. Crea un nuevo proyecto y llámalo libro.azul.serviciobasico.

2. En la actividad predenida, pon dos botones en un LinearLayout ver-


tical, con etiquetas Lanzar servicio y Detener servicio, y conecta un
método en cada uno de sus eventos onClick.

3. Crea un servicio nuevo usando el asistente. Conguralo como privado


(no exportado), y comprueba su inclusión en el chero de maniesto.

4. Implementa los métodos anteriores, y escribe en el log un mensaje


informando de su ejecución.

public class ServicioBasico extends Service {

public ServicioBasico() {
}

@Override
public IBinder onBind(Intent intent) {
android.util.Log.i(TAG, "onBind(" + intent + ")");
throw new UnsupportedOperationException("Unsupported");
}

@Override
public void onCreate() {
android.util.Log.i(TAG, "onCreate()");
}

@Override
public int onStartCommand(Intent intent,
int flags, int startId) {
android.util.Log.i(TAG, "onStartCommand(" + intent + "," +
flags + ", " + startId + ")");
return START_NOT_STICKY;
}

@Override
public void onDestroy() {
android.util.Log.i(TAG, "onDestroy()");
}

protected final String TAG = getClass().getSimpleName();

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
77

} // ServicioBasico

5. En los métodos de los botones, lanza o para el servicio.

public void onLanzarServicio(View v) {


startService(new Intent(this, ServicioBasico.class));
}

public void onPararServicio(View v) {


stopService(new Intent(this, ServicioBasico.class));
}

6. Lanza la aplicación.

7. Pulsa sobre el botón de lanzar el servicio. Observa las llamadas a los


métodos a onCreate() y onStartCommand().

8. Cierra la aplicación.

9. Vuelve a lanzarla y pulsa de nuevo el botón. Observa que ahora no se


llama a onCreate(), lo que demuestra que el servicio no se detuvo.
Además, el último parámetro se ha incrementado.

10. Detén el servicio y vuelve a probar.

Práctica 4.2: Servicios y la hebra principal

1. Haz una copia de la práctica anterior.

2. En el método onStartCommand() del servicio, pon un bucle innito.


Tendrás que quitar elreturn para que compile.

3. Lanza la aplicación. ¾Qué ocurre? ¾Cómo lo explicas?

Los servicios se ejecutan en la hebra principal del proceso, igual que las
actividades. La denición habitual de que los servicios se usan para ejecu-
ciones en segundo plano es engañosa. Se reere a que no usan interfaz
gráco, pero no se ejecutan en hebras independientes. Esto ocurre incluso si
el servicio está implementado en otro proceso.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
78 Capítulo 4. Servicios

Por tanto, lo normal será delegar en una hebra el procesamiento de los


comandos. Esto puede resultar confuso. ¾Para qué lanzar un servicio que ter-
minará lanzando una hebra y no hacerlo en una hebra creada por la actividad
directamente? Al crear un servicio, el proceso pasa a tener más prioridad que
si no lo tuviera, por lo que tardará más en ser eliminado del sistema, y sus
hebras (no gestionadas) tendrán más oportunidad de mantenerse en marcha.

Además, con un servicio podemos pedirle al sistema qué queremos que


haga con los comandos en el caso en el que se viera obligado a eliminarlo.
Para eso servía el valor devuelto por onStartCommand():

START_NOT_STICKY: el servicio no volverá a ser lanzado si el sistema se


vio obligado a destruirlo.

START_STICKY: Android volverá a lanzar el servicio cuando recupere


recursos, y llamará a onStartCommand(), pasando null como intent.

START_REDELIVER_INTENT: el servicio se volverá a lanzar, y se pasará


a onStartCommand() el intent que acabamos de recibir.

Hay un cuarto posible valor, START_STICKY_COMPATIBILITY, para imi-


tar el comportamiento de versiones antiguas de Android (1.x) y que no se
recomienda.

El segundo parámetro de onStartCommand() da información sobre si la


solicitud es un reintento o una solicitud normal recién llegada.

Práctica 4.3: IntentService


Dado que para procesar las solicitudes hay que utilizar hebras, hay dos
modelos diferentes:

Servicio monocliente: las solicitudes se procesan de una en una, según


van llegando.

Servicio multicliente: para cada solicitud se lanza una hebra que la


atiende de manera independiente.

El primer esquema requiere una hebra, dado que las solicitudes se reciben
en la hebra principal, pero se quieren atender en una hebra de trabajo. Se
necesita una cola de mensajes pendientes para que la hebra trabajadora
recoja los intents y los procese de uno en uno.

La segunda opción requiere multitarea y vigilar los posibles problemas


con la concurrencia.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
79

Lo normal será utilizar el primer modelo. Android proporciona la clase


IntentService que facilita su implementación, proporcionando todos los
métodos ya implementados y requiriendo únicamente la escritura de uno
nuevo, onHandleIntent(Intent), que será llamado en la hebra de trabajo.
La superclase se encargará de cerrar el servicio cuando no queden solicitudes
pendientes.

En esta práctica vamos a crear una actividad con un teclado telefónico.


Al pulsar cada número, se envía a un servicio la solicitud de que se reproduzca
el sonido asociado de la marcación por tonos. El sonido dura un tiempo, por
lo que si se pulsan los números con la suciente velocidad se irán acumulando
las pulsaciones pendientes que se reproducirán incluso aunque la actividad
se cierre.

1. Crea un nuevo proyecto y llámalo libro.azul.dialer.

2. Modica el layout de la actividad para que tenga un TableLayout


con los botones de un teléfono. Ponle los números como texto, y los
símbolos * y #. Asocia a todos los botones el mismo evento.

<TableLayout ...>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:layout_column="0"
android:onClick="onBoton" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2"
android:layout_column="1"
android:onClick="onBoton" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="3"
android:layout_column="2"
android:onClick="onBoton" />
</TableRow>
[ ... Resto de filas ... ]
</TableLayout>

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
80 Capítulo 4. Servicios

Figura 4.1: Teclado telefónico

3. Crea una nueva clase para el servicio y llámala DialerService. Haz


que herede de IntentService y mete en el chero de maniesto la
1
declaración del servicio .

<service
android:name=".DialerService"
android:exported="false" >
</service>

4. Dene un atributo protegido de tipo ToneGenerator que será lo que


usemos para la generación de los tonos.

5. Implementa un constructor sin parámetros que llame al constructor de


la clase padre, que recibe una cadena con el nombre. La cadena es sólo
útil para depuración. Inicializa además el atributo del generador.

public DialerService() {
super("DialerService");
tg = new ToneGenerator(AudioManager.STREAM_ALARM, 100);
}

6. Para seguir la pista al ciclo de vida, sobreescribe los métodos onCreate()


y onDestroy(). Muestra en el log que se han ejecutado, y llama al mé-
todo de la superclase.

@Override
public void onCreate() {
android.util.Log.i(TAG, "onCreate()");
super.onCreate();
}

@Override
1
También puedes utilizar el asistente del IDE, aunque la plantilla del servicio que te
creará tendrá mucho código que no usaremos.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
81

public void onDestroy() {


android.util.Log.i(TAG, "onDestroy()");
super.onDestroy();
}

protected final String TAG = getClass().getSimpleName();

7. Declara un atributo estático de tipo entero que guarde la duración del


sonido. Establecelo a 500.

8. Declara un atributo estático de cadena que guarde el nombre del campo


en el intent que usaremos para recibir el sonido que vamos a reproducir.
Asociale el valor  sonido.

9. Crea el método onHandleIntent(Intent) que reciba la solicitud, re-


produzca el sonido pedido en el intent y espere a que termine.

@Override
protected void onHandleIntent(Intent intent) {
int sonido;
sonido = intent.getIntExtra(SONIDO, 0);
android.util.Log.i(TAG, "onHandleIntent(" + sonido + ")");
tg.startTone(sonido, DURACION);
try {
Thread.sleep(DURACION);
}
catch (InterruptedException ie) {}
}

protected final static String SONIDO = "sonido";

protected final static int DURACION = 500;

10. Para facilitar su uso, crea un método estático que reciba una cadena
con la tecla pulsada, y que cree el intent y se lo envíe al servicio.

public final static void play(Context context, String key) {


Intent intent = new Intent(context, DialerService.class);
int sonido;
switch(key.charAt(0)) {
case '#':
sonido = ToneGenerator.TONE_DTMF_D;
break;
case '*':
sonido = ToneGenerator.TONE_DTMF_S;

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
82 Capítulo 4. Servicios

break;
default:
// 0..9
sonido = ToneGenerator.TONE_DTMF_0 + key.charAt(0) - '0';
}
intent.putExtra(SONIDO, sonido);
context.startService(intent);
}

11. En la actividad principal, implementa el método que reacciona a la


pulsación de los botones. Obtén la etiqueta y llama al método estático
del servicio.

public void onBoton(View v) {


Button b = (Button) v;
DialerService.play(this, b.getText().toString());
}

12. Lanza la aplicación y pulsa un botón. Observa que en el log aparece la


creación y destrucción del servicio una vez que el sonido ha terminado.

13. Pulsa en rápida sucesión varias teclas, y cierra la actividad. Los sonidos
se mantendrán sonando incluso aunque la actividad se haya cerrado.
El servicio terminará cuando se termine la reproducción.

Práctica 4.4: BroadcastReceiver: comunicación con la ac-


tividad

Si un servicio necesita proporcionar un resultado, lo normal será hacer


uso de un mensaje de difusión. Android usa los mensajes de difusión para
noticar eventos globales del sistema (como la existencia de una llamada
entrante, o la conexión del cable de alimentación). Pero ese mismo mecanismo
puede utilizarse para que un componente envie noticaciones arbitrarias a
otros. Por ejemplo, en la práctica anterior podríamos querer que cuando el
servicio va a comenzar a reproducir uno de los tonos se lo notique a la
actividad para que muestre el número (sincronizado con el audio).

Pero utilizar los mensajes de difusión para comunicar dos componentes


de la misma aplicación, como en este caso, tiene dos pegas. La primera es
que el mensaje es lo sucientemente privado como para que no tenga sentido
hacerlo general. La segunda es que al crear un mensaje que puede superar
los límites del proceso se exige al sistema un esfuerzo innecesario.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
83

En la librería de soporte añadieron la clase LocalBroadcastManager para


estos casos. Proporciona la funcionalidad de los mensajes de difusión, pero
a nivel de una aplicación. Vamos a utilizarla para que el servicio envíe un
intent desde la hebra secundaria cuando vaya a iniciar el sonido de una tecla,
y que sea recogido por la actividad, si está lanzada aún, para mostrarlo.

1. Haz una copia de la práctica anterior.

2. Modica el layout para poner encima de la botonera un cuadro de


texto con el número marcado.

<TableLayout ...>

<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/etNumeroMarcado"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:layout_span="3"
/>
</TableRow>
[ ... filas del teclado numérico ... ]
</TableLayout>

3. En la actividad principal, haz un método estático que recibe un con-


texto y una cadena, para que se mande un intent a sí mismo usando
ese contexto y con la cadena como datos extra. Lo invocaremos luego
desde el servicio.

public final static void addKey(Context c, String key) {


Intent i = new Intent(MainActivity.class.getName());
i.putExtra(TECLA, key);
LocalBroadcastManager.getInstance(c).sendBroadcast(i);
}

4. Crea un nuevo receptor de mensajes del sistema que extraiga la tecla


del intent y la añada al cuadro de texto.

private BroadcastReceiver handler = new BroadcastReceiver() {


@Override
public void onReceive(Context context, Intent intent) {
EditText et = (EditText) findViewById(R.id.etNumeroMarcado);

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
84 Capítulo 4. Servicios

et.append(intent.getStringExtra(TECLA));
}
};

5. En el método onCreate(), obtén el LocalBroadcastReceiver asociado


a la actividad, y registra el objeto anterior, ltrando los intents en los
que estás interesado a únicamente los que estén destinados a tu clase.

@Override
protected void onCreate(Bundle savedInstanceState) {
...
IntentFilter filtro = new IntentFilter(getClass().getName());
LocalBroadcastManager.getInstance(this).
registerReceiver(handler, filtro);
}

Ya tenemos la actividad preparada. Ahora tenemos que hacer que el


servicio envíe la tecla pulsada antes de comenzar a reproducir su sonido.

1. Para poder enviar la tecla de vuelta a la actividad tenemos que conser-


varla en el intent que recibe el servicio. Añade en el servicio un nuevo
atributo estático de tipo cadena, llámalo TECLA y asígnalo a  tecla .

2. Modica el método estático play() para que se añada en el intent la


tecla del parámetro.

public final static void play(Context context, String key) {


// ...
switch(...) {
...
}
intent.putExtra(TECLA, key);
...
}

3. Modica el método onHandleIntent() para que antes de reproducir


el sonido llame al método estático de la actividad para noticarla que
se ha atendido su solicitud.

protected void onHandleIntent(Intent intent) {


int sonido;
sonido = intent.getIntExtra(SONIDO, 0);
android.util.Log.i(TAG, "onHandleIntent(" + sonido + ")");

// Avisamos a la actividad.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Notas bibliográcas 85

MainActivity.addKey(this, intent.getStringExtra(TECLA));
[ ... ]
}

4. Ejecuta la práctica y comprueba que funciona.

5. Pulsa muchos números y cierra la aplicación antes de que termine.


Cuando acabe el sonido, lánzala otra vez. Comprueba que los números
no se han registrado, pero la aplicación no ha fallado.

6. Lanza de nuevo la aplicación. Pulsa varios números y vuelve al lanza-


dor de Android (lo que no detendrá la actividad). Cuando acaben los
sonidos, vuelve a la aplicación. Deberías ver todos los números.

Notas bibliográcas
http://developer.android.com/guide/components/processes-and-threads.
html
http://developer.android.com/guide/components/services.html
http://developer.android.com/reference/android/app/Service.
html
http://developer.android.com/reference/android/app/IntentService.
html
http://developer.android.com/reference/android/support/v4/content/
LocalBroadcastManager.html
http://developer.android.com/guide/components/intents-filters.
html

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Capítulo 5

Bluetooth
Resumen: En este capítulo nos adentraremos en el uso de bluetooth,
el mecanismo para conexión entre dispositivos cercanos mediante ra-
dio, que permite construir redes inalámbricas de área personal. Para
poder realizar las prácticas de este capítulo, necesitarás, al menos, un
dispositivo físico Android con bluetooth, dado que el emulador no so-
porta bluetooth. Si quieres, además, probar las dos últimas prácticas,
con un cliente y un servidor, necesitarás dos.

Práctica 5.1: Activar bluetooth

El punto de entrada a la interacción con bluetooth en Android se consigue


a través de la clase BluetoothAdapter, que sirve para controlar un adaptador
bluetooth. La clase dispone de un método estático, getDefaultAdapter(),
que devuelve una instancia al adaptador predenido. Fíjate que esto en prin-
cipio podría hacer pensar que un dispositivo podría contar con más de un
adaptador bluetooth; sin embargo, no hay forma de conseguirlos.

Si el dispositivo tiene soporte para bluetooth, conseguiremos un objeto


con el que encuestar sobre su estado. El primer método que utilizaremos será
isEnabled(), que indica si el usuario tiene o no activa la comunicación por
bluetooth.

Si está habilitado, podremos continuar utilizándolo. Si no, podemos pe-


dir al sistema que solicite al usuario que lo active. Para eso, lanzamos un
intent con una solicitud predenida, que muestra un cuadro de diálogo mo-
dal. La actividad recibirá la respuesta en su método onActivityResult(),
como ocurre siempre con las actividades de este tipo. El cuadro de diálogo
devolverá RESULT_OK si el usuario habilitó el adaptador, y RESULT_CANCEL
en otro caso.

Vamos a hacer una aplicación que, en el onCreate(), busca el adaptador

87
88 Capítulo 5. Bluetooth

bluetooth, comprueba si está habilitado, y si no lo está le pide al usuario que


lo habilite.

1. Crea un nuevo proyecto y llámalo libro.azul.ActivarBluetooth.


2. Modica el chero de maniesto para requerir el permiso de utilizar el
adaptador bluetooth. De otro modo la aplicación fallará en ejecución.

<manifest>
...
<uses-permission android:name="android.permission.BLUETOOTH"/>
</manifest>

3. Añade en el chero de cadenas los mensajes sobre el estado de bluetooth


que daremos al usuario:

noBluetooth: No se encontró adaptador bluetooth

bluetoothDesactivado: Bluetooth deshabilitado

bluetoothActivado: Bluetooth habilitado

4. Dene un atributo BluetoothAdapter. En el método onCreate(), ini-


cialízalo con el objeto para acceder al adaptador predenido, y com-
prueba su disponibilidad. Si está habilitado, llama a un método, que
escribiremos más adelante, llamado updateBluetoothUI(). Si no, lan-
za la actividad pidiendo al usuario que lo habilite.

@Override
protected void onCreate(Bundle savedInstanceState) {
....

tvEstadoBluetooth = (TextView)
findViewById(R.id.estadoBluetooth);
bluetooth = BluetoothAdapter.getDefaultAdapter();
if (savedInstanceState != null)
return;

if (bluetooth == null)
tvEstadoBluetooth.setText(R.string.noBluetooth);
else {
if (bluetooth.isEnabled())
updateBluetoothUI();
else {
Intent intent;
intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, CALLBACK_ENABLE_BLUETOOTH);

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
89

}
}
}

private BluetoothAdapter bluetooth;

private TextView tvEstadoBluetooth;

private final static int CALLBACK_ENABLE_BLUETOOTH = 1;

5. Sobreescribe el método onActivityResult() para que reaccione ante


la respuesta del cuadro de diálogo.

@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == CALLBACK_ENABLE_BLUETOOTH) {
if (resultCode == RESULT_OK) {
updateBluetoothUI();
}
else {
tvEstadoBluetooth.setText(R.string.bluetoothDesactivado);
}
}
}

6. Implementa el método updateBluetoothUI() para que se actualice la


etiqueta y nos muestre los datos del adaptador.

private void updateBluetoothUI() {


String msj;
msj = getString(R.string.bluetoothActivado) +
": " + bluetooth.getName() +
" (" + bluetooth.getAddress() + ")";
tvEstadoBluetooth.setText(msj);
} // initBluetoothUI

7. Lanza la aplicación sobre un dispositivo físico y pruébala. Los AVD no


tienen emulación; puedes probar a ejecutar la aplicación y deberías ver
el mensaje correspondiente.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
90 Capítulo 5. Bluetooth

Práctica 5.2: Detectar cambios de estado

En la práctica anterior conguramos la vista en función del estado del


adaptador al inicio de la ejecución, pero éste podría cambiar en cualquier
momento.

Cuando el estado del adaptador cambia, Android lo notica globalmente


a través de un mensaje de difusión. Podemos registrarnos de esos mensajes
para reaccionar en corcondancia. En esta práctica, vamos a añadir un recep-
tor de esos mensajes para actualizar el interfaz ante un cambio de estado.

1. Crea una clase interna y llámala BluetoothMonitor. Haz que herede


de BroadcastReceiver. En su método onReceive() obtén del intent
el nuevo estado del bluetooth y llama al método updateBluetoothUI()
de la clase principal pasándo ese estado como parámetro. Añadiremos
el argumento al método más adelante.

2. Crea un atributo nuevo de esa clase.

class BluetoothMonitor extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
int state = intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE, -1);
// En EXTRA_PREVIOUS_STATE tenemos el estado anterior,
// pero no lo usaremos.
updateBluetoothUI(state);
}
} // onReceive

} // BluetoothMonitor

private BluetoothMonitor bluetoothMonitor = new BluetoothMonitor();

3. En el onCreate() registra el receptor de los mensajes de difusión. Ten-


drás que hacerlo siempre, incluso cuando se recree la actividad. Dado
que vamos a añadir un parámetro a updateBluetoothUI, lo aprove-
charemos para que nuestra actividad indique correctamente el estado
si se rota y el onCreate() podemos reestructurarlo un poco.

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
91

tvEstadoBluetooth = (TextView)
findViewById(R.id.estadoBluetooth);
bluetooth = BluetoothAdapter.getDefaultAdapter();

if (bluetooth == null)
tvEstadoBluetooth.setText(R.string.noBluetooth);
else {
registerReceiver(bluetoothMonitor,
new IntentFilter(
BluetoothAdapter.ACTION_STATE_CHANGED));
updateBluetoothUI(bluetooth.getState());
}
if (savedInstanceState != null)
return;

if (!bluetooth.isEnabled()) {
Intent intent =
new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, CALLBACK_ENABLE_BLUETOOTH);
} // if-else bluetooth está activo
} // onCreate

4. Implementa el método onDestroy() para desregistrar el receptor.

@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(bluetoothMonitor);
}

5. Dado que ahora recibiremos por mensajes los cambios en el estado del
bluetooth, no necesitamos procesar la respuesta del cuadro de diálogo
en el que pedíamos al usuario que habilitara el bluetooth. Elimina el
método onActivityResult().
6. Añade dos nuevas cadenas en el chero de recursos:

bluetoothActivandose: Habilitando bluetooth...

bluetoothDesactivandose: Deshabilitando bluetooth...

7. Modica el método updateBluetoothUI() para que reciba un pará-


metro con el estado y muestre la cadena que corresponda. Por ser más
complejo, lleva a un método diferente la reacción ante la detección de
bluetooth activado.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
92 Capítulo 5. Bluetooth

private void updateBluetoothUI(int newState) {

int str;
switch(newState) {
case BluetoothAdapter.STATE_ON:
updateONBluetoothUI();
return;
case BluetoothAdapter.STATE_TURNING_ON:
str = R.string.bluetoothActivandose;
break;
case BluetoothAdapter.STATE_TURNING_OFF:
str = R.string.bluetoothDesactivandose;
break;
case BluetoothAdapter.STATE_OFF:
str = R.string.bluetoothDesactivado;
break;
default:
return; // ¾?
} // switch
tvEstadoBluetooth.setText(str);

} // initBluetoothUI

private void updateONBluetoothUI() {


String msj = getString(R.string.bluetoothActivado) +
": " + bluetooth.getName() +
"(" + bluetooth.getAddress() + ")";
tvEstadoBluetooth.setText(msj);
} // updateONBluetoothUI

8. Ejecuta la práctica en el móvil. Modica el estado del bluetooth y


comprueba cómo se actualiza la etiqueta.

Práctica 5.3: Visibilidad del dispositivo

Aunque el bluetooth esté activado, por seguridad Android oculta su exis-


tencia de manera que otros dispositivos no puedan encontrarle aunque hagan
un escaneo. Como ocurría con el estado global, es posible preguntar al adap-
tador sobre su estado de visibilidad, recibir noticaciones de difusión cuando
ésta cambia, y pedir al usuario que haga visible el dispositivo durante 120

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
93

segundos.

En esta práctica vamos a mostrar al usuario la visibilidad de su dispositi-


vo cuando el bluetooth esté habilitado, permitiéndole, además, hacer visible
si no lo estaba ya.

1. Modica el layout para que el nodo raíz sea un LinearLayout con


orientación en vertical.

2. Añade dentro un RelativeLayout debajo de la etiqueta, y dale co-


mo identicador @+id/panelBTOn. En él incluiremos los controles que
mostraremos cuando el bluetooth esté habilitado, por lo que lo mos-
traremos u ocultaremos en bloque.

3. Añade dentro una etiqueta, donde mostraremos el estado de visibilidad


del dispositivo a través de bluetooth, y un botón que el usuario podrá
pulsar para hacerlo visible temporalmente.

<LinearLayout ...
android:orientation="vertical">

...

<RelativeLayout
android:id="@+id/panelBTOn"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/estadoVisibilidad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"/>

<Button
android:id="@+id/btHacerVisible"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/estadoVisibilidad"
android:text="@string/hacerVisible"
android:onClick="onHacerVisible"/>

</RelativeLayout>

</LinearLayout>

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
94 Capítulo 5. Bluetooth

4. Añade las nuevas cadenas:

bluetoothVisible: El dispositivo es visible por bluetooth

bluetoothConectable: El dispositivo es visible para dispositivos


emparejados

bluetoothInvisible: El dispositivo es invisible por bluetooth

hacerVisible: Hacer visible

5. Añade un método updateVisibilityUI() que será llamado para ac-


tualizar la etiqueta y el botón relacionados con la visibilidad del dispo-
sitivo. Recibirá como parámetro el tipo de visibilidad actual, y mostra-
rá una cadena u otra, y activará o no el botón. El método será llamado
únicamente cuando el bluetooth esté activado.

private void updateVisitilityUI(int state) {


int str;
switch(state) {
case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
str = R.string.bluetoothVisible;
break;
case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
str = R.string.bluetoothConectable;
break;
case BluetoothAdapter.SCAN_MODE_NONE:
str = R.string.bluetoothInvisible;
break;
default:
return;
}
TextView tv = (TextView) findViewById(R.id.estadoVisibilidad);
tv.setText(str);
boolean botonHabilitado;
botonHabilitado =
(state !=
BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
findViewById(R.id.btHacerVisible).setEnabled(botonHabilitado);
}

6. Amplía el receptor de mensajes del sistema para que atienda tam-


bién a las noticaciones relacionadas con la visibilidad. Modica el
onCreate() para registrarlo también para ese tipo de mensajes.

protected void onCreate(Bundle savedInstanceState) {


...

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
95

else {
registerReceiver(bluetoothMonitor,
new IntentFilter(
BluetoothAdapter.ACTION_STATE_CHANGED));
registerReceiver(bluetoothMonitor,
new IntentFilter(
BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
updateBluetoothUI(bluetooth.getState());
}
...
}

class BluetoothMonitor extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
...
}
else if (action.equals(
BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)) {
int state = intent.getIntExtra(
BluetoothAdapter.EXTRA_SCAN_MODE, -1);
// En EXTRA_PREVIOUS_SCAN_MODE tenemos el estado anterior,
// pero no lo usaremos.
updateVisitilityUI(state);
}
} // onReceive

} // BluetoothMonitor

7. Implementa el método asociado al evento de pulsación, para que se


pida a Android que le pregunte al usuario si quiere hacer visible el
dispositivo. Declara una constante en la clase con el valor que nos
devolverá el cuadro de diálogo en onActivityResult(), aunque dado
que tenemos el receptor de los mensajes de difusión no lo atenderemos.

public void onHacerVisible(View v) {


startActivityForResult(
new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE),
CALLBACK_REQUEST_DISCOVERABLE);
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
96 Capítulo 5. Bluetooth

private final static int CALLBACK_REQUEST_DISCOVERABLE = 2;

8. Cuando se actualiza la vista general del estado de bluetooth hay que


ocultar todos los controles relacionados con el estado habilitado de
bluetooth. Modica updateBluetoothUI() para que oculte siempre el
panel, dado que si el bluetooth estaba visible salíamos antes.

private void updateBluetoothUI(int newState) {

...
switch(newState) {
...
} // switch
...
findViewById(R.id.panelBTOn).setVisibility(View.INVISIBLE);

} // initBluetoothUI

9. Del mismo modo, en el método llamado cuando bluetooth está habili-


tado, haz visible el panel y actualiza la etiqueta de visibilidad.

private void updateONBluetoothUI() {


...
updateVisitilityUI(bluetooth.getScanMode());
findViewById(R.id.panelBTOn).setVisibility(View.VISIBLE);
} // updateONBluetoothUI

10. Ejecuta la práctica. Comprueba que funciona correctamente.

Práctica 5.4: Dispositivos emparejados

Aunque es posible desde una actividad poner al adaptador en modo bús-


queda, e informar al usuario de los dispositivos encontrados, La búsqueda
es una tarea asíncrona que requiere trabajo en segundo plano que metería
mucho ruido en la práctica.

Lo que es sencillo es averiguar los dispositivos emparejados, es decir aque-


llos con los que ya se ha realizado alguna comunicación previa y son capaces
de comunicarse de manera encriptada. Para eso, basta con llamar al méto-
do getBoundedDevices() del objeto BluetoothAdapter. Obtendremos un
Set<BluetoothDevice>, con todos los dispositivos, a los que les podremos
pedir el nombre, el tipo, dirección y mucha otra información de interés.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
97

En esta práctica vamos a incluir una lista a nuestra ventana, que muestre
los dispositivos bluetooth emparejados. Por simplicidad, utilizaremos una
lista básica aprovechando el soporte de Android. Dado que la conversión de
los BluetoothDevice a cadena devuelven la dirección física (y no el nombre)
será eso lo que mostremos.

1. Añade una nueva cadena noDevices con texto No hay dispositivos em-
parejados.

2. Modica el layout para añadir, dentro del bloque visible sólo cuando
bluetooth está activo, una lista y una etiqueta que se mostrará si no
hay dispositivos emparejados.

<ListView
android:id="@+id/listaDispositivos"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/btHacerVisible"
android:layout_alignParentBottom="true"/>

<TextView
android:id="@+id/noDevices"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_below="@+id/btHacerVisible"
android:layout_alignParentBottom="true"
android:text="@string/noDevices"/>

3. Crea en la clase el atributo listaDispositivos de tipo ListView. En


el onCreate(), busca la lista, asígnala al atributo, y asóciala la vista
a mostrar cuando la lista esté vacía.

protected void onCreate(Bundle savedInstanceState) {


...
listaDispositivos = (ListView)
findViewById(R.id.listaDispositivos);
listaDispositivos.setEmptyView(findViewById(R.id.noDevices));
...
}

private ListView listaDispositivos;

4. Añade en el método updateONBluetoothUI() código para obtener la


lista de dispositivos emparejados y añadirlos a la lista:

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
98 Capítulo 5. Bluetooth

private void updateONBluetoothUI() {


...
Set<BluetoothDevice> devices;
devices = bluetooth.getBondedDevices();
final ArrayAdapter<BluetoothDevice> aa;
aa = new ArrayAdapter(this,
android.R.layout.simple_list_item_1,
devices.toArray());
listaDispositivos.setAdapter(aa);
for (BluetoothDevice bt : devices) {
android.util.Log.i("BT", bt.getName());
}
} // updateONBluetoothUI
5. Prueba la práctica. Deberías ver aparecer los nombres de los dispositi-
vos con los que hayas intercambiado algo por bluetooth con el móvil.
Si no ves nada, ve a la conguración de bluetooth y empareja tu dis-
positivo con el de algún compañero y vuelve a probar.

Práctica 5.5: Servidor bluetooth

En esta práctica haremos que la aplicación se quede escuchando a la es-


pera de algún cliente. Para poderla probar, necesitaremos hacer también la
práctica siguiente, y ejecutarlas en dos móviles diferentes que estén empare-
jados.

En este caso, haremos que el dispositivo se quede siempre escuchando en


el adaptador. En bluetooth no existe el concepto de puerto. En lugar de eso,
las aplicaciones indican su deseo de quedarse escuchando en un UUID (Uni-
1
versally unique identier . Los clientes de ese servicio se conectan usando
ese UUID, sabiendo que en el otro extremo estará la aplicación servidora.

No obstante, dado que sobre bluetooth se ejecutan diferentes tipos de ser-


vicios, existen algunos UUID predeterminados. Más allá de eso, los primeros
bytes de los UUID especican la clase de servicio que se proporciona, de lo
que depende en última instancia cómo funcionará el protocolo. Para el tipo
de comunicación que nos ocupa, el UUID deberá comenzar por 00001101.
1
Un UUID es un número de 16 bytes aleatorio que sirve como identicador universal.
Cuando se necesita un identicador único para algo, se genera un UUID nuevo (de manera
aleatoria). Dado el ancho de su representación, se considera estadísticamente imposible
que el UUID obtenido vaya a coincidir con el obtenido por nadie más en el mundo: si cada
habitante de la tierra se dedicara a obtener 1.000 UUID por segundo se necesitarían más
de 100 millones de veces la edad del universo para obtenerlos todos.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
99

Como servidores, el primer paso es indicar al adaptador nuestro deseo de


escuchar en un UUID. Obtendremos un server socket de bluetooth, sobre el
que nos sentaremos a esperar un cliente. Cuando llegue, el sistema nos de-
volverá un socket con el que podremos comunicarnos con el cliente. El server
socket lo cerraremos y, llegado el caso, lo abriremos otra vez posteriormente.
Esto es necesario porque bluetooth no soporta varios clientes simultáneos
sobre el mismo UUID.

Ten en cuenta que muchas de las etapas del proceso requieren tiempo (en
concreto, la espera del cliente es bloqueante). Debido a eso tendremos que
realizarlo en una hebra independiente para no bloquear la hebra principal de
la aplicación.

En la práctica, crearemos una nueva hebra con todo el código del servidor.
Cuando detectemos que el bluetooth está activado, lanzaremos la hebra sobre
un UUID para que espere al cliente. En cuanto llegue, le mandaremos una
cadena de saludo, nos desconectaremos, y empezaremos otra vez.

Cuando la actividad se destruya, detendremos también la hebra a la


espera de clientes. Para eso, basta con cancelar el server socket, lo que
hará que la llamada al método que espera un cliente vuelva inmediatamente.

1. Haz una copia de la práctica de la sección anterior.

2. Añade el permiso BLUETOOTH_ADMIN para que la aplicación pueda hacer


uso de comunicación por bluetooth.

3. Crea la clase ServerThread que heredará de Thread y que hará todo


el trabajo del lado del servidor. Crea un atributo de dicha clase, sin
inicializar.

UUID SERVICE_UUID = UUID.fromString("00001101-....");

private class ServerThread extends Thread {

BluetoothServerSocket _serverSocket;

public void run() {

while (!isInterrupted()) {
try {
_serverSocket =
bluetooth.listenUsingRfcommWithServiceRecord(
"LibroAzul", SERVICE_UUID);
} catch (IOException e) {
escribe("ERROR: no pude obtener el server socket");
break;
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
100 Capítulo 5. Bluetooth

BluetoothSocket socket = null;


try {
socket = _serverSocket.accept();
} catch (IOException e) {
// No hacemos nada. Quizá nos cancelaron...
}
if (socket != null) {
// ½Tenemos un cliente! Cerramos el server socket.
try {
_serverSocket.close();
_serverSocket = null;
} catch (IOException e) {
}
escribe("½½NOS HA LLEGADO UN CLIENTE!!");
try {
socket.close();
} catch (IOException e) {
}
}
} // while

} // run

// Genera un toast con el mensaje.


private void escribe(final String str) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, str,
Toast.LENGTH_LONG).show();
}
});
}

// Detenemos la hebra
public void cancel() {
try {
interrupt();
if (_serverSocket != null)
_serverSocket.close();
} catch (IOException e) { }
}
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
101

ServerThread serverThread;

4. En el método updateONBluetoothUI() crea una hebra nueva de la


clase y iniciala.

serverThread = new ServerThread();


serverThread.start();

5. En el método onDestroy() anula la ejecución de la hebra del servidor


para liberar recursos.

if (serverThread != null)
serverThread.cancel();

De momento, no podrás probar la práctica hasta que no hagas la siguien-


te.

Práctica 5.6: Cliente bluetooth

El lado del cliente es similar. Necesitamos conocer el UUID, y usarlo


para obtener un socket. Ten en cuenta que el server socket lo conseguimos
a través del BluetoothAdapter; en el lado del cliente sin embargo hay que
utilizar ya BluetoothDevice, pues el socket se crea con el objetivo de conec-
tarse directamente a un dispositivo. Una vez conseguido el socket, se solicita
la conexión, y se espera. Si todo va bien, conseguiremos una conexión. La
conexión y comunicación con un dispositivo se ve seriamente afectada si el
adaptador está en modo búsqueda, por lo que antes de intentar conectarnos
con el servidor se recomienda detener el potencial escaneo activo. Además, la
conexión puede requerir bastante tiempo por lo que, de nuevo, se recomienda
utilizar una hebra auxiliar.

En esta práctica vamos a permitir al usuario seleccionar uno de los dis-


positivos emparejados en la lista, y lanzaremos una hebra que intentará co-
nectarse a él. Si en el otro extremo se ha lanzado la práctica anterior (o ésta,
pues compartirán el código) se realizará la conexión.

1. Haz una copia de la práctica anterior.

2. Crea una nueva clase ClientThread que herede de Thread. En el cons-


tructor deberá recibir el BluetoothDevice al que se intentará conectar.
Cuando se lance, creará el socket, intentará conectarse, y mostrará un
mensaje cuando lo haga. Crea también un atributo de esa clase.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
102 Capítulo 5. Bluetooth

private class ClientThread extends Thread {

private BluetoothDevice _device;


private BluetoothSocket _socket;

public ClientThread(BluetoothDevice device) {


_device = device;
}

public void run() {

try {
_socket = _device.
createRfcommSocketToServiceRecord(
SERVICE_UUID);
} catch (IOException e) {
escribe("ERROR: no pude obtener el socket");
_socket = null;
return;
}

bluetooth.cancelDiscovery();
try {
_socket.connect();
} catch (IOException connectException) {
escribe("ERROR: no conseguí conexión");
try {
_socket.close();
} catch (IOException closeException) { }
_socket = null;
return;
}
escribe("½½CONECTADOS!!");
try {
_socket.close();
} catch (IOException e) {
}
_socket = null;
}

// Genera un toast con el mensaje.


private void escribe(final String str) {
runOnUiThread(new Runnable() {
@Override

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
103

public void run() {


Toast.makeText(MainActivity.this, str,
Toast.LENGTH_LONG).show();
}
});
}

public void cancel() {


try {
_socket.close();
} catch (IOException e) { }
}

} // ClientThread

3. En el método updateONBluetoothUI() añade a la lista un listener para


detectar la pulsación sobre la lista. Cuando lo hagas, lanza una de esas
hebras sobre el dispositivo seleccionado.

private void updateONBluetoothUI() {


...
listaDispositivos.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent,
View view, int position, long id) {
clientThread = new ClientThread(aa.getItem(position));
clientThread.start();
}
});
...

4. En el método onDestroy() cancela la ejecución de la hebra del cliente


si existe.

if (clientThread != null)
clientThread.cancel();

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
104 Capítulo 5. Bluetooth

Notas bibliográcas
http://developer.android.com/guide/topics/connectivity/bluetooth.
html
http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.
html
http://developer.android.com/reference/android/bluetooth/BluetoothDevice.
html
Bluetooth Assigned Numbers : https://www.bluetooth.org/en-us/specification/
assigned-numbers

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Capítulo 6

Conexión por red


Resumen: En este capítulo aprenderemos cómo podemos indagar so-
bre el estado de conectividad de Android, y cómo conseguir que el sis-
tema nos informe ante algún cambio. También realizaremos un cliente
y un servidor sencillos utilizando el API básico de conexión con sockets,
y terminaremos haciendo una aplicación que hace uso de un servicio
web.

Práctica 6.1: Estado de la red

La conectividad a la red en Android se puede conseguir a través de dife-


rentes adaptadores, dependiendo del dispositivo y del momento. Aunque en
principio el sistema operativo da soporte para Ethernet e incluso WiMAX,
la mayor parte de los dispositivos existentes basan su conectividad en wi-
y redes móviles (como 3g o 4g).

Para obtener información sobre el estado de la conectividad del dispositi-


vo se hace uso del servicio del gestor de conectividad, ConnectivityManager.
A él le podemos preguntar sobre el estado de conexión de cada uno de los
diferentes tipos de adaptadores soportados, así como el predenido, que será
el que se utilice para encaminar tráco. Para eso, la aplicación requiere el
permiso ACCESS_NETWORK_STATE
En esta práctica vamos a colocar varias etiquetas en la actividad princi-
pal, en las que mostraremos el estado de conectividad del dispositivo.

1. Crea un nuevo proyecto y llámalo libro.azul.estadodelared.


2. Dado que querremos obtener el estado de la red, modica el chero de
maniesto para solicitar el permiso.

<uses-permission

105
106 Capítulo 6. Conexión por red

android:name="android.permission.ACCESS_NETWORK_STATE" />

1. En el layout, queremos mostrar tres líneas. La primera hará referen-


cia al estado de la conexión por wi-, la segunda a la conexión móvil
(3g/4g), y la tercera a cuál es la predenida. Pondremos tres etiquetas
en un LinearLayout, y las construiremos aprovechando cadenas con
formato.

<LinearLayout ...
android:orientation="vertical">

<TextView ...
android:id="@+id/wifi"
android:text="@string/wifi"/>

<TextView ...
android:id="@+id/redMovil"
android:text="@string/redMovil"/>

<TextView ...
android:id="@+id/estadoRed"
android:layout_marginTop="6dp"/>

</LinearLayout>

2. Crea las cadenas asociadas a las etiquetas, como por ejemplo:

wifi: Wi-: %1$s

redMovil: Red móvil: %1$s

hayRed: Hay conectividad de red ( %1$s)

noHayRed: No hay conectividad de red

3. Por comodidad, crea un método para establecer el texto de una etique-


ta.

protected void setLabel(int id, String str) {


TextView tv = (TextView) findViewById(id);
tv.setText(str);
}

4. Crea un método para actualizar las etiquetas en función del estado de


red devuelto por el ConnectivityManager.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
107

protected void updateNetworkType(NetworkInfo ni,


NetworkInfo predefinida,
int labelId, int textId) {
String template = getString(textId);
String str = String.format(template, ni.getState());
if ((predefinida != null) &&
ni.getType() == predefinida.getType())
str += " (*)";

setLabel(labelId, str);
}

protected void updateNetworkStatus() {

ConnectivityManager cm = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);

NetworkInfo predefinida = cm.getActiveNetworkInfo();

NetworkInfo ni;
ni = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
updateNetworkType(ni, predefinida, R.id.wifi, R.string.wifi);

ni = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
updateNetworkType(ni, predefinida, R.id.redMovil,
R.string.redMovil);

String str;
if ((predefinida != null) && predefinida.isConnected()) {
// Hay conexión.
str = getString(R.string.hayRed);
str = String.format(str, predefinida.getTypeName());
}
else
str = getString(R.string.noHayRed);
setLabel(R.id.estadoRed, str);
}

5. En el onCreate() llama a updateNetworkStatus().


6. Prueba la práctica en un móvil, con diferentes conguraciones de la
red.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
108 Capítulo 6. Conexión por red

Práctica 6.2: Nocaciones del cambio de estado

El gestor de conectividad envía noticaciones de difusión al sistema cuan-


do cambia el estado de la red. Las aplicaciones pueden registrar de manera
estática (en su chero de maniesto) un receptor, de forma que cuando cam-
bie el estado el receptor será lanzado independientemente de que lo estuviera
algún otro componente de la aplicación. Por ejemplo, un cliente de correo
podría querer registrar un receptor de mensajes para ser lanzado cuando se
detecte un cambio. Si éste ha sido para conseguir conexión, podría consultar
si hay mensajes nuevos, y lanzar el proceso de consulta periódica.

No obstante, salvo en casos especícos, no se recomienda hacerlo así. Lo


normal será que la aplicación quiera saber si cambia el estado de conectivi-
dad mientras esté activa, sin preocuparle si cambia cuando el usuario no la
haya lanzado: a un navegador no le importa saber que vuelve a haber red
si el usuario no lo está usando. Para evitar que el receptor se lance conti-
nuamente, es preferible registrarlo y desregistrarlo de manera dinámica de
manera similar a como hicimos en la práctica 5.2.

1. Haz una copia de la práctica anterior.

2. Dene un atributo networkMonitor que sea un objeto de una subclase


de BroadcastReceiver. En el método onReceive(), que será llamado
por el sistema cuando cambie la conguración de red del dispositivo,
llama al método updateNetworkState() que hemos programado antes.
Puedes hacerlo todo a la vez utilizando una clase anónima.

BroadcastReceiver networkMonitor = new BroadcastReceiver() {


@Override
public void onReceive(Context context, Intent intent) {
updateNetworkStatus();
}
};

3. En el método onCreate() registra el objeto como receptor de mensajes,


interesándose por los cambios de conguración de red.

protected void onCreate(Bundle savedInstanceState) {


...

IntentFilter filter = new IntentFilter(


ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(networkMonitor, filter);
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
109

4. En el onDestroy(), desregistralo para no tener fugas de contextos.

@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkMonitor);
}

5. Ejecuta la práctica, modicando, con ella lanzada, la conguración de


red. Comprueba que se actualiza el el estado correctamente.

Práctica 6.3: Cliente de sumas

En esta práctica vamos a crear un cliente de red sencillo en Android. Para


poder hacerlo, necesitaremos antes un servidor al que conectarnos. Vamos a
empezar creando el servidor, directamente en Java. El servidor se quedará
escuchando en un puerto TCP (en el 9898) y esperará recibir clientes de
sumas. El cliente le enviará dos números nada más conectarse, el servidor
contestará con el resultado de sumar ambos y cerrará la conexión.

Para eso, el servidor crea un server socket, y entra en un bucle innito


a la espera de clientes. Cada vez que llegue uno leerá una linea con los dos
números y devolverá la salida.

1. Crea un programa en Java que implemente el servidor.

import java.net.*;
import java.io.*;
import java.util.Scanner;

public class ServidorSumas {

public static final int SERVER_PORT = 9898;

public static void main(String[] args) {

try {
ServerSocket ss = new ServerSocket(SERVER_PORT);
while(true) {
System.out.println("Esperando un cliente...");
Socket cliente = ss.accept();

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
110 Capítulo 6. Conexión por red

Scanner sc = new Scanner(cliente.getInputStream());


PrintStream ps = new PrintStream(
cliente.getOutputStream());
int a, b;
a = sc.nextInt();
b = sc.nextInt();
System.out.println(a + " + " + b + " = " + (a+b));
ps.println(a+b);
sc.close();
ps.close();
} // while
} catch(IOException e) {
System.out.println(e);
}
} // main
}

2. Compila el servidor y ejecútalo.

3. Pruébalo realizando una conexión manual con cualquier cliente de red


sencillo. Escríbe dos números, y deberías ver el resultado.

4. Deja lanzado el servidor.

Queremos que el cliente en Android se conecte al servidor cada segundo,


le envíe dos números aleatorios y muestre al usuario el resultado. Además,
queremos que sea cuidadoso con el estado de la red para que no se conecte
si no hay conexión, y que sea educado y cuando la actividad pierda el foco
también deje de hacer las conexiones.

5. Haz una copia de la práctica anterior.

6. Renombra el paquete principal y llamalo libro.azul.sumador.


7. Modica el layout para que tenga una etiqueta en la parte superior de
la ventana en la que escribiremos información sobre las sumas, y una
en la parte inferior donde mostraremos el estado de la conexión. Utiliza
un RelativeLayout.

<RelativeLayout ...>

<TextView
android:id="@+id/resultado"
android:text="@string/hebraDetenida"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
111

<TextView
android:id="@+id/estadoRed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>

</RelativeLayout>

8. Modica las cadenas disponibles. Cambia el título de la aplicación por


Sumas, manten las cadenas hayRed y noHayRed, y borra el resto. Añade
una hebraDetenida con el texto Hebra cliente detenida.

9. Crea dos constantes en la clase con la IP y puertos del servidor. Ten


en cuenta que en una aplicación real permitiríamos al usuario congu-
rar estos valores desde la propia aplicación, y los guardaríamos en las
preferencias.

protected static final String SERVER_IP = "10.0.2.2";


protected static final int SERVER_PORT = 9898;

10. Crea una nueva clase ClienteSumas que se encargue del cliente de red.
No vamos a hacerla que herede de AsyncTask porque impediríamos la
ejecución de cualquier otra tarea en la aplicación, dado que pretende-
mos que el cliente dure por siempre. Cosas importantes:

Aunque sea un cliente innito, la hebra debe ser interrumpible.


Si desde fuera se le solicita que termine, debe hacerlo limpiamente.

Queremos que muestre la cadena Conectando... cuando intente el


inicio de conexión al servidor.

Cuando se conecte, queremos que elija dos números aleatorios, los


mande al servidor y espere el resultado.

Cuando lo recoja, queremos ver la suma solicitada y el resultado


en la etiqueta superior.

Si se produce cualquier error, queremos verlo.

Queremos que espere un segundo entre solicitud y solicitud.

Necesitamos un atributo de la clase para guardar la hebra actual


y poderla manipular.

class ClienteSumas extends Thread {

TextView tv;

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
112 Capítulo 6. Conexión por red

@Override
public void run() {
tv = (TextView) findViewById(R.id.resultado);
while(!isInterrupted()) {
try {
setText("Conectando...");
Socket socket = new Socket(SERVER_IP, SERVER_PORT);
int a, b;
String result;
a = (int) (Math.random() * 1000); // 0..999
b = (int) (Math.random() * 1000);
PrintStream ps = new PrintStream(
socket.getOutputStream());
Scanner sc = new java.util.Scanner(
socket.getInputStream());
ps.println("" + a + " " + b);
result = sc.next();
setText("" + a + " + " + b + " = " + result);
socket.close();
}
catch (Exception e) {
if (!isInterrupted())
setText("ERROR: " + e);
}
try {
Thread.sleep(1000);
}
catch(InterruptedException ie) {
interrupt();
}
} // while no haya que parar
setText(R.string.hebraDetenida);
android.util.Log.i("ClienteSumador", "Hebra detenida");
} // run

protected void setText(final String s) {


runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(s);
}
});
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
113

protected void setText(final int stringId) {


runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(stringId);
}
});
}

} // ClienteSumas

ClienteSumas _cliente;

11. El atributo _cliente será null cuando no tengamos la hebra lanza-


da (porque estamos suspendidos o porque no hay conexión a la red
disponible). Cuando tengamos la hebra lanzada y queramos pararla,
tendremos que solicitar su detención y borrar el atributo. Haz un mé-
todo detenerHebraCliente() que lo haga.

protected void detenerHebraCliente() {


if (_cliente != null) {
_cliente.interrupt();
_cliente = null;
}
}

12. Para ser mejores ciudadanos, el broadcast receiver vamos a registrarlo


en la pareja de métodos onStart() y onStop(). Así cuando la activi-
dad no sea visible no nos preocuparemos de los cambios de red. Crea
ambos métodos, llama en cada caso al equivalente en la clase padre, y
mueve el registro y desregistro del broadcast receiver. Además, cuando
nos suspendamos tendremos que detener la hebra cliente.

@Override
protected void onStart() {
super.onStart();
IntentFilter filter = new IntentFilter(
ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(networkMonitor, filter);
}

@Override
protected void onStop() {
super.onStop();

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
114 Capítulo 6. Conexión por red

unregisterReceiver(networkMonitor);
detenerHebraCliente();
}

13. Modica el método updateNetworkStatus() para que no se preocupe


de las etiquetas de los detalles sobre qué tipo de red es la disponible.
Lo único que nos interesa es la parte nal, mirando si tenemos o no
conectividad. Si la tenemos, lanzamos la hebra, si no lo está ya, y si
no, la detenemos.

protected void updateNetworkStatus() {

ConnectivityManager cm = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);

NetworkInfo predefinida = cm.getActiveNetworkInfo();

String str;
if ((predefinida != null) && predefinida.isConnected()) {
// Hay conexión.
str = getString(R.string.hayRed);
str = String.format(str, predefinida.getTypeName());
if (_cliente == null) {
_cliente = new ClienteSumas();
_cliente.start();
}
}
else {
str = getString(R.string.noHayRed);
detenerHebraCliente();
}
setLabel(R.id.estadoRed, str);
}

14. Prueba la aplicación, y comprueba que funciona. Prueba a detener


el servidor, anular la conectividad de red u ocultar la aplicación y
comprueba que funciona bien.

Práctica 6.4: Servidor de sumas

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
115

1. Haz una copia de la práctica 4.1. Renombra el paquete principal a


libro.azul.servidorsumas.
2. Renombra el servicio de ServicioBasico a ServidorSumas.
3. Modica el texto de la cadena app_name para que sea Servidor de
sumas

4. Crea una nueva clase HebraServidora que herede de Thread. Para el


método run() copia el código del servidor de la práctica anterior que
ejecutamos directamente en Java. Sustituye los System.out.println
por android.util.Log.i(TAG, ...).
5. ¾Consideras razonable que la clase anterior sea interna a ServidorSuma?
6. Modica la clase para que sea interrumpible.

7. Crea un atributo de la clase anterior, y llámalo _hebraServidora.


8. En el método onCreate() crea la hebra y lánzala.

9. En el método onDestroy() detenla.

10. Si el servicio estaba lanzado y Android se ve obligado a destruirlo,


querríamos que se volviera a lanzar. En onStartCommand(), devuelve
START_STICKY.
11. Lanza la aplicación en un AVD, y pulsa el botón de Lanzar servicio.

12. Usando telnet conéctate al puerto de control del AVD y redirige el


puerto 9898 del AVD al puerto 9000 del antrión. .

13. Usando telnet conéctate al puerto 9000 del antrión y hazle una con-
sulta. Comprueba que responde correctamente.

14. Vuelve a la conexión de conguración del AVD y elimina la redirección.


Si no lo has hecho ya, para el servidor en Java (que estará escuchando
en el puerto 9898 del antrión), y añade una redirección nueva para
que el puerto 9898 del AVD se mapee al puerto 9898 del servidor.

15. Lanza un segundo AVD y ejecuta en él la práctica anterior. Comprueba


que funciona.

En su estado actual, el servidor tiene muchos puntos de mejora. Algunas


cuestiones:

Conectate manualmente al servidor (con telnet, como hicimos para


probarlo antes en el punto 13) y escribe algo que no encaje con el
protocolo. ¾Qué ocurre? ¾Cómo lo resolverías?

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
116 Capítulo 6. Conexión por red

Si la conectividad de red se pierde, ¾qué ocurre? ¾Qué mejorarías y


cómo?

Si quisiéramos que el servidor se lanzara automáticamente cuando hu-


biera conexión de red, ¾qué harías?

Si el servidor no fuera tan rápido y quisiéramos tener múltiples clientes,


¾cómo lo harías?

Práctica 6.5: Viabicing

En esta práctica vamos a conectarnos a un servicio web del Ayuntamiento


de Barcelona, que pone a disposición de las aplicaciones la localización y
estado de sus estaciones de aparcamiento de bicicletas públicas. El servicio
web está accesible en http://wservice.viabicing.cat/v1/getstations.
php?v=1.

1. Crea un nuevo proyecto y llámalo Viabicing.

2. Añádele los permisos de monitorizar la red y acceder a Internet.

<uses-permission
android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.INTERNET" />

3. Crea una constante con la URL del servicio web que vamos a utilizar.

protected static final String WEB_SERVICE_URL =


"http://wservice.viabicing.cat/v1/getstations.php?v=1";

4. Como en prácticas anteriores, vamos a querer detectar cuándo se pro-


duce un cambio en el estado de la red. Crea el broadcast receiver ha-
bitual. En lugar de utilizar el método updateNetworkStatus() que
hemos estado utilizando, mete directamente en la clase anónima que
se mire si se ha pasado a tener o a perder la conexión, y que llame a
dos métodos nuevos, onNetworkOn() y onNetworkOff() según el ca-
so. Registra y desregistra el receptor de mensajes en el onStart() y
onStop(). Además, cuando la aplicación pierda el foco no queremos
que utilice la red, por lo que simularemos que se ha desconectado.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
117

@Override
protected void onStart() {
super.onStart();
IntentFilter filter = new IntentFilter(
ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(networkMonitor, filter);
}

@Override
protected void onStop() {
super.onStop();
unregisterReceiver(networkMonitor);
onNetworkOff();
}

protected void onNetworkOn() {

protected void onNetworkOff() {

BroadcastReceiver networkMonitor = new BroadcastReceiver() {


@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager cm = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);

NetworkInfo predefinida = cm.getActiveNetworkInfo();

if ((predefinida != null) && predefinida.isConnected())


// Hay conexión.
onNetworkOn();
else
onNetworkOff();
}
}; // networkMonitor

5. Cuando detectemos que la red está activa, vamos a lanzar una tarea
a través de una AsyncTask, que se descargará del servicio web la in-
formación. Vamos a querer pasarle desde la hebra principal la URL
donde está el servicio web, por lo que el tipo para el primer argumento
de la clase genérica será String. Las otras de momento las dejaremos

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
118 Capítulo 6. Conexión por red

en Void. Crea la clase vacía por el momento, y un atributo con ese


tipo.

class ObtenerBicis extends AsyncTask<String, Void, Void> {

@Override
protected Void doInBackground(String... params) {
return null;
}

@Override
protected void onPostExecute(Void aVoid) {
}

} // ObtenerBicis

ObtenerBicis _asyncTask;

6. Cuando se detecte la disponibilidad de la red, crea, si no lo está ya,


la AsyncTask y lánzala. Cuando se detecte la desconexión, cancelala.
Además, para mantener correctamente el atributo de la clase, en el
método onPostExecute() de la tarea asíncrona ponlo a null para
marcar que ha terminado.

protected void onNetworkOn() {


if (_asyncTask == null) {
_asyncTask = new ObtenerBicis();
_asyncTask.execute(WEB_SERVICE_URL);

}
}

protected void onNetworkOff() {


if (_asyncTask != null) {
_asyncTask.cancel(true);
_asyncTask = null;
}
}

class ObtenerBicis extends AsyncTask<String, Void, Void> {

...

protected void onPostExecute(Void aVoid) {

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
119

_asyncTask = null;
}

...

7. En la tarea asíncrona vamos a conectarnos al servicio web y obtener


el XML. Para eso, utilizamos las clases URL y HttpURLConnection, de
la librería estándar de Java. En las primeras versiones de Android la
implementación tenía muchos errores y lo habitual era utilizar la libre-
ría de Apache para conexiones HTTP, pero desde Android 2.3 Google
recomienda hacer uso de HttpURLConnection. Por comodidad, vamos
a hacer un método nuevo en la ObtenerBicis que realice la conexión
y devuelve un InputStream del que se puede obtener la respuesta.

private InputStream downloadUrl(String urlString)


throws IOException {
URL url = new URL(urlString);
HttpURLConnection httpCon;
httpCon = (HttpURLConnection) url.openConnection();
httpCon.setRequestMethod("GET");
httpCon.setDoInput(true);
httpCon.connect();
return httpCon.getInputStream();
}

8. Para probar que la lectura se realiza correctamente, vamos a llamarlo


desde doInBackground() y vamos a escribir todo el contenido de la
cadena en el log.

try {
InputStream is = downloadUrl(params[0]);
java.util.Scanner s;
s = new java.util.Scanner(is);
while (s.hasNext())
android.util.Log.i(TAG, s.nextLine());
s.close();
} catch (IOException e) {
e.printStackTrace();
}

9. Declara en la clase de la actividad el atributo TAG convenientemente.

10. Comprueba que todo funciona correctamente.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
120 Capítulo 6. Conexión por red

La cadena devuelta por el servidor es un XML que tenemos que analizar


y guardarlo de manera cómoda para obtener por código la información. Si
analizas el valor devuelto es fácil intuir la estructura:

<?xml version="1.0" encoding="UTF-8"?>


<bicing_stations>
<updatetime> tiempo unix </updatetime>
<station>
<id>1</id>
<type>BIKE</type>
<lat>41.397952</lat>
<long>2.180042</long>
<street>...</street>
<height>...</height>
<streetNumber>...</streetNumber>
<nearbyStationList>...</nearbyStationList>
<status>...</status>
<slots>...</slots>
<bikes>...</bikes>
</station>
<station> .... </station>
</bicing_stations>

No queremos conservar toda la información. Nos interesa el tiempo unix


(última actualización de los datos), y para cada lugar, el identicador, la calle
y número, y el número de huecos libres y de bicis. Crearemos dos clases, una
para la información de cada localización, y otra para la lista de localizaciones
y el instante de la última actualización.

1. Crea una nueva clase (en un chero separado) que guarde la informa-
ción de cada localización.

class Station {

public int id;

public String street;

public String streetNumber; // Cadena; a veces no hay ("")

public int slots;

public int bikes;

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
121

public String toString() {


StringBuilder sb = new StringBuilder();
sb.append(id).append(": ");
sb.append(street).append(", no ").append(streetNumber);
sb.append("; ").append(slots).append(" slots; ");
sb.append(bikes).append(" bikes");
return sb.toString();
}

2. Crea una nueva clase que guare una lista de localizaciones y el tiempo
Unix de la última actualización.

class ViabicingInfo {

public void addStation(Station s) {


stations.add(s);
}

List<Station> stations = new ArrayList<Station>();

int timestamp = -1;

Para hacer el análisis del XML vamos a utilizar un XML Pull parser in-
tegrado con Android. Esta familia de analizadores facilita la programación a
través de analizadores descendentes. La librería recorre, bajo demanda, cada
elemento del XML. En cada paso le preguntamos qué viene a continuación,
y actuamos en consecuencia. Así, por ejemplo, si nos encontramos un nuevo
elemento de tipo station invocaremos a un método auxiliar encargado de
analizar los elementos <station> y que devolverá un objeto Station.
Crea una nueva clase ViabicingXMLParser que contenga los métodos
encargados del análisis, todos ellos estáticos.

public class ViabicingXMLParser {

private static final String ns = null;

public static ViabicingInfo parse(InputStream is)


throws XmlPullParserException, IOException {

try {

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
122 Capítulo 6. Conexión por red

XmlPullParser parser = Xml.newPullParser();


parser.setFeature(
XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(is, null); // Codificación predefinida.
parser.nextTag(); // Nos saltamos el "inicio"
return readStations(parser);
} finally {
is.close();
}

} // parse

//------------------------------------------------------------

private static ViabicingInfo readStations(XmlPullParser parser)


throws XmlPullParserException, IOException {

ViabicingInfo result = new ViabicingInfo();

parser.require(XmlPullParser.START_TAG, ns, "bicing_stations");

while (parser.next() != XmlPullParser.END_TAG) {


if (parser.getEventType() != XmlPullParser.START_TAG)
continue;

String name = parser.getName();


switch(name) {
case "station":
result.addStation(readStation(parser));
break;
case "updatetime":
if (result.timestamp != -1) {
// ½½Ya nos habíamos encontrado uno!!
// El XML está roto.
throw new XmlPullParserException("Two timestamps");
}
result.timestamp =
Integer.parseInt(readString(parser));
break;
default:
// ¾¾??
skip(parser);
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
123

}
return result;

} // readStations

//-----------------------------------------------------------

private static Station readStation(XmlPullParser parser)


throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, ns, "station");
Station result = new Station();
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
switch(name) {
case "id":
result.id = Integer.parseInt(readString(parser));
break;
case "street":
result.street = readString(parser);
break;
case "streetNumber":
result.streetNumber = readString(parser);
break;
case "slots":
result.slots = Integer.parseInt(readString(parser));
break;
case "bikes":
result.bikes = Integer.parseInt(readString(parser));
break;
default:
skip(parser);
} // switch
} // while
return result;

} // readStation

//--------------------------------------------------------

private static String readString(XmlPullParser parser)


throws XmlPullParserException, IOException {

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
124 Capítulo 6. Conexión por red

String result;
if (parser.next() == XmlPullParser.TEXT) {
result = parser.getText();
parser.nextTag();
return result;
}
else
return "";

} // readString

//---------------------------------------------------------

private static void skip(XmlPullParser parser)


throws XmlPullParserException, IOException {

if (parser.getEventType() != XmlPullParser.START_TAG)
throw new IllegalStateException();

int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
} // skip

} // ViabicingXMLParser

3. Modica el método doInBackground() para que utilice la clase anterior


para analizar el XML descargado y obtenga la información completa.
Devuelve un objeto ViabicingInfo como resultado. Tendrás que ajus-
tar convenientemente los tipos genéricos de la AsyncTask de la que
estamos heredando, así como el prototipo de onPostExecute(). Para
probar, escribe en el log la información recopilada.

protected ViabicingInfo doInBackground(String... params) {

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
125

ViabicingInfo viabicingInfo;
try {
InputStream is = downloadUrl(params[0]);
viabicingInfo = ViabicingXMLParser.parse(is);
android.util.Log.i(TAG, "Ultima actualizacion: " +
viabicingInfo.timestamp);
for (Station st: viabicingInfo.stations) {
android.util.Log.i(TAG, st.toString());
}
} catch (IOException e) {
android.util.Log.i(TAG, e.toString());
e.printStackTrace();
} catch (XmlPullParserException e) {
android.util.Log.i(TAG, e.toString());
e.printStackTrace();
}
return viabicingInfo;
}

Ya sólo nos falta enseñarselo al usuario.

1. Añade las cadenas habituales de Hay conectividad de red o No hay


conectividad de red.

2. Pon una etiqueta en la parte inferior de la ventana donde mostraremos


el estado de la red.

3. Pon una etiqueta en la parte superior donde mostraremos la última


actualización de los datos.

4. Pon en medio un ListView.

<RelativeLayout ...>
<TextView
android:id="@+id/ultimaActualizacion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"/>
<ListView
android:id="@+id/listaLocalizaciones"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/ultimaActualizacion"
android:layout_above="@+id/estadoRed"/>

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
126 Capítulo 6. Conexión por red

<TextView android:id="@+id/estadoRed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>
</RelativeLayout>

5. Modica los métodos onNetworkOn() y onNetworkOff() para que ac-


tualicen la etiqueta del estado de la red.

6. Añade en el onPostExecute() todo el código para enviar a la lista los


datos descargados.

protected void onPostExecute(ViabicingInfo viabicingInfo) {

TextView tvTimestamp = (TextView)


findViewById(R.id.ultimaActualizacion);
ListView lv = (ListView)
findViewById(R.id.listaLocalizaciones);
if (viabicingInfo == null) {
// Hubo error :(
[ Mensaje en la etiqueta ]
}
else {
ArrayAdapter<Station> aa;
aa = new ArrayAdapter(MainActivity.this,
android.R.layout.simple_list_item_1,
viabicingInfo.stations);
lv.setAdapter(aa);
Date time = new Date((long)viabicingInfo.timestamp*1000);
tvTimestamp.setText(time.toString());
}

_asyncTask = null;
}

1. Prueba la aplicación, y comprueba que aparece la lista de lugares.

Hay diferentes puntos de mejora:

Vista vacía para la lista. Podríamos poner directamente una imagen


animada de espera.

Procesar la cancelación.

Gestionar los errores y noticarselos al usuario.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Notas bibliográcas 127

Actualizar correctamente el interfaz cuando se pierde la conectividad


de red.

Limpiar las etiquetas para que no salgan entidades HTML.

Notas bibliográcas
http://developer.android.com/training/basics/network-ops/connecting.
html
http://developer.android.com/training/basics/network-ops/managing.
html
http://developer.android.com/training/basics/network-ops/xml.
html
http://developer.android.com/training/connect-devices-wirelessly/
index.html
http://developer.android.com/tools/help/emulator.html
http://developer.android.com/tools/devices/emulator.html

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Capítulo 7

Animaciones
Resumen: En este capítulo veremos el sistema de animaciones inte-
grado en Android desde su primera versión. Es útil para realizar efectos
dinámicos sencillos que, además, se pueden especicar como recursos
(en XML), lo que facilita su reutilización.

Práctica 7.1: Animaciones básicas entre fragmentos

Hoy en día el usuario espera interfaces dinámicas y amigables. Las anima-


ciones de elementos del interfaz de usuario se han convertido en habituales en
cualquier aplicación, y Android proporciona soporte para ellas de diferentes
formas.

En esta práctica vamos a ver las animaciones que soporta Android en las
transacciones entre fragmentos.

1. Haz una copia de la práctica 2.7. En ella, mostrábamos en una activi-


dad, dos fragmentos dinámicos, uno con la lista de libros y otro con el
resumen del libro seleccionado. Este último fragmento lo sustituímos
bajo demanda.

2. Busca el código donde se hacía el cambio de fragmento. Estaba en la


actividad principal, en el método onLibroSeleccionado().
3. Una vez abierta la transacción entre fragmentos, congura la transición
(animación) a reproducir.

transaction = getSupportFragmentManager().beginTransaction();
transaction.setTransition(
FragmentTransaction.TRANSIT_FRAGMENT_*);
...
transaction.commit();

129
130 Capítulo 7. Animaciones

TRANSIT_FRAGMENT_NONE,
4. Los posibles valores para el parámetro son
TRANSIT_FRAGMENT_OPEN y TRANSIT_FRAGMENT_CLOSE. Prueba los tres.
Es preferible que utilices un dispositivo físico.

Práctica 7.2: Animaciones personalizadas entre fragmentos

Las posibilidades de las transiciones predeterminadas son muy escasas.


Afortunadamente, es posible seleccionar animaciones independientes para
cada uno de los dos fragmentos que entran en juego en la transacción.

1. Haz una copia de la práctica anterior.

2. En lugar de llamar al método setTransition() para poner una tran-


sición predenida, llama asetCustomAnimations(). Recibe dos pará-
metros, con la animación que quieres poner al fragmento entrante y al
saliente. Por ejemplo:

transaction.setCustomAnimations(android.R.anim.slide_in_left,
android.R.anim.slide_out_right);

3. Lanza la práctica y comprueba el efecto.

4. Si no lo estaba ya, pide a la transacción que se guarde en la pila de


vuelta:

transaction.addToBackStack(null);

5. Prueba la práctica de nuevo. Pulsa varios libros y pulsa volver. Al


deshacer, no se ejecutan animaciones.

6. setCustomAnimations tiene una segunda alternativa con dos paráme-


tros más para especicar la animación asignada al fragmento entrente
y saliente al pulsar el botón Volver. Congura las animaciones de
volver con las mismas animaciones:

transaction.setCustomAnimations(android.R.anim.slide_in_left,
android.R.anim.slide_out_right,
android.R.anim.slide_in_left,
android.R.anim.slide_out_right);

7. Comprueba el efecto. ¾Qué te parece? ¾Cambiarías algo?

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
131

Práctica 7.3: Animaciones tween: escalado

Las animaciones que acabamos de aplicar al fragmento son animaciones


tween (como versión abreviada de in-between ), que son animaciones inter-
poladas sobre vistas. Están soportadas desde la primera versión de Android
y, aunque en el nivel de API 11 fueron mejoradas por las animaciones de pro-
piedades, aún siguen siendo muy utilizadas. Se especican o bien por código
o, mejor, a través de un XML que almacenaremos en el directorio res/anim,
lo que permite especicar efectos visuales sencillos pero llamativos que re-
sultan fáciles de reutilizar para dinamizar nuestro interfaz de usuario.

Ésta será la primera de una secuencia de prácticas para experimentar


con estas animaciones. Puedes realizar todas sobre el mismo proyecto, o bien
ir haciendo copias del proyecto anterior e incluso cambiando el nombre del
paquete para poder tenerlas todas instaladas en el móvil simultáneamente.

1. Crea un proyecto nuevo y llamalo libro.azul.tween1 .

2. Mete en el directorio de recursos drawable una imagen cualquiera (por


ejemplo, un libro azul). Procura que la imagen que escojas se vea bien
en el dispositivo donde vayas a probarlo y que no sea simétrica ante
giros.

3. Modica el layout predenido para quitar la etiqueta introducida por


el asistente, y pon un ImageView que muestre la imagen en el centro
de la actividad. Además, pon en el layout raíz un evento onClick para
que se ejecute un método al pulsar sobre cualquier espacio vacío de la
actividad.

1
Si pretendes diferenciar cada práctica, puedes utilizar mejor el nombre
libro.azul.tweenscale.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
132 Capítulo 7. Animaciones

<RelativeLayout ...
android:onClick="onClickView">

<ImageView
android:id="@+id/imagen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/libroazul"
android:layout_centerInParent="true"/>

</RelativeLayout>

4. En el directorio res crea un nuevo directorio anim.

5. Crea un chero de recurso de animación tween.xml en él.

6. La plantilla crea un nodo raíz de tipo <set>. De momento seremos


menos ambiciosos y lo cambiaremos por <scale>. Modica el contenido
para que tenga una animación de escalado que empiece con escala 1
(tamaño original) y acabe con escala 0 para ver desvancerse al libro.

<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:toXScale="0.0"
android:toYScale="0.0"/>

7. Para ejecutar la animación, implementa el método llamado ante la pul-


sación en la actividad. Tendrás que buscar la vista del libro, y asociarle
la animación:

public void onClickView(View v) {


View imagen = findViewById(R.id.imagen);
Animation anim;
anim = AnimationUtils.loadAnimation(this, R.anim.tween);
imagen.startAnimation(anim);
}

8. Ejecuta la práctica. ¾Qué conclusiones sacas?

9. Añade como atributo android:fillAfter="true". ¾Para qué sirve?

10. Añade como atributo android:pivotX="50 %". ¾Para qué sirve?

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
133

Práctica 7.4: Animaciones tween: traslación

1. A partir de la práctica anterior (o de una copia), modica la animación


para hacer uso de translate en lugar de scale:

<translate xmlns:android="..."
android:duration="1000"
android:fillAfter="true"
android:fromXDelta="20%"
android:fromYDelta="20%"
android:toXDelta="100%"
android:toYDelta="0%"/>

2. Ejecuta la práctica. ¾Qué conclusiones sacas?

3. Modica la animación anterior, pon 0 % en los atributos from* y cam-


bia toXDelta por 50 %p. ¾Qué indica la p?
4. Modica el layout y sustituye el paddingRight por layout_marginRight.
Ejecuta la práctica. ¾Cómo explicas el resultado?

5. Deshaz el último cambio.

Práctica 7.5: Animaciones tween: rotación

1. A partir de la práctica anterior (o de una copia), modica la animación


para hacer uso de rotate en lugar de translate:

<rotate xmlns:android="..."
android:duration="1000"
android:fillAfter="true"
android:fromDegrees="0"
android:toDegrees="360"/>

2. Ejecuta la práctica. ¾Qué conclusiones sacas? ¾Cómo conseguirías que


rotara sobre sí mismo?

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
134 Capítulo 7. Animaciones

Práctica 7.6: Animaciones tween: alfa

1. A partir de la práctica anterior (o de una copia), modica la animación


para hacer uso de alpha:

<alpha xmlns:android="..."
android:duration="1000"
android:fillAfter="true"
android:fromAlpha="1"
android:toAlpha="0" />

2. Ejecuta la práctica.

Práctica 7.7: Animaciones tween: animaciones simultáneas

1. Utilizando como nodo raíz <set>, es posible especicar múltiples ani-


maciones que se ejecutarán todas a la vez. Modica la práctica anterior
(o una copia) para crear una animación que rote, y escale simultánea-
mente:

<set xmlns:android="..."
android:duration="1000"
android:fillAfter="true">
<scale
android:fromXScale="100%"
android:fromYScale="100%"
android:toXScale="0%"
android:toYScale="0%"
android:pivotX="50%"
android:pivotY="50%"/>
<rotate
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
135

android:pivotY="50%"/>
</set>

2. Ejecuta la práctica.

3. Observa que hemos puesto la duración de manera global en el set.


Quita la especicación de la duración de set, y pon una duración de
1 segundo al escalado y de medio segundo a la rotación y observa el
resultado.

4. Deshaz el último cambio.

5. Sustituye el escalado por una traslación:

<translate
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="100%"
android:toYDelta="0%"/>

6. Ejecuta la práctica. ¾Hace lo que esperabas? ¾Por qué? Modica la


animación para que realice el efecto que estabas esperando.

Práctica 7.8: Animaciones tween: animaciones en secuencia

Las animaciones tienen un atributo startOffset para indicar en qué


momento queremos que comience respecto al instante de inicio del bloque.
Si se utiliza, el <set> padre no deberá tener tiempo global.

1. Modica la práctica anterior (o una copia) para que el <set> no tenga


tiempo global.

2. Pon una duración de un segundo a la animación de traslación.

3. Pon una duración de medio segundo a la rotación, y startOffset de


500.

4. Ejecuta la práctica.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
136 Capítulo 7. Animaciones

Práctica 7.9: Animaciones tween: fillAfter y fillBefore

Con lo que sabes hasta ahora y usando como base la práctica anterior,
¾eres capaz de hacer una animación que haga desaparecer al libro (escala 0)
y luego volverlo a hacer aparecer (escala 1)?

<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="500"
android:fromXScale="1"
android:fromYScale="1"
android:toXScale="0"
android:toYScale="0"
android:fillEnabled="true"
android:fillAfter="false"/>
<scale
android:startOffset="500"
android:duration="500"
android:fromXScale="0"
android:fromYScale="0"
android:toXScale="1"
android:fillEnabled="true"
android:fillBefore="false"
android:toYScale="1"/>
</set>

Notas bibliográcas
http://developer.android.com/guide/topics/resources/animation-resource.
html
http://developer.android.com/guide/topics/graphics/view-animation.
html
http://developer.android.com/guide/topics/graphics/drawable-animation.
html

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Capítulo 8

Audio y grácos para juegos


Resumen: En este capítulo veremos cómo reproducir clips de au-
dio y música de fondo, y cómo implementar el bucle principal de un
videojuego para conseguir el repintado contínuo.

Práctica 8.1: Streams de salida de audio

Cuando en la práctica 4.3 reprodujimos los tonos de un teléfono al


marcar un número, quizá notaras que si cambiabas el volumen del móvil el
volumen de los pitidos no se veía afectado. Si revisas el código que usamos
en su momento era algo así:

tg = new ToneGenerator(AudioManager.STREAM_ALARM, 100);

Lo que hacíamos era asociar el generador de tonos creado al stream de


audio de la alarma. Sin embargo, el volumen de manera predenida está
asociado al sonido del timbre.

1. Crea una práctica nueva y lánzala.

2. Manipula el volúmen. Verás aparecer un indicador diciendo que se cam-


bia el volúmen del timbre, que es el que, por ejemplo, se ve afectado
cuando el móvil se silencia (se pone en vibración).

3. En el onCreate(), indica a Android que quieres que los botones del


volúmen manipulen el stream de la alarma usando el método de la
actividad (heredado de Context) encargado de ello.

setVolumeControlStream(AudioManager.STREAM_ALARM);

137
138 Capítulo 8. Audio y grácos para juegos

4. Lanza la aplicación y cambia el volumen. Observa que ahora Android


informa de un cambio de volumen diferente.

5. Cambialo de nuevo por STREAM_MUSIC y prueba una tercera vez.

Hay varios streams entre los que se puede elegir al emitir sonido: alarma,
tonos de marcado, música, noticaciones, llamada entrante, sistema, y con-
versación telefónica. Los juegos querrán utilizar el stream de música, por lo
que deberían congurarlo al arrancar.

Práctica 8.2: Efectos de sonido

Un juego tiene dos tipos de audio:

Música de fondo: lo habitual será una pista de audio de larga duración


que se reproduce a modo de banda sonora. Suele exigir reproducción de
un stream, leyendo el chero por trozos y reproduciendo en streaming.

Efectos de sonido: pequeños clips de audio que se reproducen en mo-


mentos especícos, como disparos, golpes o pasos. Para evitar latencia,
los clips deben estar cargados en memoria, por lo que tendrán que ser
cortos.

En Android, los efectos de sonido se reproducen a través de la clase


SoundPool. En tiempo de carga del nivel, se inicializa con los clips de audio
que esperamos reproducir en algún momento y que queremos tener previa-
mente cargados. Con él, es posible reproducir simultáneamente múltiples
efectos, encargándose de realizar la mezcla entre todos ellos. El número de
sonidos simultáneos se congura en el momento de la carga, y se puede pro-
porcionar prioridad a los diferentes efectos de manera que si se le solicita la
reproducción de más clips de los posibles, SoundPool se encargará de des-
cartar los menos prioritarios. La reproducción se puede poner en ciclo, y se
puede congurar la velocidad de reproducción de manera individual, de for-
ma que es posible por ejemplo reproducir un sonido en menos tiempo para
conseguir un tono más agudo. Además, es capaz de reproducir cheros com-
primidos, como .mp3 u .ogg con soporte de hardware, si existe, minimizando
el consumo de recursos.

1. Haz una copia de la práctica anterior y renómbralo a SoundPool.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
139

2. Descargate algún clip de audio corto del banco de imágenes y sonidos


1
del INTEF , o de cualquier otro de los almacenes públicos .
2

3. Crea en el proyecto un directorio para assets. Los assets son cheros


que se empaquetan en el APK, pero que no son controlados por el gestor
de recursos de Android, por lo que no se seleccionarán en función de
la conguración ni tendrán identicador dentro de la clase R.

4. Crea en ese directorio un subdirectorio sounds y mete en él el sonido


que hayas elegido.

5. Modica el layout para que cuando se pulse sobre cualquier parte se


ejecute el método onClick() de la actividad.

6. En la actividad, declara un atributo SoundPool que guardará el gestor,


y un entero que guardará el identicador del único sonido que vamos
a cargar.

SoundPool soundPool;
int soundId;

1. En el método onCreate(), tras la conguración del stream de salida de


audio que hicimos en la práctica anterior, crea el SoundPool y pídele
que cargue el clip de audio. Para eso, haz uso de AssetManager, el
gestor de los cheros empaquetados en el directorio assets/.

soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);


AssetManager assetManager = getAssets();
AssetFileDescriptor descriptor = null;
try {
descriptor = assetManager.openFd("sounds/disparo.ogg");
soundId = soundPool.load(descriptor, 1);
} catch (IOException e) {
e.printStackTrace();
}

2. En el onDestroy(), libera el SoundPool.

protected void onDestroy() {


super.onDestroy();
soundPool.release();
}

3. En el onClick(), reproduce el sonido que has cargado.

1
http://recursostic.educacion.es/bancoimagenes/web/
2
Por ejemplo http://soundbible.com/.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
140 Capítulo 8. Audio y grácos para juegos

soundPool.play(soundId, 1, 1, 0, 0, 1);

Los parámetros son el identicador del clip de audio, el volumen izquier-


do, volumen derecho, prioridad, número de repeticiones y velocidad de re-
producción.

4. Lanza la aplicación. Es preferible que uses un móvil físico, dado que


los AVDs no son demasiado ecientes. Pulsa sobre cualquier punto de
la actividad para reproducir el sonido.

La clase SoundPool tiene otros métodos que quizá te resulten de interés,


como descargar, pausar, detener o ajustar los parámetros de la reproducción
de un clip, pararlos todos o recibir una noticación cuando se ha terminado de
cargar todos los sonidos. Consulta la documentación para más información.

Quedan varias preguntas abiertas:

¾Qué métodos del ciclo de vida deberíamos utilizar?

¾Qué ocurre si tenemos muchos sonidos y la carga tarda mucho?

¾Cómo organizamos a nivel software para que sea fácil de gestionar?

Práctica 8.3: Música de fondo

Para la música de fondo no podemos utilizar SoundPool porque éste carga


en memoria completamente los clips de audio, y la música normalmente
ocupará demasiado.

Para reproducir música en streaming  se utiliza la clase MediaPlayer,


que carga el sonido sobre la marcha. Hay que tener en cuenta algunas con-
sideraciones:

Un objeto MediaPlayer sirve para reproducir una única pista de audio.

Para indicarle el origen de los datos, se requiere un FileDescriptor.


Podemos convertir un AssetFileDescriptor en un FileDescriptor,
pero necesitaremos entonces también el desplazamiento dentro de él y
su longitud.

Antes de poder reproducir la música, es necesario preparar el objeto


llamando a prepare().

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
141

Una vez preparado, se pueden utilizar los métodos start(), pause()


o stop(). Una llamada a este último exige una llamada posterior a
prepare() si se quiere volver a empezar.
Es posible establecer modo ciclo (usando setLooping()), controlar
el volumen (setVolume(izq, der)), consultar si se está reproducien-
do (isPlaying()) o recibir una noticación cuando deje de hacerlo
(setOnCompletionListener()). Algunos de los métodos son asíncro-
nos, de modo que por ejemplo stop() podría tener una ligera latencia.
Cuando termine de utilizarse, debería liberarse con release().

1. Haz una copia de la práctica anterior.

2. Busca, en algunas de las fuentes anteriores, una pista de audio que


pueda hacer las veces de música de fondo y guardala en assets/music/.
3. Crea un atributo nuevo MediaPlayer mediaPlayer;
4. En el método onCreate() asígnale un objeto nuevo. Obtén además el
descriptor al chero de audio, dáselo al MediaPlayer, llama a prepare()
y habilita la reproducción en ciclo.

protected void onCreate(Bundle savedInstanceState) {


...
mediaPlayer = new MediaPlayer();
try {
...
descriptor = assetManager.openFd("music/MusicaFondo.ogg");
mediaPlayer.setDataSource(descriptor.getFileDescriptor(),
descriptor.getStartOffset(),
descriptor.getLength());
mediaPlayer.prepare();
mediaPlayer.setLooping(true);
} catch (IOException e) {
e.printStackTrace();
}
...
}

5. En el onResume() lanza la reproducción de la música de fondo.

protected void onResume() {


super.onResume();
if (mediaPlayer != null)
mediaPlayer.start();
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
142 Capítulo 8. Audio y grácos para juegos

6. En el onPause() detén la reproducción. Utiliza pause() de modo que


en onResume() no tengamos que volver a llamar a prepare(). No libe-
raremos tantos recursos, pero aceleraremos la puesta en marcha poste-
rior.

protected void onPause() {


super.onPause();
if (mediaPlayer != null)
mediaPlayer.pause();
}

7. En onDestroy() detén y libera denitivamente el MediaPlayer.

protected void onDestroy() {


...
mediaPlayer.stop();
mediaPlayer.release();
}

8. Lanza la aplicación y comprueba la reproducción del audio. Date cuen-


ta de que si haces click, el clip de audio seguirá reproduciéndose, su-
perpuesto a la música.

Práctica 8.4: Renderizado continuo

En Android, la aparición de imágenes en la actividad puede realizarse


a través del uso de layouts y vistas, especialmente ImageView que permite
especicar un drawable como fuente de la representación.

Sin embargo, hacer uso de este mecanismo junto con, por ejemplo, el
sistema de animaciones del capítulo 8, ocasiona una sobrecarga excesiva.
Una alternativa mejor es implementar una subclase de la clase View, y usar-
la como único elemento del layout. Para el dibujado, sobreescribiremos el
método onDraw(), que es llamado cada vez que se necesita repintar, y ahí
aprovecharemos las alternativas de dibujado sobre canvas de Android.

1. Haz una copia de la práctica anterior y llámala rendercontinuo.

2. Implementa una clase interna que herede de View. Sobreescribe el méto-


do onDraw() y utiliza el Canvas para llenar todo el espacio de cualquier
color.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
143

class RenderView extends View {

public RenderView(Context context) {


super(context);
}

protected void onDraw(Canvas canvas) {


canvas.drawRGB(0, 255, 0);
}

} // class RenderView

3. Modica el método onCreate() de la actividad para no inar el layout,


y establecer directamente como vista de la actividad un objeto de la
nueva clase. El XML del layout puedes borrarlo.

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(new RenderView(this));
...
}

4. Lanza la aplicación. Comprueba que toda la ventana aparece de un


color plano. Pulsa sobre cualquier sitio para reproducir el clip de audio.
¾Qué ocurre?

5. Sobreescribe en la clase el método onTouchEvent(), haz que llame al


onClick() de la actividad y vuelve a probar.

@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
onClick(this);
return true;
}

Una de las grandes diferencias entre un videojuego y una aplicación de


escritorio normal es que un videojuego necesita repintarse continuamente.
El juego se mueve independientemente de las acciones del usuario. Para eso,
necesitamos forzar el repintado, de manera que Android planique una nueva
llamada a onDraw() de nuestra vista. Para verlo, vamos a hacer que nuestra
actividad cambie continuamente el color de fondo.

6. En la clase RenderView, dene tres atributos nuevos de tipo entero, r,


g, b que van a guardar el color de fondo que usaremos para pintarlo.
Asignales como valor inicial, por ejemplo 0, 64 y 128.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
144 Capítulo 8. Audio y grácos para juegos

7. Modca onDraw() para que en lugar de utilizar el color jo, se utilice


el guardado por los tres atributos.

8. Además, haz que en cada llamada se modiquen los tres valores, incre-
mentándose en un valor jo (por ejemplo, 29).

9. Fuerza el repintado llamando a invalidate().

int r = 0;
int g = 64;
int b = 128;

protected void onDraw(Canvas canvas) {


canvas.drawRGB(r, g, b);
r += 29; r %= 256;
g += 29; g %= 256;
b += 29; b %= 256;
invalidate();
}

10. Ejecuta la práctica y comprueba que la ventana cambia continuamente


de color.

11. Si quieres mostrar en logcat un indicador de la velocidad de actuali-


zación, declara un atributo que guarde el último instante en el que se
proporcionó el dato, el número de fotogramas que han pasado desde
entonces, y lleva la cuenta en el onDraw():

long last;
int cont = 0;

protected void onDraw(Canvas canvas) {


...
++cont;
long current = System.currentTimeMillis();
if (current - last > 2000) {
android.util.Log.i("RenderView", "FPS: " +
(float) cont*1000 / (current - last));
last = current;
cont = 0;
}
}

Una vez que tenemos un modo de pintar continuamente, podemos añadir


no sólo el pintado, sino también la actualización del estado del juego para
hacerle avanzar.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
145

En cualquier caso, el problema de este mecanismo es que todo el juego


se ejecutará en la hebra principal de la aplicación, y ésta podría tener otras
cosas que hacer. Además, el repintado se realiza continuamente. Si ejecutas
la aplicación y esperas a que se suspenda el móvil (o cambias de aplicación,
sin cerrarla), notarás que la música cesa, pero en el logcat sigue apareciendo
la información de velocidad de refresco.

Práctica 8.5: SurfaceView: renderizado continuo en otra he-


bra

Para mejorar el mecanismo anterior, es preferible delegar el dibujado y,


en general, el bucle del juego, a una hebra secundaria. El esquema a alto nivel
es:

Creamos una hebra que será la encargada de ejecutar el bucle principal


del juego.

La hebra es lanzada desde el onResume() y se solicita su nalización


desde onPause().

En el interior de la hebra, hay un bucle que no termina hasta que no se


solicite externamente (desde onPause(). Para marcar la nalización,
se debe hacer uso de un atributo volátil.

En el onPause() de la actividad, se espera hasta que se conrme que la


hebra ha terminado. Eso bloqueará temporalmente la hebra principal,
pero evitará problemas de concurrencia si la hebra tarda en cerrarse y
se vuelve a llamar a onResume().

Todo el juego tiene lugar en esa hebra: análisis de la entrada, ejecución


de la lógica y pintado. La dicultad surge porque, como sabemos, el pintado
no se puede hacer desde una hebra secundaria.
SurfaceView llega en nuestra ayuda. Esta clase envuelve a una supercie
(Surface) dentro de una View, que puede ser colocada en una actividad.
Una supercie es una abstracción de un buer crudo que es utilizado por
el Surface Manager para componer la representación nal en pantalla. Para
poder dibujar en la supercie, es necesario obtener un SurfaceHolder, que
es el que realmente nos da acceso a la supercie de la SurfaceView. Con él,
podemos obtener un Canvas sobre el que pintar, por lo que el mecanismo
último para dibujar elementos es similar al del modelo anterior. Cuando
hayamos terminado de pintar, le solicitamos al SurfaceHolder que sincronice

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
146 Capítulo 8. Audio y grácos para juegos

el canvas con la supercie visible en primer plano, y él se encargará de hacerlo


en la hebra principal.

Por abreviar, normalmente se programa una subclase de SurfaceView


que es también un Runnable y contiene la hebra en su interior.

1. Haz una copia de la práctica anterior y renómbrala a surfaceview.

2. Adapta la clase RenderView para que sea una subclase de SurfaceView,


Runnable y siga el esquema explicado.
implemente el interfaz

class RenderView extends SurfaceView implements Runnable {


...

public RenderView(Context context) {


super(context);
holder = getHolder(); // Nuevo
last = System.currentTimeMillis();
}

[ ... rgb, onDraw, onTouchEvent ... ]

//-------------------------------------------

Thread renderThread = null;


SurfaceHolder holder;
volatile boolean running = false;

public void resume() {


running = true;
renderThread = new Thread(this);
renderThread.start();
}

public void run() {

while (!holder.getSurface().isValid())
;

while(running) {
Canvas canvas = holder.lockCanvas();
myDraw(canvas);
holder.unlockCanvasAndPost(canvas);
}
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
147

public void pause() {


running = false;
while(true) {
try {
renderThread.join();
break;
} catch(InterruptedException ie) {
// Nada especial. Reintentamos.
}
}
}
} // class RenderView

3. Declara un atributo RenderView en la actividad, y modica el método


onCreate() para que lo cree. Antes no conservábamos una referen-
cia al objeto, pero ahora la necesitamos porque contiene la hebra que
querremos lanzar o detener.

4. En los métodos onResume() y onPause() llama a los métodos resume()


y pause() de renderView.

RenderView renderView;

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
renderView = new RenderView(this);
setContentView(renderView);
...
}

protected void onResume() {


super.onResume();
renderView.resume();
...
}

@Override
protected void onPause() {
super.onPause();
renderView.pause();
...
}

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
148 Capítulo 8. Audio y grácos para juegos

Práctica 8.6: Una entidad

La riqueza interactiva de los juegos la proporcionan las entidades. Una


entidad es cualquier objeto dinámico dentro del juego, como un jugador, un
item, o un enemigo.
Hay diferentes arquitecturas para implementar las entidades. La más clá-
sica es utilizar herencia. Se crea una clase abstracta que dene los métodos
que toda entidad debe tener, y en el bucle principal se tiene una lista con
todas las entidades del nivel y se actualizan convenientemente.

1. Crea una clase Entidad. Puedes hacerla en un chero Java indepen-


diente.

abstract public class Entidad {

abstract public void tick(long delta);

abstract public void draw(Canvas c);

protected float x;

protected float y;

2. Declara en la clase de la actividad un nuevo atributo que guarde una


lista de entidades.

List<Entidad> entidades = new ArrayList<Entidad>();

3. En el método run de la hebra principal (en RenderView), añade código


para llamar al tick() de todas las entidades existente. Tendrás que
llevar la pista de cuánto tiempo ha pasado, en milisegundos, y enviarlo
como parámetro.

public void run() {


long last = System.nanoTime();
while(running) {
long current = System.nanoTime();
long delta = current - last;
delta /= 1000000; // A milisegundos

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
149

for (Entidad e: entidades)


e.tick(delta);
last = current;

...
}
}

4. En el método de dibujado, después de haber repintado el fondo, pide


a todas las entidades que se pinten.

protected void onDraw(Canvas canvas) {


canvas.drawRGB(0, 0, 0);
for (Entidad e: entidades)
e.draw(canvas);

...
}

5. Crea un tipo cualquiera de entidad que en el tick() cambie su estado


de alguna forma, y en el draw() se pinte en su posición.

public class Pacman extends Entidad {

public Pacman(float posX, float posY, float vel) {

apertura = 0;
x = posX;
y = posY;

incApertura = vel;

circlePaint = new Paint();


circlePaint.setAntiAlias(true);
circlePaint.setColor(Color.YELLOW);

circlePaint.setStyle(Paint.Style.FILL);

@Override
public void tick(long delta) {

apertura += incApertura * (float)delta;

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
150 Capítulo 8. Audio y grácos para juegos

if (apertura > 45) {


apertura = 90 - apertura; // 45 - (apertura - 45)
incApertura = -incApertura;
}
else if (apertura < 0) {
apertura = -apertura;
incApertura = -incApertura;
}

} // tick

@Override
public void draw(Canvas canvas) {

RectF rectF = new RectF(x, y, x+100, y+100);


canvas.drawArc(rectF, apertura, 360 - 2*apertura,
true, circlePaint);

} // draw

protected float apertura;

protected float incApertura;

protected Paint circlePaint;

6. En el onCreate() de la actividad, añade un par de entidades que sean


distinguibles cuando se representen.

entidades.add(new Pacman(10, 10, 0.2f));


entidades.add(new Pacman(200, 200, 0.3f));

7. Ejecuta la práctica y comprueba el resultado.

Ten en cuenta que este ejemplo es solo una prueba de concepto. Para que
esto pueda escalar y convertirse en un juego se debería replantear completa-
mente la arquitectura. Por ejemplo, algunas cuestiones:

La fusión de la hebra del juego y la SurfaceView es cómodo inicial-


mente por ser más rápido de programar, pero a la larga compensa
mantenerlos separados.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
151

Es necesaria la existencia de gestores de recursos. Se ha intuído en la


parte de los clips de audio con los efectos, y es necesario en muchos
otros lugares, como imágenes.

No nos hemos preocupado de las diferentes conguraciones de pantalla


(aspect ratio y densidad de píxeles. En el ejemplo, las entidades se están
pintando usando píxeles como unidad, por lo que su tamaño relativo
respecto a la pantalla variará enormemente según el dispositivo.

En su estado actual, la cuenta de los FPS la llevamos en el método de


dibujado, pero el sitio lógico sería el bucle principal del juego.

Práctica 8.7: Detalles nales

1. Para evitar que aparezca la Action bar, que no la vamos a usar, haz
que la clase herede directamente de Activity en lugar de usar la clase
de la librería de soporte.

2. Para saltar a pantalla completa, solicita al gestor que no muestre el


título.

requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);

3. Si lo ejecutas en un móvil físico y no haces nada, el móvil terminará


suspendiéndose. Si bien es un comportamiento deseable la mayor parte
del tiempo, en un juego el usuario podría estar viendo el contenido de
la pantalla sin querer tocar nada y no querrá que el móvil se suspenda.
Usando el gestor de energía, podemos crear un bloqueo. En el método
onCreate() crea el objeto del bloqueo y guárdalo en un atributo nuevo.
En onResume(), adquiere el bloqueo, y en onPause() liberalo:

PowerManager.WakeLock wakeLock;

protected void onCreate(Bundle savedInstanceState) {

...
PowerManager powerManager;
powerManager = (PowerManager)

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
152 Capítulo 8. Audio y grácos para juegos

getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(
PowerManager.FULL_WAKE_LOCK, "MyLock");
...

protected void onResume() {


...
wakeLock.acquire();
}

@Override
protected void onPause() {
...
wakeLock.release();
}

4. El bloqueo fuerza a que la pantalla esté a máximo brillo y que la CPU


no pierda rendimiento. Dado que esta conguración consume mucha
batería, es necesario un permiso en el chero de maniesto:

<uses-permission android:name="android.permission.WAKE_LOCK"/>

5. Para evitar tener que plantear el juego tanto en horizontal como en ver-
tical, decide qué orientación preeres y fuérzala. Eso además evitará
que se destruya la actividad si se gira el móvil, simplicando buena par-
<activity>,
te de la gestión. En el chero de maniesto, en el elemento
añade un atributo nuevo android:screenOrientation="landscape"
(o portrait).

Notas bibliográcas
http://developer.android.com/reference/android/media/SoundPool.
html
http://developer.android.com/reference/android/media/MediaPlayer.
html
http://developer.android.com/guide/topics/graphics/2d-graphics.
html

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
Notas bibliográcas 153

https://developer.android.com/training/scheduling/wakelock.
html
Beginning Android Games, Mario Zechner, Apress.

IFC02CM15 - Programación avanzada de dispositivos móviles - Julio 2015


Pedro Pablo Gómez Martín
¾Qué te parece desto, Sancho?  Dijo Don Quijote 
Bien podrán los encantadores quitarme la ventura,
pero el esfuerzo y el ánimo, será imposible.

Segunda parte del Ingenioso Caballero


Don Quijote de la Mancha
Miguel de Cervantes

Y adiós, que ya viene el alba.


Y dando a sus mulas, no
atendió a más preguntas.

Segunda parte del Ingenioso Caballero


Don Quijote de la Mancha
Miguel de Cervantes

También podría gustarte