Open In App

How to Create an Android App That Moves a View Using Motion Sensors

Last Updated : 11 Mar, 2025
Summarize
Comments
Improve
Suggest changes
Share
Like Article
Like
Report

A Motion Sensor in an android device detects movement and acceleration, and can be used for many purposes, including navigation, security, and user interaction. One of the most commonly used motion sensors in Android devices is the Accelerometer, which detects changes in movement and orientation.

In this article, we will explore how to use the accelerometer to control a view within an Android application.

How do they work?

  • Motion sensors measure acceleration, gravity, and rotation
  • They can detect tilt, shake, and swing
  • They can also measure the slope and direction of the device

Prerequisites

  • A Physical Android Device with accelerometer : Since emulator won't simulate sensors, we must use a physical device that comes with accelerometer.
  • Android Studio : Install Android Studio, the official IDE for Android development.
  • Basic Knowledge of Kotlin : We will be developing this app using the Kotlin programming language.

Step by Step Implementation of Application

Creating a application for that monitors sensors in a device is a complex task, so we will follow steps to create this application.

Step 1: Create a New Project

To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio.

Note that you must select Kotlin as the programming language.

Step 2: Adding Dependencies

Let's add dependencies for Dagger Hilt, ViewModel, LiveData and Activity KTX. Navigate to Gradle Scripts > built.gradle.kts(Module :app) and add the following dependencies under the dependencies {} scope at the bottom of the file.

dependencies {
...
implementation ("com.google.dagger:hilt-android:2.50")
kapt ("com.google.dagger:hilt-android-compiler:2.50")
implementation ("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation ("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation ("androidx.activity:activity-ktx:1.8.0")
}

In the same file at the top, add the following plugins for kapt and dagger-hilt under the plugins {} scope

plugins {
...
id ("kotlin-kapt")
id("dagger.hilt.android.plugin")
}

Now, navigate to Gradle Scripts > built.gradle.kts(Project :<app_name>) and add the following plugin for dagger-hilt under the plugins {} scope.

plugins {
...
id("com.google.dagger.hilt.android") version "2.51.1" apply false
}


Step 3: Enable View binding

For the ease of use and good practice, we will using view binding. Navigate to Gradle Scripts > built.gradle.kts(Module :app) and add the following code anywhere under the android {} scope.

android {
...
buildFeatures {
viewBinding = true
}
...
}

Step 4: Create an abstract class/ Interface for super class of Measurable Sensors

In this file we will be creating an abstract class or an interface for the broader variety of Sensor known as Measurable Sensor, so that one can implement this interface for any available sensor in a device. Navigate to app > kotlin+java> {package-name}, right click on the folder, select New > Kotlin class/file, and set the name as MeasurableSensor.

MeasurableSensor.kt:

Kotlin
package org.geeksforgeeks.motionsensor

abstract class MeasurableSensor(
    protected val sensorType: Int
) {

    protected var onSensorValuesChanged: ((List<Float>) -> Unit)? = null

    abstract val doesSensorExist: Boolean

    abstract fun startListening()
    abstract fun stopListening()

    fun setOnSensorValuesChangedListener(listener: (List<Float>) -> Unit) {
        onSensorValuesChanged = listener
    }
}


Code Breakdown:

In this file, we are defining what the sensor should be able to do. This is an abstract class which works for every type of sensor.

1. To check whether the device has that specific sensor. It returns a boolean value.

abstract val doesSensorExist: Boolean

2. To initialize/register and unregister the sensor listener and start/stop collecting sensor data when initialized.

abstract fun startListening()
abstract fun stopListening()

3. This function is triggered whenever the sensor value changes. It takes a list of floating-point numbers (List<Float>), which represent the sensor readings (e.g., x, y, z values for an accelerometer).

protected var onSensorValuesChanged: ((List<Float>) -> Unit)? = null

4. This function allows external classes to set a listener for sensor value changes.

fun setOnSensorValuesChangedListener(listener: (List<Float>) -> Unit) {
onSensorValuesChanged = listener
}


Step 5: Create an abstract class for Android Sensors

This class inherits the MeasurableSensor abstract class we just created and provides a implementation for handling Android sensors. It works like a base class for specific sensors (e.g., accelerometer, gyroscope) and implements the SensorEventListener interface to listen for sensor changes.

Navigate to app > kotlin+java> {package-name}, right click on the folder, select New > Kotlin class/file, and set the name as AndroidSensor.kt.

AndroidSensor.kt:

Kotlin
package org.geeksforgeeks.motionsensor

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager

abstract class AndroidSensor(
    private val context: Context,
    private val sensorFeature: String,
    sensorType: Int
): MeasurableSensor(sensorType), SensorEventListener {

    override val doesSensorExist: Boolean
        get() = context.packageManager.hasSystemFeature(sensorFeature)

    private lateinit var sensorManager: SensorManager
    private var sensor: Sensor? = null

    override fun startListening() {
        if(!doesSensorExist) {
            return
        }
        if(!::sensorManager.isInitialized && sensor == null) {
            sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
            sensor = sensorManager.getDefaultSensor(sensorType)
        }
        sensor?.let {
            sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_FASTEST)
        }
    }

    override fun stopListening() {
        if(!doesSensorExist || !::sensorManager.isInitialized) {
            return
        }
        sensorManager.unregisterListener(this)
    }

    override fun onSensorChanged(event: SensorEvent?) {
        if(!doesSensorExist) {
            return
        }
        if(event?.sensor?.type == sensorType) {
            onSensorValuesChanged?.invoke(event.values.toList())
        }
    }

    override fun onAccuracyChanged(p0: Sensor?, p1: Int) = Unit
}


