Activitats

Bitàcola de rutes

L’objectiu d’aquesta activitat és desenvolupar una senzilla aplicació que enregistri la posició en la qual es troba l’usuari quan aquest faci una fotografia. Amb això es disposarà d’una bitàcola de la ruta que va seguint, per exemple, per recordar pas per pas una excursió per la muntanya, amb les posicions i les fotos corresponents.

A la figura següent es mostra una captura de l’aplicació en funcionament, quan ja ha registrat uns quants punts de ruta. Lògicament, les fotos són les que fa l’emulador i les coordenades també s’han enviat a l’emulador; amb un dispositiu real i una bona ruta de muntanya segur que queda molt millor.

Figura Aspecte de l’aplicació de bitàcola de rutes

Podeu descarregar a continuació el codi corresponent a aquesta activitat :

Com que es tracta d’una aplicació relativament complexa, s’ha de desenvolupar pas per pas, comprovant cada cop que l’aplicació funciona correctament abans de passar al següent pas.

En primer lloc es prepararà l’aplicació per tal que pugui mostrar un ListView amb les fotos i les coordenades. Tot seguit es rebran les coordenades GPS, i finalment s’afegirà el necessari per fer la foto i afegir-la a la vista.

Preparar la llista de visualització de la ruta

Aquesta aplicació anirà afegint els punts de ruta a una llista (ListView) que inclourà les coordenades GPS i la foto (en miniatura) de cada punt. Cal preparar l’aplicació perquè mostri aquesta llista, però també cal definir el format de cada element de la llista, és a dir, com es veurà cada punt de la ruta a la llista.

La interfície de l’aplicació és molt senzilla, només és necessari afegir un ListView al layout que hi ha per defecte:

  1. <RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" >
  5.  
  6. <ListView
  7. android:id="@+id/llistaRuta"
  8. android:layout_width="match_parent"
  9. android:layout_height="wrap_content"
  10. android:layout_alignParentRight="true"
  11. android:layout_alignParentTop="true" >
  12.  
  13. </ListView>
  14.  
  15. </RelativeLayout>

Per tal de definir l’aspecte de cada punt de la ruta a la llista, s’ha de crear un fitxer de layout especial per definir aquests elements. Aneu a la carpeta layout del vostre projecte i feu clic amb el botó dret, seleccioneu New/XML/Layout XML File i doneu-li el nom de layout_llista i de Root Tag LinearLayout, tal com es veu a la figura següent.

Figura Creació d’un layout per als elements de la llista

Amb aquesta operació es genera un nou fitxer de layout amb un element LinearLayout que contindrà la descripció d’un element de la llista, en aquest cas les coordenades GPS i la imatge que s’hagi capturat a cada punt de la ruta.

Aquest fitxer defineix una imatge al costat de dos textos, un per a la latitud i un altre per a la longitud, combinant-los amb un parell de LinearLayout. La imatge té una mida fixa per tal que la llista quedi més endreçada. Editeu el fitxer per tal que quedi com aquest:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="horizontal">
  6.  
  7. <ImageView
  8. android:id="@+id/foto"
  9. android:contentDescription="@string/hello_world"
  10. android:layout_width="100dp"
  11. android:layout_height="100dp"
  12. android:paddingTop="3dp"
  13. android:paddingRight="3dp"
  14. android:paddingBottom="3dp" />
  15.  
  16. <LinearLayout
  17. android:layout_width="match_parent"
  18. android:layout_height="match_parent"
  19. android:orientation="vertical"
  20. android:paddingTop="20dp" >
  21.  
  22. <TextView
  23. android:id="@+id/latitud"
  24. android:layout_width="wrap_content"
  25. android:layout_height="wrap_content"
  26. android:textSize="15dp" />
  27.  
  28. <TextView
  29. android:id="@+id/longitud"
  30. android:layout_width="wrap_content"
  31. android:layout_height="wrap_content"
  32. android:textSize="15dp" />
  33. </LinearLayout>
  34. </LinearLayout>

És molt important que respecteu els noms dels elements d’aquest fitxer, perquè es faran servir per omplir la llista amb cada punt de la ruta, i si no coincideixen es produirà un error.

Inicialització de l’aplicació

