Spirit Level with Accelerometer | Sensors in Jetpack Compose | Kotlin | Android Studio

In this guide, we will create an Image Spirit Level View in Jetpack Compose using the accelerometer sensor. This feature simulates the behavior of a spirit level (a tool used to measure the tilt of a surface) and provides visual feedback for device orientation through dynamic animations.

Let’s break the code step by step for better understanding.

Step 1: Setting Up the Composable

The SpiritLevelView composable is the main entry point where the spirit level UI and logic are defined.

@Composable
fun SpiritLevelView() {
    val context = LocalContext.current
    val sensorManager = context.getSystemService(SENSOR_SERVICE) as SensorManager
    val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

    var xTilt by remember { mutableFloatStateOf(0f) }
    var yTilt by remember { mutableFloatStateOf(0f) }
}

Explanation:

  1. SensorManager:
    • Access the device’s SensorManager to interact with hardware sensors.
    • Retrieve the accelerometer sensor, which provides real-time tilt information.
  2. Mutable States:
    • xTilt and yTilt are used to store the tilt values on the horizontal and vertical axes, respectively. These values are dynamically updated as the device moves.

Step 2: Handling Sensor Events

We use a SensorEventListener to capture accelerometer data and update the tilt values.

val sensorEventListener = object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
        event?.let {
            xTilt = -event.values[0]
            yTilt = -event.values[1]
        }
    }

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

Explanation:

  • onSensorChanged:
    • Called whenever the sensor detects a change in tilt.
    • The accelerometer provides values in the event.values array:
      • event.values[0]: Horizontal tilt (X-axis).
      • event.values[1]: Vertical tilt (Y-axis).
    • The tilts are reversed (-event.values) to align with the physical directions.

Step 3: Registering and Unregistering the Listener

To ensure efficient resource management, the listener is registered when the composable is created and unregistered when it’s disposed.

LaunchedEffect(Unit) {
    sensorManager.registerListener(
        sensorEventListener,
        accelerometer,
        SensorManager.SENSOR_DELAY_GAME
    )
}

DisposableEffect(Unit) {
    onDispose {
        sensorManager.unregisterListener(sensorEventListener)
    }
}    

Explanation:

  1. LaunchedEffect:
    • Registers the accelerometer listener when the composable is first launched.
    • Uses SENSOR_DELAY_GAME for a faster response to sensor data, ideal for real-time animations.
  2. DisposableEffect:
    • Ensures the listener is unregistered when the composable is removed from the UI, preventing memory leaks.

Step 4: Designing the Layout

The layout consists of two spirit levels: one vertical and one horizontal. Each level is implemented using the SpiritLevelBar composable.

Box(
   modifier = Modifier
                .fillMaxSize()
                .background(Color.White),
   contentAlignment = Alignment.Center
) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.SpaceBetween,
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp)
    ) {
        SpiritLevelBar(
            modifier = Modifier
                .weight(1f)
                .width(50.dp),
            tilt = yTilt,
            isVertical = true
        )

        Spacer(modifier = Modifier.height(32.dp))

        SpiritLevelBar(
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp),
            tilt = xTilt,
            isVertical = false
        )
    }
}    

Explanation:

  1. Outer Box:
    • Centers the entire UI within the screen.
    • Uses Column to stack the vertical and horizontal bars with spacing in between.
  2. SpiritLevelBar:
    • Handles the tilt visualization for each bar.
    • Accepts parameters like tilt (tilt value) and isVertical (orientation).

Step 5: Creating the Spirit Level Bar

The SpiritLevelBar composable is responsible for:

  1. Drawing the rectangular spirit level bar.
  2. Animating a ball inside the bar to indicate the tilt.
@Composable
fun SpiritLevelBar(modifier: Modifier, tilt: Float, isVertical: Boolean) {
    val ballSize = 40.dp
    val maxTilt = 4.5f // Maximum tilt angle.
    val offsetAnim = remember { Animatable(0f) }

    BoxWithConstraints(
        modifier = modifier
            .background(Color.LightGray.copy(0.4f), RoundedCornerShape(20.dp))
            .padding(2.dp)
    ) {
        val barSize = if (isVertical) maxHeight.value else maxWidth.value
        val maxOffset = barSize / 2f - ballSize.value / 2f

        val targetOffset = (tilt / maxTilt).coerceIn(-1f, 1f) * maxOffset

        LaunchedEffect(targetOffset) {
            offsetAnim.animateTo(
                targetValue = targetOffset,
                animationSpec = tween(durationMillis = 100)
            )
        }

        Canvas(
            modifier = Modifier
                .align(Alignment.Center)
                .size(ballSize)
                .offset(
                    x = if (!isVertical) offsetAnim.value.dp else 0.dp,
                    y = if (isVertical) -offsetAnim.value.dp else 0.dp
                )
        ) {
            val ballColor = if ((tilt * 10).toInt() == 0) Color.Green else Color.Red
            drawCircle(color = ballColor)
        }
    }

    Text(
        text = "${(tilt * 10).toInt()}°",
        color = Color.Green,
        fontSize = 25.sp
    )
}

Explanation:

  1. Ball Animation:
    • Uses Animatable to animate the ball’s position smoothly.
    • The offset is calculated based on the tilt and constrained within the bar size.
  2. Canvas:
    • Draws the ball inside the spirit level bar.
    • The ball’s color changes to green when the tilt is zero (balanced).
  3. Tilt Angle:
    • Displays the current tilt angle (in degrees) near the bar for user feedback.

Final Output

  • A vertical spirit level indicates the tilt along the Y-axis.
  • A horizontal spirit level indicates the tilt along the X-axis.
  • Both bars are smooth, responsive, and visually appealing.

Enhancements to Try

  1. Add sound or vibration feedback when the device is level.
  2. Include visual alerts for extreme tilt angles.
  3. Allow users to calibrate the spirit level manually.

With this setup, you have a fully functional and interactive spirit level in Jetpack Compose!

You can find the full source code for this image cropper here on GitHub Gist.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top