Documente Academic
Documente Profesional
Documente Cultură
de dispositivos móviles
IFC02CM15
Cuadernillo de prácticas
Julio, 2015
Documento maquetado con TEXiS v.1.0.
Cuadernillo de prácticas
Julio, 2015
Copyright
c Pedro Pablo Gómez Martín
Índice
1. Listas 1
Pr. 1.1. Hola mundo . . . . . . . . . . . . . . . . . . . . . . . . . 1
Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2. Fragments 27
Pr. 2.1. Dos actividades: detalles de los libros . . . . . . . . . . . 27
Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
v
vi Índice
Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4. Servicios 75
Pr. 4.1. Servicio básico . . . . . . . . . . . . . . . . . . . . . . . . 75
Notas bibliográcas . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
5. Bluetooth 87
Pr. 5.1. Activar bluetooth . . . . . . . . . . . . . . . . . . . . . . 87
7. Animaciones 129
Pr. 7.1. Animaciones básicas entre fragmentos . . . . . . . . . . . 129
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.
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" />
<Button ...
android:onClick="onPulsame" />
View v = toast.getView();
TextView tv = (TextView) v.findViewById(android.R.id.message);
tv.setTextColor(Color.RED);
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.
<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.
8. Muestra el toast.
toast.setView(v);
toast.show();
}
2. Lanza la aplicación.
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.
<ListView android:id="@+id/listView"
android:layout_width="math_parent"
android:layout_height="math_parent" />
</RelativeLayout>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</TextView>
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.
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.
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
@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.
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"));
MiArrayAdapter aa;
aa = new MiArrayAdapter(this, libros);
ListView lv = (ListView) findViewById(R.id.listView);
lv.setAdapter(aa);
} // onCreate
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
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.
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
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.
[...]
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;
}
});
[...]
[...]
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;
}
});
[...]
aa.notifyDataSetChanged();
aa.remove(l);
<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"
/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/porAutor"
android:onClick="onPorAutor"
/>
</LinearLayout>
</RelativeLayout>
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
lv.setEmptyView(findViewById(R.id.<id>));
<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>
<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"/>
@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>();
// [ .... ]
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
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).
5. Modica la clase Libro para que guarde una tercera cadena con el
supuesto resumen del libro.
class Libro {
27
28 Capítulo 2. Fragments
resumen = r;
}
} // class Libro
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"/>
<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>
lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Intent intent = new Intent(getApplicationContext(),
ResumenLibroActivity.class);
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(...):
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.
@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() {
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);
}
//---------------------------------------------
} // 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.
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).
return inflater.inflate(R.layout.fragment_resumen_libro,
container, false);
}
tv = (TextView) getView().findViewById(id);
<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"
....>
</RelativeLayout>
<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>
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.
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);
}
});
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
// 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.
<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
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>
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.
@Override
public void onLibroSeleccionado(ListaLibrosFragment.Libro l) {
}
lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
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;
}
@Override
public void onLibroSeleccionado(Libro l) {
ResumenLibroFragment rlf;
rlf = (ResumenLibroFragment) getSupportFragmentManager().
findFragmentById(R.id.resumenFragment);
rlf.setLibro(l.titulo, l.autor, l.resumen);
1. Modica el layout del fragmento de los detalles para que el nodo raíz
sea un FrameLayout que tenga dentro al LinearLayout actual.
<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">
</LinearLayout>
</FrameLayout>
String resumen) {
setVisibility(R.id.tvLibroNoSeleccionado, View.GONE);
setVisibility(R.id.panelLibro, View.VISIBLE);
setLabel(R.id.tituloLibro, titulo);
...
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.
@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
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_resumen);
ResumenLibroFragment rlf;
rlf = (ResumenLibroFragment) getSupportFragmentManager().
findFragmentById(R.id.resumenFragment);
} // onCreate
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.
2. Crea una nueva clase Java y llámala SpyFragment. Haz que herede de
android.support.v4.app.Fragment.
public SpyFragment() {
android.util.Log.i(TAG, "<constructor>");
}
@Override
public void onAttach(Activity activity) {
android.util.Log.i(TAG, "onAttach(" + activity + ")");
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();
@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.
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.
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:
// ...
if ((rlf != null) && rlf.isInLayout())
rlf.setLibro(l.titulo, l.autor, l.resumen);
else {
...
}
}
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
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.
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.
transaction.addToBackStack(null);
if (savedInstanceState == null) {
ListaLibrosFragment ...
...
}
} // onCreate
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:
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(
R.layout.fragment_sinlibroseleccionado,
container, false);
}
} // SinLibroSeleccionadoFragment
if (savedInstanceState == null) {
ListaLibrosFragment llf = new ListaLibrosFragment();
SinLibroSeleccionadoFragment slsf =
new SinLibroSeleccionadoFragment();
FragmentTransaction transaction;
transaction = getSupportFragmentManager().beginTransaction();
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.
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();
public ResumenLibroFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
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;
} // setLabel
TextView tv;
tv = (TextView) v.findViewById(id);
if (str != null)
tv.setText(str);
else
tv.setText("");
} // setLabel
} // ResumenLibroFragment
1. Ejecuta el programa.
...
} // setLibro
setArguments(new Bundle());
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
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.
61
62 Capítulo 3. Hebras y tareas asíncronas
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>
Thread t;
t = new Thread(new Runnable() {
@Override
public void run() {
while(true);
}
});
t.start();
} // onPulsame
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:
<LinearLayout
...
android:orientation="vertical">
<TextView android:id="@+id/tvResultado"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
Los dos métodos más importantes de la clase, que habrá que sobreescribir,
son:
Como puedes ver, AsyncTask es una clase genérica, con tres argumentos,
uno de los cuales aún no hemos visto:
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.
@Override
protected Long doInBackground(Void... params) {
@Override
protected void onPostExecute(Long resultado) {
} // CalcularPrimos
new CalcularPrimos().execute();
} // onPulsame
6. Prueba la aplicación.
3. Cambia el código del evento del ratón para que cancele la tarea si se
pulsó una segunda vez:
CalcularPrimos cp;
if (cp == null) {
cp = new CalcularPrimos();
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.
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);
}
<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"/>
4. Añade como atributo una ProgressBar que nos evite tener que bus-
carla en la vista continuamente.
@Override
protected void onPreExecute() {
pb = (ProgressBar) findViewById(R.id.pbCalcularPrimos);
pb.setVisibility(View.VISIBLE);
}
[ ... ]
@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
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.
75
76 Capítulo 4. Servicios
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()");
}
} // ServicioBasico
6. Lanza la aplicación.
8. Cierra la aplicación.
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.
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.
<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>
<service
android:name=".DialerService"
android:exported="false" >
</service>
public DialerService() {
super("DialerService");
tg = new ToneGenerator(AudioManager.STREAM_ALARM, 100);
}
@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.
@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) {}
}
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.
break;
default:
// 0..9
sonido = ToneGenerator.TONE_DTMF_0 + key.charAt(0) - '0';
}
intent.putExtra(SONIDO, sonido);
context.startService(intent);
}
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.
<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>
et.append(intent.getStringExtra(TECLA));
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
...
IntentFilter filtro = new IntentFilter(getClass().getName());
LocalBroadcastManager.getInstance(this).
registerReceiver(handler, filtro);
}
// Avisamos a la actividad.
MainActivity.addKey(this, intent.getStringExtra(TECLA));
[ ... ]
}
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
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.
87
88 Capítulo 5. Bluetooth
<manifest>
...
<uses-permission android:name="android.permission.BLUETOOTH"/>
</manifest>
@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);
}
}
}
@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);
}
}
}
@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
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
@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:
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
segundos.
<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>
else {
registerReceiver(bluetoothMonitor,
new IntentFilter(
BluetoothAdapter.ACTION_STATE_CHANGED));
registerReceiver(bluetoothMonitor,
new IntentFilter(
BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
updateBluetoothUI(bluetooth.getState());
}
...
}
@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
...
switch(newState) {
...
} // switch
...
findViewById(R.id.panelBTOn).setVisibility(View.INVISIBLE);
} // initBluetoothUI
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"/>
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.
BluetoothServerSocket _serverSocket;
while (!isInterrupted()) {
try {
_serverSocket =
bluetooth.listenUsingRfcommWithServiceRecord(
"LibroAzul", SERVICE_UUID);
} catch (IOException e) {
escribe("ERROR: no pude obtener el server socket");
break;
}
} // run
// Detenemos la hebra
public void cancel() {
try {
interrupt();
if (_serverSocket != null)
_serverSocket.close();
} catch (IOException e) { }
}
}
ServerThread serverThread;
if (serverThread != null)
serverThread.cancel();
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;
}
} // ClientThread
if (clientThread != null)
clientThread.cancel();
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
<uses-permission
105
106 Capítulo 6. Conexión por red
android:name="android.permission.ACCESS_NETWORK_STATE" />
<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>
setLabel(labelId, str);
}
ConnectivityManager cm = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
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);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkMonitor);
}
import java.net.*;
import java.io.*;
import java.util.Scanner;
try {
ServerSocket ss = new ServerSocket(SERVER_PORT);
while(true) {
System.out.println("Esperando un cliente...");
Socket cliente = ss.accept();
<RelativeLayout ...>
<TextView
android:id="@+id/resultado"
android:text="@string/hebraDetenida"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/estadoRed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
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:
TextView tv;
@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
} // ClienteSumas
ClienteSumas _cliente;
@Override
protected void onStart() {
super.onStart();
IntentFilter filter = new IntentFilter(
ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(networkMonitor, filter);
}
@Override
protected void onStop() {
super.onStop();
unregisterReceiver(networkMonitor);
detenerHebraCliente();
}
ConnectivityManager cm = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
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);
}
13. Usando telnet conéctate al puerto 9000 del antrión y hazle una con-
sulta. Comprueba que responde correctamente.
<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.
@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();
}
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
@Override
protected Void doInBackground(String... params) {
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
}
} // ObtenerBicis
ObtenerBicis _asyncTask;
}
}
...
_asyncTask = null;
}
...
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();
}
1. Crea una nueva clase (en un chero separado) que guarde la informa-
ción de cada localización.
class Station {
2. Crea una nueva clase que guare una lista de localizaciones y el tiempo
Unix de la última actualización.
class ViabicingInfo {
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.
try {
} // parse
//------------------------------------------------------------
}
return result;
} // readStations
//-----------------------------------------------------------
} // readStation
//--------------------------------------------------------
String result;
if (parser.next() == XmlPullParser.TEXT) {
result = parser.getText();
parser.nextTag();
return result;
}
else
return "";
} // readString
//---------------------------------------------------------
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
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;
}
<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"/>
<TextView android:id="@+id/estadoRed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
_asyncTask = null;
}
Procesar la cancelación.
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
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.
En esta práctica vamos a ver las animaciones que soporta Android en las
transacciones entre fragmentos.
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.
transaction.setCustomAnimations(android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
transaction.addToBackStack(null);
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);
1
Si pretendes diferenciar cada práctica, puedes utilizar mejor el nombre
libro.azul.tweenscale.
<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>
<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"/>
<translate xmlns:android="..."
android:duration="1000"
android:fillAfter="true"
android:fromXDelta="20%"
android:fromYDelta="20%"
android:toXDelta="100%"
android:toYDelta="0%"/>
<rotate xmlns:android="..."
android:duration="1000"
android:fillAfter="true"
android:fromDegrees="0"
android:toDegrees="360"/>
<alpha xmlns:android="..."
android:duration="1000"
android:fillAfter="true"
android:fromAlpha="1"
android:toAlpha="0" />
2. Ejecuta la práctica.
<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%"
android:pivotY="50%"/>
</set>
2. Ejecuta la práctica.
<translate
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="100%"
android:toYDelta="0%"/>
4. Ejecuta la práctica.
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
setVolumeControlStream(AudioManager.STREAM_ALARM);
137
138 Capítulo 8. Audio y grácos para juegos
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.
SoundPool soundPool;
int soundId;
1
http://recursostic.educacion.es/bancoimagenes/web/
2
Por ejemplo http://soundbible.com/.
soundPool.play(soundId, 1, 1, 0, 0, 1);
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.
} // class RenderView
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
onClick(this);
return true;
}
8. Además, haz que en cada llamada se modiquen los tres valores, incre-
mentándose en un valor jo (por ejemplo, 29).
int r = 0;
int g = 64;
int b = 128;
long last;
int cont = 0;
//-------------------------------------------
while (!holder.getSurface().isValid())
;
while(running) {
Canvas canvas = holder.lockCanvas();
myDraw(canvas);
holder.unlockCanvasAndPost(canvas);
}
}
RenderView renderView;
@Override
protected void onPause() {
super.onPause();
renderView.pause();
...
}
protected float x;
protected float y;
...
}
}
...
}
apertura = 0;
x = posX;
y = posY;
incApertura = vel;
circlePaint.setStyle(Paint.Style.FILL);
@Override
public void tick(long delta) {
} // tick
@Override
public void draw(Canvas canvas) {
} // draw
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:
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.
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
PowerManager.WakeLock wakeLock;
...
PowerManager powerManager;
powerManager = (PowerManager)
getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(
PowerManager.FULL_WAKE_LOCK, "MyLock");
...
@Override
protected void onPause() {
...
wakeLock.release();
}
<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
https://developer.android.com/training/scheduling/wakelock.
html
Beginning Android Games, Mario Zechner, Apress.