Uso del Acelerómetro en Android Studio | Jetpack Compose

Video thumbnail

En esta entrada veremos unos de los componentes fundamentales que podemos encontrar en cualquier dispositivo Android relativamente reciente y se trata de un sensor que permite medir la aceleración del dispositivo: el acelerómetro.

Anteriormente vimos como usar el MediaPlayer para reproducir audios en Android Studio.

El acelerómetro en Android se mide en base a los tres ejes conocidos (X,Y y Z); cada uno de ellos puede ser accedido a través de la clase SensorManager.

Método Moderno con Jetpack Compose

Con la llegada de Jetpack Compose, la forma de construir interfaces de usuario en Android ha cambiado significativamente. Aunque la lógica para acceder al sensor del acelerómetro es la misma (usando SensorManager), la forma en que gestionamos el estado y actualizamos la UI es diferente.

Ya no es necesario implementar SensorEventListener directamente en una Activity. En su lugar, podemos crear un composable que se encargue de registrar y escuchar los cambios del sensor.

Vamos a conocer cómo podemos utilizar el acelerómetro, que sirve para conocer la disposición del dispositivo (entiéndase la orientación). Es muy utilizado, por ejemplo, en juegos de billar para meter la pelota en el agujero.

Para comenzar, vamos a crear otro Composable. Como hemos hecho en el resto de los ejercicios, lo colocaremos en nuestro onCreate dentro del Scaffold, así que no hay ningún misterio con esto:

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyProyectAndroidTheme {
                Scaffold(modifier = Modifier.fillMaxSize(),

                ) { innerPadding ->
                    Column() {
                        Greeting(
                            name = "Android",
                            modifier = Modifier.padding(innerPadding)
                        )
                        SensorDataScreen()
                    }
                }
            }
        }

    }
}

Configuración inicial y variables

Empezaremos con la implementación obteniendo el contexto para acceder a la información de la actividad. Declararemos tres variables principales; el acelerómetro nos da la posición en el eje 3D (X, Y y Z), por lo que definimos una para cada uno para poder visualizarlas al rotar el equipo.

Posteriormente, necesitamos el SensorManager. Este es el elemento global para acceder a todos los sensores del dispositivo (luminosidad, humedad, etc.). Específicamente, declararemos otra variable para el acelerómetro haciendo el casteo correspondiente:

@Composable
fun SensorDataScreen() {
    val context = LocalContext.current
    // Estados para guardar los ejes X, Y, Z
    var x by remember { mutableFloatStateOf(0f) }
    var y by remember { mutableFloatStateOf(0f) }
    var z by remember { mutableFloatStateOf(0f) }
}

Implementación del Listener y Reactividad

Para detectar los movimientos, declaramos un SensorEventListener. Este es un listener genérico de sensores, por lo que dentro de la función onSensorChanged debemos usar un condicional para preguntar si el sensor que cambió es el de tipo acelerómetro.

Cuando el evento detecta un cambio, obtenemos los valores X, Y y Z y actualizamos nuestras variables de tipo MutableState. Estas son variables reactivas (como sucede en Flutter) que harán que toda la interfaz se recargue automáticamente al cambiar sus valores.

@Composable
fun SensorDataScreen() {
    // Estados para guardar los ejes X, Y, Z
    ***

    // 2. Gestionamos el ciclo de vida del sensor
    DisposableEffect(Unit) {
        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                if (event?.sensor?.type == Sensor.TYPE_ACCELEROMETER) {
                    // Actualizamos los estados con los valores del sensor
                    x = event.values[0]
                    y = event.values[1]
                    z = event.values[2]
                }
            }

            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
        }

        // Registramos el sensor al iniciar
        sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_UI)

        // IMPORTANTE: Al salir, liberamos el sensor para ahorrar batería
        onDispose {
            sensorManager.unregisterListener(listener)
        }
    }

}