Per tal de coordinar els diferents mètodes, aquesta aplicació requereix que es defineixin un conjunt d’objectes comuns al cos de la classe MainActivity, és a dir, dins de la classe però abans de començar onCreate(). Aquests objectes són: APP_CAMERA, identificador per recollir la informació a l'onActivityResult; identificadorImatge, URI de la imatge que s’ha fet; llistaPunts, una llista on s’emmagatzema la informació de tots els punts de ruta; adaptador, que converteix la informació al ListView, un element que conté les dades del darrer punt visitat, i lat i lon, que guardaran el darrer canvi de posició:

  1. public class MainActivity extends ActionBarActivity {
  2.  
  3. // Número que identifica l'activitat de l'aplicació de fotos
  4. private static final int APP_CAMERA = 0;
  5.  
  6. // Identificador de la imatge que crearà l'aplicació de fotos
  7. private Uri identificadorImatge;
  8.  
  9. // ArrayList amb els elements que es veuran a la llista
  10. List<HashMap<String, String>> llistaPunts = new ArrayList<HashMap<String, String>>();
  11.  
  12. // Adaptador que treu els continuts de llistaPunts pel ListView
  13. SimpleAdapter adaptador = null;
  14.  
  15. // Informació de cada punt de la ruta
  16. HashMap<String, String> element = null;
  17.  
  18. double lat, lon;

D’altra banda, al mètode onCreate() hi ha unes quantes coses a fer: inicialitzem les variables de latitud i longitud a 0. Com que la ListView associa un nom (“foto”, “latitud” i “longitud”) amb un camp de cada punt de ruta, cal desar la informació dels punts de ruta associada a aquests noms (arrays clausOrigen i vistesDesti). També s’ha d’inicialitzar l’adaptador vist abans i associar-lo a la ListView de l’aplicació. Finalment, s’ha d’engegar la recepció del senyal GPS. El mètode, doncs, quedarà així:

  1. @Override
  2. public void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_main);
  5.  
  6. lat = lon = 0.0;
  7.  
  8. // Claus de cada camp que es vol visualitzar
  9. String[] clausOrigen = {"foto", "latitud", "longitud"};
  10.  
  11. // Identificadors dels elements de layout_llista corresponents per visualitzar-los
  12. int[] vistesDesti = {R.id.foto, R.id.latitud, R.id.longitud};
  13.  
  14. // Adaptador que agafa els valors de llistaPunts i els mostra per la ListView
  15. adaptador = new SimpleAdapter(getBaseContext(), llistaPunts, R.layout.layout_llista, clausOrigen, vistesDesti);
  16.  
  17. // Connectar la ListView amb l'adaptador que s'ha creat
  18. ListView listViewRuta = (ListView) findViewById(R.id.llistaRuta);
  19. listViewRuta.setAdapter(adaptador);
  20.  
  21. // Engegar la recepció GPS
  22. LocationManager gestorLoc =
  23. (LocationManager) getSystemService(Context.LOCATION_SERVICE);
  24. gestorLoc.requestLocationUpdates(LocationManager.GPS_PROVIDER, 60*1000, 50, this);
  25. }

Afegiu els imports necessaris en cas que l’Android Studio no ho hagi fet.

Recepció de coordenades GPS

En primer lloc, cal donar-li permís a l’aplicació perquè pugui llegir les coordenades GPS i també perquè pugui guardar les fotografies a la memòria externa. Aneu al fitxer AndroidManifest.xml i afegiu els següents permisos abans de la línia on comença application:

  1. <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Per fer que aquesta aplicació sigui capaç de rebre les coordenades GPS, el més senzill és fer que l’activitat principal implementi la interfície LocationListener: editeu la capçalera de l’activitat per tal que quedi:

  1. public class MainActivity extends ActionBarActivity implements LocationListener {

Una vegada hem afegit l'implements ens marca un error al nom de l’activitat. Per què? Doncs perquè falten els mètodes corresponents a LocationListener. Si us poseu a sobre del nom de l’activitat i seleccioneu l’opció que us dóna (Implement methods), Android Studio els afegirà automàticament. Són els següents:

  1. @Override
  2. public void onLocationChanged(Location arg0) {
  3.  
  4. }
  5.  
  6. @Override
  7. public void onProviderDisabled(String arg0) {
  8.  
  9. }
  10.  
  11. @Override
  12. public void onProviderEnabled(String arg0) {
  13.  
  14. }
  15.  
  16. @Override
  17. public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
  18.  
  19. }

