Activitats

Moviment amb sensors

L’objectiu d’aquesta activitat és el de practicar l’ús dels sensors en una aplicació bàsica. La nostra aplicació ens ha de permetre moure una bola de pinball per la pantalla fent ús dels sensors i permetent-nos canviar la velocitat arrossegant el dit per la pantalla.

Inicialment, la bola tindrà una velocitat de 2.0f que es multiplicarà al valor obtingut pel sensor en qualsevol de les direccions. Si arrosseguem el dit cap a dalt augmentarem la velocitat en 0.5f fins un límit de 5.0f i si arrastrem el dit cap a baix el contrari, disminuirem en 0.5f fins a 0.5f.

L’aplicació ha de funcionar en landscape i no ha de tenir ActionBar (poseu el tema Theme.AppCompat.Light.NoActionBar o qualsevol sense ActionBar).

Figura Exemple de l’aplicació en funcionament

Per tal de realitzar l’aplicació, cal que us descarregueu els recursos que trobareu a la secció “Recursos activitat ‘Moviment amb sensors’” dels annexos.

Una vegada creat el projecte, afegirem el fitxer bola.png (que es troba als recursos de l’aplicació) al directori drawable. El primer que farem és definir el layout així que editarem el fitxer activity_main.xml per tal d’afegir un ImageView amb la imatge definida anteriorment.

El contingut del layout serà el següent:

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
  3. android:layout_height="match_parent" tools:context=".MainActivity">
  4.  
  5. <ImageView
  6. android:layout_width="wrap_content"
  7. android:layout_height="wrap_content"
  8. android:id="@+id/imageView"
  9. android:layout_alignParentTop="true"
  10. android:layout_alignParentLeft="true"
  11. android:layout_alignParentStart="true"
  12. android:src="@drawable/bola" />
  13. </RelativeLayout>

Per forçar que l’activitat funcioni en horitzontal i que no variï quan l’usuari gira el mòbil, ho podem fer editant el fitxer AndroidManifest.xml. Haurem d’afegir la propietat screenOrientation de l’etiqueta activity i assignar-li el valor landscape:

  1. android:screenOrientation="landscape"

El fitxer quedarà de la següent manera:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3. package="ioc.xtec.cat.u4a1_movement">
  4.  
  5. <application
  6. android:allowBackup="true"
  7. android:icon="@mipmap/ic_launcher"
  8. android:label="@string/app_name"
  9. android:theme="@style/AppTheme">
  10. <activity
  11. android:name=".MainActivity"
  12. android:label="@string/app_name"
  13. android:screenOrientation="landscape">
  14. <intent-filter>
  15. <action android:name="android.intent.action.MAIN" />
  16.  
  17. <category android:name="android.intent.category.LAUNCHER" />
  18.  
  19. </intent-filter>
  20. </activity>
  21. </application>
  22.  
  23. </manifest>

Una altra de les modificacions que haurem de fer és la de treure l’ActionBar, per fer-ho haurem de canviar el tema de l’aplicació. Editem el fitxer /app/res/values/styles.xml i el modifiquem perquè quedi de la següent manera:

  1. <resources>
  2.  
  3. <!-- Base application theme. -->
  4. <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
  5. <!-- Customize your theme here. -->
  6. </style>
  7.  
  8. </resources>