Gestión de recursos con DisposableEffect

Es fundamental utilizar DisposableEffect para limpiar recursos y evitar fugas de memoria (memory leaks). Cuando la actividad ya no esté en uso, debemos liberar el acelerómetro mediante un unregisterListener. Si no liberamos el recurso, el desempeño de la aplicación se verá afectado.

Niveles de Precisión

Al registrar el listener, podemos elegir distintos niveles de precisión (y consumo de batería):

  • SENSOR_DELAY_NORMAL: Lento, ideal para ahorro de energía.
  • SENSOR_DELAY_UI: Ideal para interfaces de usuario.
  • SENSOR_DELAY_GAME: Diseñado para juegos.
  • SENSOR_DELAY_FASTEST: El más rápido posible, pero con mayor consumo.

Creación de la Interfaz y el “Truco Visual”

En la parte visual, armamos una Column con un texto que muestra los valores de los ejes y un Box que simula la pelota. Usamos el modificador offset para cambiar su posición en X e Y basándonos en los datos del acelerómetro. Le damos un tamaño de 50, color rojo y forma circular.

Para probar esto en el emulador, puedes ir a los tres puntos, entrar en Virtual Sensors, buscar Device Pose y variar la orientación manualmente:

@Composable
fun SensorDataScreen() {
    val context = LocalContext.current
    // Estados para guardar los ejes X, Y, Z
    ***

    // 2. Gestionamos el ciclo de vida del sensor
    ***

    // 3. UI para mostrar los datos
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Acelerómetro", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(20.dp))
        Text(text = "Eje X: ${"%.2f".format(x)}")
        Text(text = "Eje Y: ${"%.2f".format(y)}")
        Text(text = "Eje Z: ${"%.2f".format(z)}")

        // Un pequeño truco visual: una caja que se mueve según el sensor
        Box(
            Modifier
                .offset(x = (-x * 10).dp, y = (y * 10).dp)
                .size(50.dp)
                .background(Color.Red, CircleShape)
        )
    }
}

Optimización con Remember

¿Por qué usamos remember? Compose, al igual que Flutter, redibuja la pantalla muchas veces por segundo. Si declaramos las variables directamente sin el remember, el sistema las redeclararía 30 o 50 veces por segundo, lo cual es ineficiente.

Remember le dice a Compose que guarde el valor y evite redeclarar la variable en cada actualización.

Esto es especialmente importante al pedir acceso al hardware (sensores), ya que es un proceso costoso:

// 1. Obtenemos el SensorManager y el acelerometro (faltaria verificar si NO es null, dispositivos baratos pueden NO traer acelerometro
//    val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
//    val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

    // mas eficiente, para evitar que cada vez que se recarga la pagina se cree el sensor
    val sensorManager = remember {
        context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    }
    val accelerometer = remember {
        sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    }

Consideración Final

Ten en cuenta que en dispositivos económicos el acelerómetro puede no existir, lo que devolvería un valor null. En proyectos reales, siempre debes verificar que el sensor sea distinto de null antes de intentar registrarlo para evitar errores en la aplicación.

val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

Código completo:

