안드로이드 가속도센서 속도 측정 - andeuloideu gasogdosenseo sogdo cheugjeong


open coding

2021. 1. 6. 23:15

안드로이드 가속도센서 속도 측정 - andeuloideu gasogdosenseo sogdo cheugjeong

단위시간당 회전한 각도 값을 측정하는 센서. : 회전속도 측정.

주로 수평유지장치에 사용되고 있음.

........................................................

1.센서data받기 listener 다른거랑 비슷 : type을 Gyroscope로 설정하는거임.

2. Manifest : screenOrientation="portrait"

스마트폰 회전해서 측정하니, 스마트폰 화면 고정해야겠지

..............................................................................................................................................

<Manifest> screenOrientation="portrait"

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.www.gyroscopesensor">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

<MainActivity>

package com.www.gyroscopesensor;

import androidx.appcompat.app.AppCompatActivity;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    //define variables
    TextView textView;

    SensorManager manager;
    SensorListener listener;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //connect to xml
        textView = findViewById(R.id.textView);

        manager = (SensorManager)getSystemService(SENSOR_SERVICE); //각 객체설정
        listener = new SensorListener();

    }//finish


//sensor start method -> onclick도 설정
    public void startSensor(View view){
        Sensor sensor = manager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);    //Gyrocsope센서
        //아래 만들었던 리스너 연결
        boolean chk = manager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI);
        if(chk == false){
            textView.setText("Gyroscope 센서를 지원하지 않습니다.");
        }
    }
//sensor stop method -> onclick 도 설정
    public void stopSensor(View view){
        manager.unregisterListener(listener);
    }



 //sensor data 받기 listener 설정
 class SensorListener implements SensorEventListener {
     @Override
     public void onSensorChanged(SensorEvent event) {//센서변화
         if(event.sensor.getType() == Sensor.TYPE_GYROSCOPE){    //Gyroscope센서라면
             textView.setText("x축 각속도 : "+event.values[0] + "\n");
             textView.append("y축 각속도 : "+event.values[1] + "\n");
             textView.append("z축 각속도 : "+event.values[2] );
         }
     }
     @Override
     public void onAccuracyChanged(Sensor sensor, int accuracy) {//감도변화

     }
 }//finish


}


안드로이드 가속도센서 속도 측정 - andeuloideu gasogdosenseo sogdo cheugjeong


휴대폰이 움직이는 방향에 따라 ImageView가 움직일 수 있는 간단한 방법을 공유해보려고 한다.
(gif 에서 움직이는 View는 "토마토뷰" 라고 지칭하겠다.)

해당 영상은 아래와 같은 조건을 가지고 있다.
1. 화면을 터치한 곳에 토마토뷰가 생성된다.
2. 생성된 토마토 뷰는 휴대폰이 움직이는 방향대로 움직인다.
3. 토마토 뷰가 바닥으로 떨어지는 속도는 랜덤이다. (즉, 각 토마토뷰가 받는 중력이 다르다.)
4. 토마토 뷰는 화면밖으로 벗어날 수 없다.

Sensor Manager 설정하기

토마토뷰를 움직이려면 휴대폰을 기울일 때 변경되는 x,y 좌표값이 필요한데, 좌표값을 감지하기 위해 가속도 센서가 필요하다.

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        initSensorManager()
    }

    private fun initSensorManager() {
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    }

fyi, 가속도 센서외에 다양한 센서 기능을 지원하고 있는데, 사용하고 있는 휴대폰이 특정 센서를 지원하지 않을 수 있으니 자세히 확인해보길 바란다! (예시로, 나의 테스트폰의 경우 가속도 센서는 지원하지만 중력 센서는 지원하지 않는다.)

x, y, z값은 아래와 같이 변경된다.

사진 출처 : https://mailmail.tistory.com/2

안드로이드 가속도센서 속도 측정 - andeuloideu gasogdosenseo sogdo cheugjeong

  • 휴대폰이 바닥에 누워있는 경우(세번째사진) x, y값이 0에 가까워진다.
  • 바닥에 누워있는 상태에서 휴대폰을 바닥과 90도(가운데 사진) 가 되도록 천천히 올리면 y 값이 +9.8에 가까워진다.
  • 바닥에 누워있는 상태에서 휴대폰을 거꾸로 세울수록 y 값이 -9.8에 가까워진다.
  • 휴대폰을 세운 상태에서 오른쪽 방향, 수직으로 내려갈수록 x값이 -9.8에 가까워진다.
  • 휴대폰을 세운 상태에서 왼쪽 방향, 수직으로 내려갈수록 x값이 +9.8에 가까워진다.

화면 터치 시 토마토 뷰 만들기

토마토뷰의 크기는 가로, 세로 동일하게 140 * 140 으로 만들었다.

    private fun addTomatoView(touchedX: Float, touchedY: Float) {
        val tomato = ImageView(this).apply {
            setBackgroundResource(randomImageRes.random())
            layoutParams = LinearLayout.LayoutParams(TOMATO_SIZE, TOMATO_SIZE)
            /**
             * 좌표는 뷰의 왼쪽 상단이 기준점
             */
            x = touchedX - TOMATO_SIZE / 2
            y = touchedY - TOMATO_SIZE / 2
        }
        binding.root.addView(tomato)
        fallingModels.add(TomatoModel(tomato))
    }
    
    companion object {
        private const val TOMATO_SIZE = 140
    }