Code Breakdown:

-> Parameters and Class Declaration

abstract class AndroidSensor(
private val context: Context,
private val sensorFeature: String,
sensorType: Int
): MeasurableSensor(sensorType), SensorEventListener

This is an android specific sensor class which inherits the MeasurableSensor abstract class we just created and the built-in SensorEventListener interface. The parameters are as follows:

  • context - to get the instance of the sensor manager.
  • sensorFeature - it represents the sensor feature (e.g., PackageManager.FEATURE_SENSOR_ACCELEROMETER which we will be using in this article).
  • sensorType - it's passed to the MeasurableSensor parent class, representing the specific sensor type (e.g., Sensor.TYPE_ACCELEROMETER).

-> Checking if sensor exists

override val doesSensorExist: Boolean
get() = context.packageManager.hasSystemFeature(sensorFeature)

The above function uses the package manager to check whether the specific sensor exists in the device. This will ensure that the app doesn't try to fetch a sensor even if it isn't available on the device.

-> Initializing sensor manager and sensor

private lateinit var sensorManager: SensorManager
private var sensor: Sensor? = null

What is Sensor Manager?

The SensorManager is a system service in Android that allows apps to communicate with different sensors on the device. It provides:

  • Access to hardware sensors (accelerometer, gyroscope, proximity, etc.).
  • Methods to register/unregister sensor event listeners (start and stop receiving sensor updates).
  • Information about available sensors on the device.

What is Sensor?

The Sensor class represents a specific physical sensor on the device. This could be an accelerometer, gyroscope, magnetometer, etc.

-> startListening()

override fun startListening() {
if(!doesSensorExist) {
return
}
if(!::sensorManager.isInitialized && sensor == null) {
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
sensor = sensorManager.getDefaultSensor(sensorType)
}
sensor?.let {
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_FASTEST)
}
}

First, it checks whether the sensor exists, if it doesn't, then it returns the function (doing nothing). Then, it initializes Sensor Manager and Sensor if it isn't already done. Finally, if the sensor exists, it registers the class as a listener using the keyword, SENSOR_DELAY_FASTEST, to get faster updates.

-> stopListening()

override fun stopListening() {
if(!doesSensorExist || !::sensorManager.isInitialized) {
return
}
sensorManager.unregisterListener(this)
}

This unregisters the listener from getting any further updates when the app is not in use.

-> onSensorChanged()

override fun onSensorChanged(event: SensorEvent?) {
if(!doesSensorExist) {
return
}
if(event?.sensor?.type == sensorType) {
onSensorValuesChanged?.invoke(event.values.toList())
}
}

First, it checks whether the current event is utilizing the current sensor type. If it is so, then it converts the sensor values to a list and triggers the onSensorValuesChanged callback.

-> onAccuracyChanged()

override fun onAccuracyChanged(p0: Sensor?, p1: Int) = Unit

This method is required by SensorEventListener, but we don't need it, so it does nothing.

Step 6: Create a class to define specific sensors

In this file, we will be defining out specific sensor, which in out case is the Accelerometer. We can define any other sensor here and use it in the same way. Navigate to app > kotlin+java> {package-name}, right click on the folder, select New > Kotlin class/file, and set the name as Sensors.

Sensors.kt:

Kotlin
package org.geeksforgeeks.motionsensor

import android.content.Context
import android.content.pm.PackageManager
import android.hardware.Sensor

