반응형

사용자와의 상호작용(Interaction)을 처리하는 것은 모바일 프로그래밍에서 굉장히 중요합니다.

안드로이드 애플리케이션은 기본적으로 Activity를 통해 화면을 구성하며, 사용자는 화면을 터치함으로써 애플리케이션에 다양한 이벤트를 전달할 수 있습니다.

 

많은 분들이 안드로이드 앱 개발을 하면서 onTouchEvent()나 OnClickListener#onClick() 등을 사용하며 터치와 클릭에 대한 처리를 하셨을 텐데요. 하지만 안드로이드 플랫폼에서 터치 이벤트를 내부적으로 어떻게 처리하는지에 대해서는 모르는 경우가 많습니다.

 

그렇기 때문에 종종 복잡한 레이아웃 구성이나, 터치 이벤트를 가로채야 하는 등 일반적인 상황에서 약간만 벗어나는 구현을 해야 할 때 헤매기 십상인데요. 이번 포스팅에서는 안드로이드가 어떤 단계를 거쳐서 터치 이벤트를 전달하고 처리하는지에 대해 알아보도록 하겠습니다.

 

Touch Event 전달 순서

안드로이드에서 View에 대한 터치 이벤트가 발생했을 때 이에 대하여 어디서부터 어디로, 어떤 과정을 통해 이벤트가 전달이 될까요?

 

다음 예제를 통해 쉽게 알아보도록 하겠습니다.

 

그림 1. Touch Event 발생

 

위 그림과 같이 레이아웃이 구성되어있다고 가정해보겠습니다.

 

Touch Event가 발생하면, 모든 이벤트 알림의 시작점은 액티비티로부터 시작됩니다.

Activity에서 시작된 이벤트 알림이 Activity -> ViewGroup A -> ViewGroup B -> View 순으로 전달하게 됩니다.

즉, Top down 방식인 거죠.

 

그리고 이벤트를 받은 View에서 Touch Event에 대한 처리를 수행하겠다고 하면, 이번에는 반대로 View -> ViewGroup B -> ViewGroup A -> Activity 순으로 이벤트 처리를 전달하게 됩니다. 한 마디로 액티비티에서 시작해 액티비티로 끝나게 되는 것이죠.

 

그림 2. Touch Event 전달 경로

 

[그림 2]를 살펴보시면 이해가 더 명확하게 될 텐데요.

Activity의 dispatchTouchEvent() 함수를 통해 자식 뷰인 ViewGroup A에게 이벤트 알림이 가고, ViewGroup A에서 onInterceptTouchEvent()를 먼저 실행하여 intercept 여부를 확인한 다음에 false일 경우에만 다시 자식 뷰인 ViewGroup B에게 알림을 전달하게 됩니다.

 

만약 onInterceptTouchEvent() 함수를 Override 하여 true를 리턴하게 구현한다면, 해당 뷰그룹에서 더 이상 자식 뷰에게 터치 이벤트를 전달하지 않게 됩니다.

 

여기서 하나 주의할 점은 onInterceptTouchEvent() 함수는 ViewGroup에 정의된 함수이기 때문에, 액티비티와 View에는 정의되어 있지 않습니다. 그렇다고 해서 인터셉트가 아주 불가능한 것은 아닌데요. 액티비티에서는 dispatchTouchEvent()를 Override 하여 인터셉트에 대한 처리를 구현할 수 있습니다. (View는 불가능)

 

그리고 [그림 2]의 맨 밑에 View를 보면 OnTouchListener가 있을 경우와 없을 경우가 있는데요. 만약 View에 TouchListener를 등록해 놓았다면 리스너의 onTouch() 함수가 처리될 것이고, TouchListener가 별도로 등록되어있지 않다면 View 클래스에 기본적으로 구현되어있는 onTouchEvent()가 호출됩니다.

 

터치 이벤트가 내려올 때 onInterceptTouchEvent()의 결과에 따라 더 이상 아래로 전달되지 않았던 것처럼, 반대로 올라갈때는 onTouchEvent()의 결과가 true면 더이상 전달하지 않게 됩니다.

 

예제

백문이 불여일코! 예제를 통해 위에서 설명한 과정이 제대로 동작하는지 테스트해보겠습니다.

 

먼저 위 그림에서 ViewGroup 의 역할을 할 CustomLayout과, View 의 역할을 할 CustomView을 작성해보겠습니다. CustomView는 TextView를 상속하게 구현했습니다.

 

그리고 터치 이벤트 전달과 관련된 함수들의 순서를 파악하기 위해 각각의 함수들을 재정의하여 로그를 찍어보도록 하겠습니다.

 

CustomLayout.kt

class CustomLayout(context: Context) : FrameLayout(context) {
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("TouchEventTest", "called dispatchTouchEvent() in CustomLayout")
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("TouchEventTest", "called onInterceptTouchEvent() in CustomLayout")
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.d("TouchEventTest", "called onTouchEvent() in CustomLayout")
        return super.onTouchEvent(event)
    }
}

 

CustomView.kt

class CustomView(context: Context) : AppCompatTextView(context) {
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("TouchEventTest", "called dispatchTouchEvent() in CustomView")
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.d("TouchEventTest", "called onTouchEvent() in CustomView")
        return super.onTouchEvent(event)
    }
}

 

다음으로 액티비티에 위에서 만든 것들을 추가해주겠습니다.

 

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val customLayout = CustomLayout(this)
        addContentView(customLayout, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))

        val customView = CustomView(this)
        customView.text = "Hello, Ready Story!"
        customLayout.addView(customView)
    }
}

 

자, 이제 실행해서 로그가 어떻게 찍히는지 보겠습니다.

 

그림 3. 예제 결과

앞서 설명한 것과 같이 부모 뷰인 CustomLayout의 dispatchTouchEvent() -> onInterceptTouchEvent() 순으로 호출되고 이어서 자식 뷰인 CustomView의 dispatchTouchEvent()가 호출됩니다. 그리고 별도의 TouchListener를 등록하지 않았기 때문에 CustomView의 onTouchEvent()가 호출되고 이어서 CustomLayout의 onTouchEvent()가 호출됩니다.

 

이번에는 CustomLayout의 onInterceptTouchEvent()의 결과를 true로 하여 CustomView로 터치 이벤트가 전달되지 않도록 해보겠습니다.

 

CustomLayout.kt

class CustomLayout(context: Context) : FrameLayout(context) {
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("TouchEventTest", "called dispatchTouchEvent() in CustomLayout")
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("TouchEventTest", "called onInterceptTouchEvent() in CustomLayout")
        return true // true 값 반환시 자식에게 터치 이벤트 전달 X
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.d("TouchEventTest", "called onTouchEvent() in CustomLayout")
        return super.onTouchEvent(event)
    }
}

 

그림 4. 터치 인터셉트 결과 화면

[그림 4]의 결과와 같이 CustomLayout에서 CustomView로 터치 이벤트를 전달하지 않고, CustomLayout의 onTouchEvent()가 곧바로 호출되는 것을 확인할 수 있습니다.

 

위 예제에 대한 전체 코드는 Github 저장소에서 확인할 수 있습니다.


 

반응형
반응형