이 때 토마토뷰가 생성되는 x,y 좌표는 touchedX(touchedY) - TOMATO_SIZE / 2 로 설정했다. 이유는 아래와 같은데,

안드로이드 가속도센서 속도 측정 - andeuloideu gasogdosenseo sogdo cheugjeong

이미지뷰는 사용자가 터치한 좌표가 왼쪽 최상단 기준으로 레이아웃에 addView 된다. 즉 위 사진처럼, 유저가 클릭한 곳이 기준점인 (0,0) 이라면 기준점 기준으로 오른쪽 아래에 이미지뷰가 그려질 것이다.

하지만 유저는 화면을 터치했을 때, 토마토뷰가 오른쪽 아래에 생성되는 것이 아닌, 기준점자체 에 만들어지기를 기대할 것이다. (유저가 터치한 곳 가운데에 토마토뷰가 생성되어야 하고, 곧 그 좌표는 토마토뷰의 웃는 곳일 것이다.)

안드로이드 가속도센서 속도 측정 - andeuloideu gasogdosenseo sogdo cheugjeong

이렇게, 가운데에 토마토뷰를 생성하기 위해서 유저가 터치한 곳의 x, y 좌표에 TOMATO_SIZE / 2 (== 70)를 뺀 좌표값에 addView를 한 것이다.


토마토 모델 생성

data class TomatoModel(
    val tomato: ImageView,
    val speed: Float = Random.nextInt(2, 10).toFloat(),
)

TomatoModel은 ImageView와 ImageView의 속도를 제어할 수 있는 변수 가지고 있다.
생성되는 토마토뷰는 2에서 10 사이의 speed를 랜덤으로 가질 수 있고, speed는 휴대폰이 움직일 때마다 뷰가 움직이는 속도에 관여한다.


x, y값이 변경될 때 뷰 움직이기

    override fun onSensorChanged(event: SensorEvent?) {
        if (event?.sensor == accelerometerSensor) {
            fallingModels.map {
                val exX = event!!.values[0] * it.speed
                val exY = event.values[1] * it.speed

                /**
                 * 오른쪽은 -, 왼쪽은 + 좌표
                 * 위는 -, 아래는 + 좌표
                 */
                with(it.tomato) {
                    x -= exX
                    y += exY

                    if (y > getRealRootViewHeight()) y = getRealRootViewHeight().toFloat()
                    if (y < 0) y = 0f

                    if (x > getRealRootViewWidth()) x = getRealRootViewWidth().toFloat()
                    if (x < 0) x = 0f
                }
            }
        }
    }

가속도 센서 값은 SensorEventListener 를 override 하고, 값이 변경될 때마다 onSensorChanged 에서 감지가 가능하다.

val exX = event!!.values[0] * it.speed
val exY = event.values[1] * it.speed

event.values 를 통해 가속도 센서의 x, y, z 값을 가져올 수 있고, speed 만큼 곱해 토마토뷰의 움직이는 속도를 설정할 수 있다.

with(it.tomato) {
    x -= exX
    y += exY
}

센서 값이 변경될 때마다 토마토뷰의 x, y 값을 변경한다.

  • 위에서 말했듯이, y 좌표는 휴대폰을 바닥에 누웠을 때 기준으로 navigation bar 쪽을 올리면 - 좌표가 되고, status bar 쪽을 올리면 + 좌표가 되기 때문에 y += exY로 설정했다.
  • 반대로 x 좌표는 휴대폰을 왼쪽으로 기울였을 때 + 좌표가 되고, 오른쪽으로 기울였을 때 - 좌표가 되기 때문에 y -= exY로 설정했다.
if (y > getRealRootViewHeight()) y = getRealRootViewHeight().toFloat()
if (y < 0) y = 0f

if (x > getRealRootViewWidth()) x = getRealRootViewWidth().toFloat()
if (x < 0) x = 0f

그리고, x, y 값이 화면 영역을 벗어나는 범위라면 0 혹은 레이아웃의 가로, 세로에 고정할 수 있도록 한다.

하지만 단순히 디바이스의 세로 사이즈를 가져오게되는 경우 문제가 생길 수 있는데, status bar, navigation bar 를 포함한 사이즈를 가져오기 때문에 토마토뷰가 화면 영역에 벗어나게 될 수 있다. 그래서 system bar 영역을 제외한 순수 화면의 세로크기가 필요하다.

private fun getRealRootViewWidth(): Int {
        return window.decorView.width - TOMATO_SIZE
    }

private fun getRealRootViewHeight(): Int {
        return if (Build.VERSION.SDK_INT < 30) {
            window.decorView.height - TOMATO_SIZE - window.decorView.rootWindowInsets.run {
                systemWindowInsetTop + systemWindowInsetBottom
            }
        } else {
            val insets = window.decorView.rootWindowInsets.displayCutout?.run {
                safeInsetBottom + safeInsetTop
            } ?: 0
            window.decorView.height - TOMATO_SIZE - insets
        }
    }

그래서 나의 경우, inset 값을 이용해 순수 화면의 크기만을 가져올 수 있도록 구성했다.

이 때 코드를 자세히 살펴보면 TOMATO_SIZE 도 빼고 계산하고 있음을 알 수 있는데, 그 이유는 위에서 설명했던 "화면 터치 시 토마토 뷰 만들기" 카테고리와 비슷한 맥락이다. 토마토뷰의 좌표 기준은 📌 왼쪽 상단 📌 이기 때문이다.

샘플 코드 : https://github.com/jsh-me/AndoridFallingView