@Composable
fun SensorDataScreen() {
    val context = LocalContext.current
    // Estados para guardar los ejes X, Y, Z
    var x by remember { mutableFloatStateOf(0f) }
    var y by remember { mutableFloatStateOf(0f) }
    var z by remember { mutableFloatStateOf(0f) }

    // 1. Obtenemos el SensorManager y el acelerometro (faltaria verificar si NO es null, dispositivos baratos pueden NO traer acelerometro
//    val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
//    val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

    // mas eficiente, para evitar que cada vez que se recarga la pagina se cree el sensor
    val sensorManager = remember {
        context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    }
    val accelerometer = remember {
        sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    }

    // 2. Gestionamos el ciclo de vida del sensor
    DisposableEffect(Unit) {
        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                if (event?.sensor?.type == Sensor.TYPE_ACCELEROMETER) {
                    // Actualizamos los estados con los valores del sensor
                    x = event.values[0]
                    y = event.values[1]
                    z = event.values[2]
                }
            }

            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
        }

        // Registramos el sensor al iniciar
        sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_UI)

        // IMPORTANTE: Al salir, liberamos el sensor para ahorrar batería
        onDispose {
            sensorManager.unregisterListener(listener)
        }
    }

    // 3. UI para mostrar los datos
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Acelerómetro", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(20.dp))
        Text(text = "Eje X: ${"%.2f".format(x)}")
        Text(text = "Eje Y: ${"%.2f".format(y)}")
        Text(text = "Eje Z: ${"%.2f".format(z)}")

        // Un pequeño truco visual: una caja que se mueve según el sensor
        Box(
            Modifier
                .offset(x = (-x * 10).dp, y = (y * 10).dp)
                .size(50.dp)
                .background(Color.Red, CircleShape)
        )
    }
}

Este enfoque es más declarativo y se integra mejor con el ciclo de vida de los componentes de Compose, representando la forma moderna de trabajar con sensores en Android.

¿Cómo probarlo?

Dispositivo Real: Es lo mejor. Solo mueve el móvil y verás los números cambiar.

Emulador: En los tres puntos del emulador (...), ve a la sección Virtual Sensors. Ahí puedes mover un teléfono virtual con el ratón y el emulador enviará esos datos a tu código.

Acelerómetro

Empezando con la clase de SensorManager en Android (Método Legacy)

Debemos de implementar la clase de SensorEventListener en nuestra actividad:

public class MainActivity extends Activity implements SensorEventListener

Una vez que implementamos la interfaz anterior debemos de sobrecargar los métodos:

@Override public void onSensorChanged(SensorEvent event) { } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { }

Solicitando acceso al acelerómetro

Lo siguiente que debemos hacer es solicitar acceso al acelerómetro del dispositivo; y esto lo hacemos a través del objeto SensorManager de la siguiente manera:

SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

Obtenemos el servicio del sistema a través del objeto getSystemService pasando como parámetro el nombre del servicio al que queremos acceder.

Obteniendo el acelerómetro del sistema

De la lista de sensores de vuelta en la consulta anterior, verificamos si hay algun sensor de tipo acelerómetro disponible; para ello se emplea la siguiente línea de código:

sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER).size()

Registrando el evento "Escuchador" o Listener del acelerómetro

Una vez que hayamos verificado que el dispositivo cuenta con al menos un sensor de tipo acelerómetro podemos registrar el evento listener o escuchador:

sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_FASTEST)

Pasamos como parámetro una instancia de la actividad, el acelerómetro y la tasa de refrescamiento, la cual puede ser:

  • SENSOR_DELAY_FASTEST
  • SENSOR_DELAY_NORMAL
  • SENSOR_DELAY_GAME
  • SENSOR_DELAY_UI

Lo único que varía entre ellas es la tasa de refrescamiento del acelerómetro.

Registrando el evento "Escuchador" o Listener

El método onSensorChanged se activa cada vez que el sistema detecta un cambio en la aceleración del dispositivo:

@Override public void onSensorChanged(SensorEvent event) { sensor = ""; sensor = "x: " + event.values[0] + " y: " + event.values[1] + " z: " + event.values[2]; }

Con el parámetro SensorEvent podemos tener entre varios datos, las coordenadas que el sistema usa en las rotaciones a través de los ejes X,Y y Z como vimos en el código anterior.

Captura de pantalla de la aplicación:

Acelerómetro en AnDroid

Siguiente paso, cómo incrustar un video de Youtube en Android

Aprende a detectar la orientación del dispositivo en los ejes X, Y y Z para mover objetos en tiempo real. Incluye manejo de SensorManager, estados reactivos con mutableStateOf y optimización con remember.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english