Compose Sensor , 기기의 방향을 계산하는 방 - YangJJune/U-Compass GitHub Wiki

Sensor 의 종류

움직임 감지 센서

  • 세 축(x,y,z) 를 따라 가속력과 회전력을 측정
  • 가속도계, 중력 센서, 자이로스코프, 회전 벡터

환경 센서

  • 주변 기온과 같은 환경 매개변수 측정
  • 압력, 조명, 습도, 기압계, 광도계

위치 센서

  • 기기의 물리적 위치 측정
  • 방향 센서, 자기

Android Sensor Framework

SensorManager

  • Sensor 서비스의 인스턴스를 생성 가능하게 해주는 클래스.
val sensorManager =
        getSystemService(LocalContext.current, SensorManager::class.java) as SensorManager

Sensor

  • SensorManager 를 통해 만들 수 있는 특정 센서의 인스턴스.
val sensor : Sensor? = sensorManager.getDefaultSensor(Sensor.SOME_SENSOR)

SensorEvent

  • 시스템에서는 이 클래스를 사용해서 event 인스턴스를 생성함. 시스템 내부적으로 해당 Sensor 에서의 이벤트가 감지하면 SensorEvent 인스턴스를 생성

SensorEventListener

  • 센서 값이 변경되거나 센서 정확도가 변경될 때 SensorEvent 를 수신하는 callback 메서드를 포함하는 인터페이스
class SensorActivity: Activity(), SensorEventListener() {
		// ... 		
		override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
        // Do something here if sensor accuracy changes.
    }

    override fun onSensorChanged(event: SensorEvent) {
        // The light sensor returns a single value.
        // Many sensors return 3 values, one for each axis.
        val lux = event.values[0]
        // Do something with this sensor value.
    }
    // ...
}

센서와 센서의 기능 식별하기

기기마다, Android 버전마다 존재하는 센서의 종류가 다를 수 있습니다.

해당 기기에 어떤 Sensor 가 존재하는 지 확인하기 위해서는 시스템 서비스에 접근해서 SensorManager 인스턴스를 만들어야 합니다.

private lateinit var sensorManager: SensorManager
...
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager

그 다음으로 모든 센서의 종류를 가져오려면 다음과 같이 작성하면 됩니다.

val deviceSensors: List<Sensor> = sensorManager.getSensorList(Sensor.TYPE_ALL)

만약 특정 유형의 센서를 가져오려면 [센서 개요 링크](https://developer.android.com/develop/sensors-and-location/sensors/sensors_overview?hl=ko) 를 참고해 어떤 Sensor Type 이 있는 지 확인하고, getDefaultSensor(Sensor.SOME_SENSOR) 를 호출하면 됩니다.

private lateinit var sensorManager: SensorManager
...
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor : Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
if (sensor != null) {
    // Success! There's a magnetometer.
} else {
    // Failure! No magnetometer.
}

센서 이벤트 모니터링

Sensor 데이터를 모니터링하려면 SensorEventListener 인터페이스를 통해 오버라이딩되는 두 개의 callback 메서드를 구현하면 됩니다.

  • onAccuracyChanged()

    • Sensor 객체의 정확도가 변경되었을 때 호출되는 메소드.
    // SensorManager.class
    public static final int SENSOR_STATUS_ACCURACY_HIGH = 3;
    public static final int SENSOR_STATUS_ACCURACY_LOW = 1;
    public static final int SENSOR_STATUS_ACCURACY_MEDIUM = 2;
    public static final int SENSOR_STATUS_NO_CONTACT = -1;
    public static final int SENSOR_STATUS_UNRELIABLE = 0;
    override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
        // Do something here if sensor accuracy changes.
    }
  • onSensorChanged()

    • SensorEvent 가 발생했을 때 호출되는 메소드.
    // SensorEvent.class
    public int accuracy;
    public boolean firstEventAfterDiscontinuity;
    public Sensor sensor;
    public long timestamp;
    public final float[] values = null;
    override fun onSensorChanged(event: SensorEvent) {
        // The light sensor returns a single value.
        // Many sensors return 3 values, one for each axis.
        val lux = event.values[0]
        // Do something with this sensor value.
    }