El mètode onLocationChanged() és el més important en aquest exemple, perquè és on es reben les coordenades GPS.

Els paràmetres de la crida anterior a requestLocationUpdates() (a l'onCreate) indiquen que es desitja rebre una nova posició GPS cada minut (60 segons * 1000 mil·lisegons/segon) i només si el dispositiu s’ha mogut com a mínim 50 metres. El this del final indica que aquesta mateixa activitat rebrà les coordenades.

Nota: per provar el programa potser us interessa rebre noves posicions cada segon, per no haver d’esperar un minut per afegir una nova posició. Simplement canvieu el 60*1000 per un 1000.

Tot seguit heu d’editar el mètode onLocationChanged() per tal que rebi les coordenades GPS i les guardi a l’espera de que l’usuari faci una fotografia:

  1. @Override
  2. public void onLocationChanged(Location loc) {
  3.  
  4. // Guardem la posició actual
  5. lat = loc.getLatitude();
  6. lon = loc.getLongitude();
  7.  
  8. }

Captura de fotos

L’usuari haurà de disposar d’un botó per llençar l’activitat de càmera per fer les fotografies. Podem crear un botó al layout o bé crear-ne un a l’ActionBar. Com que per defecte ens crea un menú per accedir a la configuració de l’aplicació i no la necessitem, l’adaptarem per crear aquest botó.

Anem al fitxer menu_main.xml que trobareu a dins d’/app/res/menu i que modificarem per deixar-lo de la següent manera:

  1. <menu xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:app="http://schemas.android.com/apk/res-auto"
  3. xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
  4. <item android:id="@+id/action_newFoto" android:title="@string/action_newFoto"
  5. android:orderInCategory="100" app:showAsAction="always" />
  6. </menu>

La propietat showsAsAction ens permet dir quan i com es mostrarà un element a l’ActionBar: ifRoom, si hi ha lloc mostrarà la icona; withText, amb el text acompanyant la icona; never, mai; always, sempre i collapseActionView, com a desplegable.

Hem modificat l'id i el title de l’etiqueta item, així com també la propietat showsAsAction que hem posat a always.

Aquest canvi implica modificar el fitxer strings.xml (que està a /app/res/values) per afegir el text corresponent a @string/action_newFoto:

  1. <resources>
  2. <string name="app_name">BitacolaRutes</string>
  3.  
  4. <string name="action_newFoto">Nova Foto</string>
  5. </resources>

Ja sols faltaria afegir el comportament que tindrà el botó, això ho farem al mètode onOptionsItemSelected on comprovarem si el botó és el que hem definit i llavors executarem el codi per llençar l’activitat de càmera. El codi quedarà així:

  1. @Override
  2. public boolean onOptionsItemSelected(MenuItem item) {
  3.  
  4. switch (item.getItemId()) {
  5.  
  6. case R.id.action_newFoto:
  7. // Es crea l'intent per l'aplicació de fotos
  8. Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
  9.  
  10. //Crea el directori bitàcola en cas de no existir
  11. File directori = new File(Environment.getExternalStorageDirectory().toString() + "/bitacola/");
  12. if (!directori.exists()) {
  13. directori.mkdir();
  14. }
  15. // Genera un nom únic per la imatge
  16. File imatge = new File(directori.getAbsolutePath(), UUID.randomUUID().toString() + "-foto.jpg");
  17. intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(imatge));
  18.  
  19. // Es guarda l'identificador de la imatge per recuperar-la quan estigui feta
  20. identificadorImatge = Uri.fromFile(imatge);
  21. // S'engega l'activitat
  22. startActivityForResult(intent, APP_CAMERA);
  23. break;
  24.  
  25. }
  26.  
  27. return super.onOptionsItemSelected(item);
  28. }

Si l’element del menú seleccionat és R.id.action_newFoto: definirem l'intent per capturar una fotografia, crearem si no existeix el directori “bitacola” a l’arrel de l’emmagatzemament extern i definirem un fitxer amb nom únic d’imatge, què serà passat com a extra de l'intent i a la vegada serà guardat a la variable identificadorImatge.

