[Android] 가속도 센서를 이용해 움직이는 View 만들기

Sehee Jeong·2021년 11월 4일
2


휴대폰이 움직이는 방향에 따라 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

  • 휴대폰이 바닥에 누워있는 경우(세번째사진) 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 로 설정했다. 이유는 아래와 같은데,

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

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

이렇게, 가운데에 토마토뷰를 생성하기 위해서 유저가 터치한 곳의 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

profile
android developer @bucketplace

1개의 댓글

comment-user-thumbnail
2022년 5월 17일

너무 잘봤습니다! 좋은 글 감사합니다!!

답글 달기