class MotionSensor(
    context: Context
): AndroidSensor(
    context = context,
    sensorFeature = PackageManager.FEATURE_SENSOR_ACCELEROMETER,
    sensorType = Sensor.TYPE_ACCELEROMETER
)


Code Breakdown:

This class extends AndroidSensor and specifically implements an accelerometer-based motion sensor. The variables declared here are -

  • context - to get the instance of the sensor manager.
  • sensorFeature - it represents the sensor feature (e.g., PackageManager.FEATURE_SENSOR_ACCELEROMETER which we will be using in this article).
  • sensorType - The specific sensor type is declared here (e.g., Sensor.TYPE_ACCELEROMETER).

Step 7: Create a View Model to manage Sensor Data

Navigate to app > kotlin+java> {package-name}, right click on the folder, select New > Kotlin class/file, and set the name as MainViewModel. This MainViewModel class manages accelerometer sensor data and sends it to the UI using LiveData. It is structured with Dependency Injection (DI) using Hilt, making it easy to manage and test.

MainViewModel.kt:

Kotlin
package org.geeksforgeeks.motionsensor

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(
    private val accelerometerSensor: MeasurableSensor
) : ViewModel() {

    private val _sensorData = MutableLiveData<Pair<Float, Float>>() // Pair for X and Y axis
    val sensorData: LiveData<Pair<Float, Float>> get() = _sensorData

    init {
        accelerometerSensor.startListening()
        accelerometerSensor.setOnSensorValuesChangedListener { values ->
            val sides = values[0]  // X-axis
            val upDown = values[1] // Y-axis
            _sensorData.postValue(Pair(sides, upDown))
        }
    }
}


Code Breakdown:

-> Parameters and Class Declaration

@HiltViewModel
class MainViewModel @Inject constructor(
private val accelerometerSensor: MeasurableSensor
) : ViewModel()

  • @HiltViewModel - Marks this class as a ViewModel that Hilt can inject dependencies into.
  • @Inject constructor( ... ) - Hilt automatically provides the required dependency (MeasurableSensor).
  • accelerometerSensor: MeasurableSensor - The use of MeasurableSensor allows flexibility, meaning this ViewModel can work with any sensor that extends MeasurableSensor, not just the accelerometer. In actual use, an instance of MotionSensor (which extends AndroidSensor -> MeasurableSensor) will be injected here.

-> Using LiveData to hold Sensor Data

private val _sensorData = MutableLiveData<Pair<Float, Float>>()
val sensorData: LiveData<Pair<Float, Float>> get() = _sensorData

The variable _sensorData hold the values for the X and Y axis from the sensor as a Pair of two float variables. The variable sensorData holds the same data but it is read-only, which makes sure the variable _sensorData is passed to the UI.

-> Listening for updates

init {
accelerometerSensor.startListening()
accelerometerSensor.setOnSensorValuesChangedListener { values ->
val sides = values[0]
val upDown = values[1]
_sensorData.postValue(Pair(sides, upDown))
}
}

In the above code, the sensor gets initialized and it starts listening to sensor updates. The values of the X-axis are stored in the variable sides and for the Y-axis, the values are stored in the variable upDown.

  • postValue(x,y) - This updates LiveData on the main thread.

Step 8: Create an object to setup Dagger Hilt Module

Navigate to app > kotlin+java> {package-name}, right click on the folder, select New > Kotlin class/file, and set the name as SensorModule. This SensorModule class is a Dagger Hilt module that provides an instance of MotionSensor whenever MeasurableSensor is required.

SensorModule.kt:

Kotlin
package org.geeksforgeeks.motionsensor

import android.app.Application
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object SensorModule {

    @Provides
    @Singleton
    fun provideMotionSensor(app: Application): MeasurableSensor {
        return MotionSensor(app)
    }
}


Code Breakdown:

What is a Dagger Hilt Module?

A Dagger-Hilt module is a class that defines how to provide dependencies. It uses @Module and @Provides annotations to tell Hilt how to create and inject objects.

- Annotations

  • @Module - This annotations marks the class a hilt view model so that it can provide dependencies.
  • @InstallIn(SingletonComponent::class) - This annotation specifies that this module's dependencies will exist for the entire app lifecycle (Singleton scope).
  • @Provides - This annotation tells hilt how to create an instance of MotionSensor.
  • @Singleton - This annotation ensures that only one instance of MotionSensor is created and shared across the app.

- provideMotionSensor()

fun provideMotionSensor(app: Application): MeasurableSensor {
return MotionSensor(app)
}