L'intent serà llençat esperant una resposta (startActivityForResult) i controlarem la recepció de dades fent servir la variable APP_CAMERA.

La nostra aplicació ha de crear una nova entrada del ListView cada vegada que es fa una fotografia així que, al mètode onActivityResult a banda de guardar la imatge, haurem de crear un nou element que constarà d’una miniatura de la fotografia i la darrera posició rebuda pel dispositiu (latitud i longitud).

Per tal de reduir els problemes de memòria que es puguin produir a causa d’imatges amb resolucions molt grans, s’ha afegit el codi que redimensionarà la imatge una vegada guardada. Per aconseguir-ho farem una crida a createScaledBitmap i definirem una amplada màxima de 1080px.

Queda només recollir la informació de la fotografia a través del mètode onActivityResult: si el requestCode és el de la variable APP_CAMERA, haurem d’obtenir el bitmap generat per la càmera amb bitmap = android.provider.MediaStore.Images.Media.getBitmap(contRes, identificadorImatge);, el comprimirem i generarem l’element del ListView amb el codi:

  1. // Anotar la posició i l'URL de la imatge a l'element del punt de ruta
  2. element = new HashMap<String, String>();
  3. element.put("latitud", "Latitud = " + lat);
  4. element.put("longitud", "Longitud = " + lon);
  5. element.put("foto", identificadorImatge.toString());
  6. llistaPunts.add(element);
  7.  
  8. // S'ha de dir a l'adaptador que els continguts de la llista han canviat
  9. adaptador.notifyDataSetChanged();

Cada element del ListView tindrà la latitud, la longitud i la ruta cap a la imatge que hem generat. L’afegim a la llista de punts i notifiquem a l’adaptador que la informació ha canviat perquè torni a dibuixar el ListView.

A continuació teniu el codi complet del mètode onActivityResult:

  1. @Override
  2. public void onActivityResult(int requestCode, int resultCode, Intent data) {
  3. // Primer cridem al mètode d'Activity perquè faci la seva tasca
  4. super.onActivityResult(requestCode, resultCode, data);
  5. switch (requestCode) {
  6. case APP_CAMERA:
  7. if (resultCode == Activity.RESULT_OK) {
  8. // El ContentResolver dóna accés als continguts
  9. // (la imatge emmagatzemada en aquest cas)
  10. ContentResolver contRes = getContentResolver();
  11. // Cal dir-li que el contingut del fitxer ha canviat
  12. contRes.notifyChange(identificadorImatge, null);
  13.  
  14. Bitmap bitmap;
  15. // Com que la càrrega de la imatge pot fallar, cal tractar
  16. // les possibles excepcions
  17. try {
  18.  
  19. // Obtenim el bitmap per reduïr-lo posteriorment per evitar problemes de memòria
  20. bitmap = android.provider.MediaStore.Images.Media
  21. .getBitmap(contRes, identificadorImatge);
  22.  
  23. // Reduim el bitmap a un màxim de 1080 píxels d'amplada i mantenim les proporcions.
  24. int alt = (int) (bitmap.getHeight() * 1080 / bitmap.getWidth());
  25. Bitmap reduit = Bitmap.createScaledBitmap(bitmap, 1080, alt, true);
  26.  
  27. // Guardem el bitmap comprimit en la mateixa ruta
  28. FileOutputStream out = new FileOutputStream(identificadorImatge.getPath());
  29. reduit.compress(Bitmap.CompressFormat.JPEG, 100, out);
  30.  
  31. // Anotar la posició i l'URL de la imatge a l'element del punt de ruta
  32. element = new HashMap<String, String>();
  33. element.put("latitud", "Latitud = " + lat);
  34. element.put("longitud", "Longitud = " + lon);
  35. element.put("foto", identificadorImatge.toString());
  36. llistaPunts.add(element);
  37.  
  38. // S'ha de dir a l'adaptador que els continguts de la llista han canviat
  39. adaptador.notifyDataSetChanged();
  40.  
  41. } catch (Exception e) {
  42. Toast.makeText(this, "No es pot carregar la imatge" +
  43. identificadorImatge.toString(),
  44. Toast.LENGTH_SHORT).show();
  45. }
  46. }
  47. }
  48. }

Anar a la pàgina anterior:
Objectes multimèdia
Anar a la pàgina següent:
Exercicis