기기 방향 계산

  1. 센서, 방위각 선언

    val context = LocalContext.current
    val sensorManager =
        remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager }
    val accelerometer =  // 가속도계 센서
        remember { sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) } 
     
    val magnetometer =  // 자기장 센서
        remember { sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) } 
    
    var azimuthDeg by remember { mutableFloatStateOf(0f) }  // 방위각 상태 값 (0~360)
  2. 센서 측정값을 담을 배열 선언

    val accelerometerReading = FloatArray(3)
    val magnetometerReading = FloatArray(3)
    val rotationMatrix = FloatArray(9)
    val orientationAngles = FloatArray(3)
    • accelerometerReading : 가속도계 센서의 측정값을 저장할 배열
    • magentometerReading : 자기장 센서의 측정값을 저장할 배열
    • rotationMatrix : 가속도 배열, 자기장 배열 값을 통해
    • orientationAngles : x,y,-z 축을 중심으로 한 회전 각도가 저장되는 배열
  3. 센서 리스너 등록, 정의

    • 센서 값의 변화에 따라 배열에 해당 값을 저장함.

      when (event.sensor.type) {
          Sensor.TYPE_ACCELEROMETER -> {
              // 가속도계 측정값 복사
              accelerometerReading[0] = event.values[0]
              accelerometerReading[1] = event.values[1]
              accelerometerReading[2] = event.values[2]
          }
      
          Sensor.TYPE_MAGNETIC_FIELD -> {
              // 자기장 센서 측정값 복사
              magnetometerReading[0] = event.values[0]
              magnetometerReading[1] = event.values[1]
              magnetometerReading[2] = event.values[2]
          }
      }
    • 두 센서 값(가속도, 자기장)을 통해 회전 행렬 계산

      // 두 센서 값으로부터 회전 행렬과 방위각 계산
      val success = SensorManager.getRotationMatrix(
          rotationMatrix,
          null,
          accelerometerReading,
          magnetometerReading
      )
    • 방위각 계산

      if (success) {
          SensorManager.getOrientation(rotationMatrix, orientationAngles)
          // orientationAngles[0] 이 방위각(rad)이며, [1] Pitch, [2] Roll입니다.
          val azimuthRad = orientationAngles[0]
          var azimuthDegrees = Math.toDegrees(azimuthRad.toDouble()).toFloat()
          if (azimuthDegrees < 0) {
              azimuthDegrees += 360f  // 음수 값을 0~360도로 변환
          }
          azimuthDeg = azimuthDegrees  // 상태 업데이트
      }
    • 센서 리스너 등록, 해제

      // 센서 리스너 등록 (실행 속도: NORMAL)
      sensorManager.registerListener(
          sensorListener,
          accelerometer,
          SensorManager.SENSOR_DELAY_NORMAL
      )
      sensorManager.registerListener(
          sensorListener,
          magnetometer,
          SensorManager.SENSOR_DELAY_NORMAL
      )
      // 컴포저블이 사라질 때 센서 해제
      onDispose {
          sensorManager.unregisterListener(sensorListener)
      }
  4. 방위각을 통해 방향을 화살표로 표시

    // 현재 기기 방위각을 센서로부터 얻음 (항상 0~360도 값 유지)
    val azimuth = rememberAzimuth()
    // 목표 방향 대비 기기 방향의 차이각 계산
    val rotationAngle = (bearingToTarget - azimuth + 360f) % 360f
    
    Image(
        imageVector = Icons.Filled.KeyboardArrowUp,  // 화살표 아이콘 (기본이 위쪽을 가리키는 그림)
        contentDescription = "목표 방향 화살표",
        modifier = modifier
            .size(100.dp)
            .rotate(rotationAngle)
    )

전체 코드

@Composable
fun rememberAzimuth(): Float {
        val context = LocalContext.current
        // SensorManager 및 센서 객체 가져오기
        val sensorManager =
            remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager }
        val accelerometer =
            remember { sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) } // 가속도계 센서
        val magnetometer =
            remember { sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) } // 자기장 센서

        var azimuthDeg by remember { mutableFloatStateOf(0f) }  // 방위각 상태 값 (0~360)

    DisposableEffect(Unit) {
        // 센서 측정값을 담을 배열
        val accelerometerReading = FloatArray(3)
        val magnetometerReading = FloatArray(3)
        val rotationMatrix = FloatArray(9)
        val orientationAngles = FloatArray(3)
        // 센서 이벤트 리스너 정의
        val sensorListener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent) {
                when (event.sensor.type) {
                    Sensor.TYPE_ACCELEROMETER -> {
                        // 가속도계 측정값 복사
                        accelerometerReading[0] = event.values[0]
                        accelerometerReading[1] = event.values[1]
                        accelerometerReading[2] = event.values[2]
                    }

                    Sensor.TYPE_MAGNETIC_FIELD -> {
                        // 자기장 센서 측정값 복사
                        magnetometerReading[0] = event.values[0]
                        magnetometerReading[1] = event.values[1]
                        magnetometerReading[2] = event.values[2]
                    }
                }
                // 두 센서 값으로부터 회전 행렬과 방위각 계산
                val success = SensorManager.getRotationMatrix(
                    rotationMatrix,
                    null,
                    accelerometerReading,
                    magnetometerReading
                )
                if (success) {
                    SensorManager.getOrientation(rotationMatrix, orientationAngles)
                    // orientationAngles[0] 이 방위각(rad)이며, [1] Pitch, [2] Roll입니다.
                    val azimuthRad = orientationAngles[0]
                    var azimuthDegrees = Math.toDegrees(azimuthRad.toDouble()).toFloat()
                    if (azimuthDegrees < 0) {
                        azimuthDegrees += 360f  // 음수 값을 0~360도로 변환
                    }
                    azimuthDeg = azimuthDegrees  // 상태 업데이트
                }
            }

            override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { /* 생략 */
            }
        }
        // 센서 리스너 등록 (실행 속도: NORMAL)
        sensorManager.registerListener(
            sensorListener,
            accelerometer,
            SensorManager.SENSOR_DELAY_NORMAL
        )
        sensorManager.registerListener(
            sensorListener,
            magnetometer,
            SensorManager.SENSOR_DELAY_NORMAL
        )
        // 컴포저블이 사라질 때 센서 해제
        onDispose {
            sensorManager.unregisterListener(sensorListener)
        }
    }

    return azimuthDeg  // 현재 방위각 값을 반환 (수시로 업데이트됨)
}

@Composable
fun ArrowDirectionIndicator(
    modifier: Modifier = Modifier,
    bearingToTarget: Float
) {
    // 현재 기기 방위각을 센서로부터 얻음 (항상 0~360도 값 유지)
    val azimuth = rememberAzimuth()
    // 목표 방향 대비 기기 방향의 차이각 계산
    val rotationAngle = (bearingToTarget - azimuth + 360f) % 360f

    Image(
        imageVector = Icons.Filled.KeyboardArrowUp,  // 화살표 아이콘 (기본이 위쪽을 가리키는 그림)
        contentDescription = "목표 방향 화살표",
        modifier = modifier
            .size(100.dp)
            .rotate(rotationAngle)
    )
}
⚠️ **GitHub.com Fallback** ⚠️