This function returns a MotionSensor, but as a MeasurableSensor (because MeasurableSensor is the superclass of MotionSensor). This allows flexibility so that any sensor implementing MeasurableSensor can be used instead.


Step 9: Create an application class to initialize Dependency Injection

Navigate to app > kotlin+java> {package-name}, right click on the folder, select New > Kotlin class/file, and set the name as MotionSensorApp. We will be using this class to setup Hilt to initialize Dependency Injection in the entire app.

MotionSensorApp.kt:

Kotlin
package org.geeksforgeeks.motionsensor

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MotionSensorApp: Application()


Code Breakdown:

The annotation @HiltAndroidApp is applied in this class to:

  • Initialize Hilt at the application level.
  • Automatically generate Hilt’s dependency graph.
  • Enable dependency injection (DI) throughout the app.

This makes it the base application class which starts before any other app components.


Step 10: Register base application class in AndroidManifest.xml

Navigate to app > manifests > AndroidManifest.xml and the add the following code inside the <application> tag.

<application
android:name=".MotionSensorApp"
...
</application>


Step 11: Working with activity_main.xml

Navigate to app > res > layout > activity_main.xml and make the following changes. We will just be adding a textview with a red background and update its rotation and other movements in the MainActivity.kt.

activity_main.xml:

XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:text="up/down 0\n left/right 0"
        android:gravity="center"
        android:background="@android:color/holo_red_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>


Design & Blueprint:

Screenshot-2025-03-01-173507


Step 12: Working with MainActivity.kt

Navigate to app > kotlin+java > {package-name} >MainActivity.kt and make the following change in the code. In this file, we will be observing the data from the Accelerometer sensor via the MainViewModel and update the changes in the UI.

MainActivity.kt:

Kotlin
package org.geeksforgeeks.motionsensor

import android.graphics.Color
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import org.geeksforgeeks.motionsensor.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(binding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        viewModel.sensorData.observe(this) { (sides, upDown) ->
            binding.textView.apply {
                rotationX = upDown * 3f
                rotationY = sides * 3f
                rotation = -sides
                translationX = sides * -10
                translationY = upDown * 10
            }

            val backgroundColor = if (upDown.toInt() == 0 && sides.toInt() == 0) Color.GREEN else Color.RED
            val textColor = if (upDown.toInt() == 0 && sides.toInt() == 0) Color.BLACK else Color.WHITE
            binding.textView.setBackgroundColor(backgroundColor)
            binding.textView.setTextColor(textColor)
            binding.textView.text = buildString {
                append("up/down ")
                append(upDown.toInt())
                append("\n")
                append("left/right ")
                append(sides.toInt())
            }
        }
    }
}


Code Breakdown:

-> @AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity()

This annotation enables Dependency Injection into the app allowing MainViewModel to inject automatically.

-> Initializing ViewModel and ViewBinding

private val viewModel: MainViewModel by viewModels()
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}

The by viewModels() uses the hilt injected sensor data to observe changes in the values.

To know more about View Binding refer to View Binding in Android Jetpack.

-> Observing sensor data and Updating UI

viewModel.sensorData.observe(this) { (sides, upDown) ->
binding.textView.apply {
rotationX = upDown * 3f
rotationY = sides * 3f
rotation = -sides
translationX = sides * -10
translationY = upDown * 10
}
...
}
}

The viewModel.sensorData.observe() observes liveData from the viewModel and returns two values sides and upDown, which are values of X-Axis and Y-Axis respectively. Then the values are used to tilt and rotate the TextView as follows:

  • rotationX = upDown * 3f - Tilts the TextView up/down.
  • rotationY = sides * 3f - Tilts the TextView left/right.
  • rotation = -sides - Rotates based on lateral movement.
  • translationX = sides * -10 - Moves horizontally.
  • translationY = upDown * 10 - Moves vertically.

-> Checking if the device is in Neutral position

val backgroundColor = if (upDown.toInt() == 0 && sides.toInt() == 0) Color.GREEN else Color.RED
val textColor = if (upDown.toInt() == 0 && sides.toInt() == 0) Color.BLACK else Color.WHITE
binding.textView.setBackgroundColor(backgroundColor)
binding.textView.setTextColor(textColor)
binding.textView.text = buildString {
append("up/down ")
append(upDown.toInt())
append("\n")
append("left/right ")
append(sides.toInt())
}

If the phone is in a neutral position (0,0), the background turns Green and text turns Black . If the phone is moving, the background turns Red and text turns White. Finally it updates the values of the x and y axis in the textview.

For Checking on the whole application refer to Motion Sensor Application.

Output:


Next Article

Similar Reads