Ja podem centrar-nos en la funcionalitat de l’aplicació, obrim el fitxer MainActivity i el primer que haurem de fer és preparar la classe per monitorar els canvis a l’acceleròmetre. Per fer això, necessitem implementar la classe SensorEventListener modificant la línia de definició de la classe. Aprofitarem a més per definir les variables globals necessàries, per controlar el monitoratge del sensor i per inicialitzar els components al mètode onCreate. Així, el codi parcial de la classe serà el següent:

  1. public class MainActivity extends ActionBarActivity implements SensorEventListener {
  2.  
  3. SensorManager sensorMgr;
  4. Sensor sensor;
  5. ImageView img;
  6.  
  7. // Control de la velocitat
  8. float velocitat = 2.0f;
  9. float iniciX, iniciY;
  10.  
  11. // Mides per fer càlculs
  12. int statusBar, width, height;
  13.  
  14. @Override
  15. protected void onCreate(Bundle savedInstanceState) {
  16. super.onCreate(savedInstanceState);
  17. setContentView(R.layout.activity_main);
  18.  
  19. // La bola
  20. img = (ImageView) findViewById(R.id.imageView);
  21.  
  22. // Obtenim les dimensions de la pantalla
  23. DisplayMetrics display = this.getBaseContext().getResources().getDisplayMetrics();
  24. width = display.widthPixels;
  25. height = display.heightPixels;
  26.  
  27.  
  28. // Mida de l'statusBar per calcular l'alçada de l'aplicació
  29. statusBar = getResources().getDimensionPixelSize(getResources().getIdentifier("status_bar_height", "dimen", "android"));
  30.  
  31. // Inicialitzem a 0 les variables per controlar les pulsacions tàctils
  32. iniciX = iniciY = 0;
  33.  
  34. // Inicialitzem el sensor de l'acceleròmetre
  35. sensorMgr = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
  36. sensor = sensorMgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  37. sensorMgr.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME);
  38. }
  39.  
  40. @Override
  41. protected void onPause() {
  42. super.onPause();
  43. sensorMgr.unregisterListener(this);
  44. }
  45.  
  46. @Override
  47. protected void onResume() {
  48. super.onResume();
  49. sensorMgr.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME);
  50. }
  51. ...
  52. }

La velocitat de la bola dependrà de les prestacions del telèfon i de la memòria disponible (podeu trobar lag durant el funcionament de l’aplicació en alguns telèfons de baixes prestacions); s’hauria de controlar el framerate per tal que fos el mateix per a tots els telèfons.

Haurem d’afegir els mètodes onSensorChanged(SensorEvent sensorEvent) i onAccuracyChanged(Sensor sensor, int i) tal com ens indica l’Android Studio. Deixarem buit el mètode onAccuracyChanged i des d'onSensorChanged farem una crida al mètode moveBall(float x, float y) que s’encarregarà de moure la bola per la pantalla i comprovar quan arriba a les vores. El contingut del mètode onSensorChanged serà el següent:

  1. @Override
  2. public void onSensorChanged(SensorEvent sensorEvent) {
  3.  
  4. if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
  5.  
  6. moveBall(sensorEvent.values[1], sensorEvent.values[0]);
  7.  
  8. }
  9.  
  10. }

Abans de res comprovarem que el sensor sigui l’adequat i enviarem les coordenades al mètode moveBall, si us fixeu, en la crida al mètode estem passant la coordenada Y (sensorEvent.values[1]) del sensor a la variable que hem anomenat X i a la inversa, la X (sensorEvent.values[0]) l’hem passat a la variable Y. Això a causa que si mirem el mòbil en portrait, l’eix X és l’horitzontal però quan passem el mòbil a landscape passa a ser el vertical; com que per convenció l’eix X és l’horitzontal l’anomenarem d’aquesta manera al mètode tot i que realment estigui rebent el valor de l’eix Y.

Al mètode moveBall haurem de processar la informació que ens arriba dels eixos horitzontal i vertical. Per l’horitzontal haurem de comprovar que el desplaçament (valor del sensor multiplicat per la velocitat) més l’amplada de la imatge no sobresurti dels límits de la pantalla (del valor 0 fins el que conté la variable width). Per fer els càlculs hem de tenir present que la coordenada [0,0] de la pantalla que es troba a la part superior esquerra i que la posició de la imatge farà referència a la part superior esquerra d’aquesta:

Figura

Així, la gestió de l’eix X quedarà de la següent manera:

  1. private void moveBall(float x, float y) {
  2.  
  3. float novaPosicioX = img.getX() + x * velocitat;
  4. // Coordenada x
  5.  
  6. // Si el moviment és cap a la dreta
  7. if (x > 0) {
  8.  
  9. // Comprovem que no surti de les dimensions de la pantalla en assignar la nova posició
  10. if (novaPosicioX + img.getWidth() <= width) {
  11. img.setX(novaPosicioX);
  12.  
  13. }
  14. // Si en surt, establim la posició màxima en horitzontal perquè es pugui veure la imatge.
  15. else img.setX(width - img.getWidth());
  16. }
  17.  
  18. // Fem el mateix pel moviment cap a l'esquerra
  19. else {
  20. if (novaPosicioX >= 0) {
  21. img.setX(novaPosicioX);
  22.  
  23. } else img.setX(0);
  24.  
  25. }
  26. }

Si l’analitzem, el primer que fem és comprovar si el valor del sensor és positiu o negatiu, és a dir, si la pilota s’ha de desplaçar cap a la dreta o cap a l’esquerra de la pantalla.

En cas de ser positiu:

  • Si la nova posició més l’ample de la imatge no supera el valor màxim de l’eix horitzontal (emmagatzemat a la variable width) canviarem la posició de la imatge al valor calculat.
  • Si la nova posició més l’ample de la imatge supera l’ample de la pantalla, posarem com a posició el valor màxim que pot prendre, és a dir, el valor que conté la variable width menys l’ample de la imatge.

En cas de ser negatiu:

  • Si la nova posició és major o igual a 0 s’actualitzarà la posició de la imatge.
  • Si la nova posició és menor que 0 li assignarem 0.

En el cas de l’eix vertical el procediment és el mateix que a l’horitzontal amb la diferència que en aquest cas hem de tenir en compte l’alçada en píxels de l’statusbar (que hem guardat prèviament a la variable statusBar). Com que la posició 0 de l’activitat es troba a la cantonada esquerra superior, just sota de l’statusbar, només farem servir l’alçada en píxels de la barra quan calculem quin és el valor màxim vertical de la pantalla: Serà l’alçada de la pantalla menys l’alçada de l’statusbar.

Així, afegint aquesta informació, la gestió del moviment vertical quedarà de la següent manera:

  1. // Coordenada y
  2.  
  3. float novaPosicioY = img.getY() + y * velocitat;
  4.  
  5. // El concepte és el mateix que a la X però hem de tenir en compte la barra d'estat
  6.  
  7. if (y > 0)
  8. if (novaPosicioY + img.getHeight() + statusBar <= height) {
  9. img.setY(novaPosicioY);
  10.  
  11. } else img.setY(height - img.getHeight() - statusBar);
  12. else {
  13. if (novaPosicioY >= 0) {
  14. img.setY(novaPosicioY);
  15.  
  16. } else img.setY(0);
  17.  
  18. }

Si executem l’aplicació podrem observar com la bola es mou seguint la inclinació del dispositiu, només ens faltaria gestionar el canvi de velocitat. Per fer-ho haurem de sobreescriure la funció public boolean onTouchEvent(MotionEvent event) i controlar si l’usuari arrossega el dit en l’eix vertical: si ho fa cap a dalt augmentarà la velocitat i si l’arrossega cap a baix la disminuirà.

El codi serà el següent:

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3.  
  4. if (event.getAction() == MotionEvent.ACTION_DOWN) {
  5.  
  6. // Registrem l'event al TextView
  7. iniciX = event.getX();
  8. iniciY = event.getY();
  9.  
  10.  
  11. } else if (event.getAction() == MotionEvent.ACTION_UP) {
  12.  
  13. // Registrem l'event al TextView
  14. float finalX = event.getX();
  15. float finalY = event.getY();
  16.  
  17. //Comrpovem si el moviment ha estat vertical
  18. if (Math.abs(finalX - iniciX) < Math.abs(finalY - iniciY)) {
  19.  
  20. // Establim el límit inferior en 0.5f
  21. if (finalY > iniciY) {
  22. if (velocitat > 0.5)
  23. velocitat -= 0.5f;
  24. // Establim el límit superior en 5.0f
  25. } else {
  26. if (velocitat < 5.0)
  27. velocitat += 0.5f;
  28. }
  29. Toast.makeText(this, "Velocitat: " + String.valueOf(velocitat), Toast.LENGTH_SHORT).show();
  30.  
  31. }
  32. }
  33. return super.onTouchEvent(event);
  34. }

I ja tindrem completada la nostra aplicació. Torneu-la a executar i proveu a pujar i baixar la velocitat per comprovar que tot funciona correctament. La velocitat de desplaçament de la bola dependrà del dispositiu ja que no hem tingut en compte el temps que passa entre les crides a moveBall (telèfons més ràpids faran més crides que telèfons més lents).

Podeu descarregar la solució des de l’apartat “Activitat ‘Moviment amb sensors’” dels annexos.

Anar a la pàgina anterior:
Programació de jocs en Android
Anar a la pàgina següent:
Exercicis d'autoavaluació