ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UI] Touch Event (2) - 터치 이벤트의 시작과 끝
    Android/UI 2021. 9. 16. 16:03

    이전 포스팅 - 2021.09.15 - [Android/TouchEvent] - [Android] Touch Event (1) - 터치 이벤트 전달 과정

     

    [Android] Touch Event (1) - 터치 이벤트 전달 과정

    https://readystory.tistory.com/185 [Android] 안드로이드의 Touch Event 는 어떻게 전달 될까? (with. Touch Intercept) 사용자와의 상호작용(Interaction)을 처리하는 것은 모바일 프로그래밍에서 굉장히 중요..

    hellose7.tistory.com

    참고한 블로그- https://jamssoft.tistory.com/161

     

    안드로이드 Touch 다루기 : onTouch와 onTouchEvent

    안드로이드에서 사용자가 Touch를 할 경우 그에 상응하는 동작을 필요에 따라 만들어 넣어야 한다. 이 Touch다루는 법을 전체적으로 다루려고 한다. 간단한 것은 어렵지 않게 만들 수 있지만, 때로

    jamssoft.tistory.com


    저번 포스팅에서 Activity -> ViewGroup -> 의 자식ViewGourp -> 의 자식 View 형태에서 터치 이벤트의 전달 과정과 각 계층의 터치 이벤트 메서드의 return값에 따른 처리 과정을 확인하였다. 

     

    onTouchEvent메서드를 곁들여서 같이 볼 것이다. 또한 터치와 클릭, 롱클릭이 어떻게 동작하는지 확인한다.

     

    터치 이벤트와 클릭 이벤트 롱클릭이벤트 등은 어떻게 처리되는 것일까?

    MotionEvent의 액션들과(ex. MotionEvent_ACTION_DOWN)  View.OnTouchListener의 onTouch메서드, View의 onTouchEvent메서드의 리턴값과 관련이 있다.

    그리고 onTouchEvent가 오버라이딩 되어있는 경우와 되어있지 않은 경우도 생각해야 하므로 상당히 복잡하다.

     

    MotionEvent.ACTION_DOWN (손가락 눌림- 터치 시작)

    MotionEvent.ACTION_MOVE ( 손가락 눌림 유지 및 손가락 움직임 )

    MotionEvent.ACTION_UP ( 손가락 떼짐 ) 


    리니어 레이아웃에 커스텀 텍스트뷰를 자식으로 추가

    리턴값에 따른 MotionEvent의 액션을 터치를 하면서 확인

     

    • CustomTextView (커스텀 텍스트뷰)
    import android.content.Context
    import android.util.Log
    import android.view.MotionEvent
    import androidx.appcompat.widget.AppCompatTextView
    
    class CustomTextView(context: Context) : AppCompatTextView(context) {
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {
            when (event?.actionMasked) {
                MotionEvent.ACTION_DOWN -> log("뷰 onTouchEvent 손가락 눌림")
                MotionEvent.ACTION_MOVE -> log("뷰 onTouchEvent 눌린 상태에서 손가락 움직임")
                MotionEvent.ACTION_UP -> log("뷰 onTouchEvent 손가락 떼짐")
            }
            return false //false로 설정
        }
    
        fun log(str: String) {
            Log.d("TouchEventTest2", str)
        }
    }

     

    onTouchEvent 메서드를 오버라이딩 하였다.

     

    • Activity_test2.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".clickable.TestActivity2">
    </LinearLayout>

    컨테이너 리니어 레이아웃, id는 container

    동적으로 커스텀뷰를 자식으로 추가하기 위함

     

    • TestActivity2
    import android.graphics.Color
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Log
    import android.view.Gravity
    import android.view.MotionEvent
    import android.view.View
    import android.view.ViewGroup
    import com.source.viewevent.eventtest.R
    
    class TestActivity2 : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test2)
    
            val root: View = findViewById(R.id.container)
            root as ViewGroup
    
            //text, gravity, 배경색 설정
            val customTextView: CustomTextView = CustomTextView(this)
            customTextView.text = "longCLickable,clickable TextView"
            customTextView.gravity = Gravity.CENTER
            customTextView.setBackgroundColor(Color.GRAY)
    
            //root의 자식으로 추가
            root.addView(
                customTextView,
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
    
            //터치 리스너 설정
            customTextView.setOnTouchListener { view, motionEvent ->
                when (motionEvent.actionMasked) {
                    MotionEvent.ACTION_DOWN -> log("뷰 onTouch 손가락 눌림")
                    MotionEvent.ACTION_MOVE -> log("뷰 onTouch 눌린 상태에서 손가락 움직임")
                    MotionEvent.ACTION_UP -> log("뷰 onTouch 손가락 떼짐")
                }
                false //false로 설정
            }
        }
    
        fun log(str: String) {
            Log.d("TouchEventTest2", str)
        }
    
    }

     

    리턴 값을 변경하면서 확인해보자

     

    • onTouch, onTouchEvent 둘다 false인 경우(위의 코드 상태)

    위의 상태는 onTouch false, onTouchEvent (오버라이딩 안된 경우에도 기본 false) false 이다

     

    앱을 실행하고 터치를 유지해보자.

    터치를 유지하거나 떼도 MotionEvent_ACTION_MOVE, MotionEvent_ACTION_UP에 해당하는 로그는 출력되지 않는다.

    왜그럴까?

     

    사용자가 터치를 시작하면 시스템에서 터치 이벤트를 연속적으로 꾸준히 보내게 된다. 

    터치 이벤트들이 꾸준히 보내질 때마다 onTouch, onTouchEvent도 꾸준히 호출되게 된다.

    터치가 시작될 때 처음으로 받는 터치 이벤트는 MotionEvent.ACTION_DOWN인데 이때의 리턴 값에 따라 이후에 발생하는 터치 이벤트들을 꾸준히 받을지 말지를 결정하게 된다!!!!

     

    첫 터치 이벤트를 받으면 onTouch에서 터치 이벤트를 처리한후 false이기 때문에

    현재 다뤄지는 터치 이벤트의 남은 처리가 있다는 것이고 (전 포스팅 참고)

    바로 다음인 onTouchEvent 메서드에서 현재 터치 이벤트의 처리가 진행된후 이 또한 false이기 때문에

    상위 뷰 그룹인 LinearLayout으로 터치 이벤트 처리가 넘어간다.

    리니어 레이아웃 onTouchEvent 또한 기본 값은 false이므로 액티비티까지 올라가서 최종적으로 false로 결정되어 터치상태에서 이후에 올 이벤트가 도착하지 않게 된다. ( 현재 최종적으로 false가 되어 터치 시작후 이후에 터치 이벤트들이 보내지지 않는다)

     

    • 상위 ViewGroup에 true가 설정된 경우

    onTouch, onTouchEvent 둘다 false에 상위 뷰그룹인 LinearLayout의 onTouchEvent가 true인 경우

     

    TestActivity2에  다음의 내용만 추가하고 앱을 실행하여 터치를 해보자.

    	val root: View = findViewById(R.id.container)
    
            //상위 ViewGroup에 true를 준 경우
            root.setOnTouchListener { v, event ->
                when (event?.actionMasked) {
                    MotionEvent.ACTION_DOWN -> log("레이아웃 onTouch 손가락 눌림")
                    MotionEvent.ACTION_MOVE -> log("레이아웃 onTouch 눌린 상태에서 손가락 움직임")
                    MotionEvent.ACTION_UP -> log("레이아웃 onTouch 손가락 떼짐")
                }
                true
            }
            root as ViewGroup

    View자체의 터치 이벤트 핸들러가 false여도

    상위계층에서 호출되는 터치 이벤트 핸들러중 하나라도 리턴 true가 있으면 

    이후에 꾸준히 사용자의 상호작용에 해당하는 터치 이벤트가 전달되게 된다. 꾸준히 전달되어 핸들러 메서드들도 꾸준히 호출된다.


    로그 출력이 복잡해지니 리니어 레이아웃의 터치 리스너를 다시 지우고 계속 보자.

     

    • onTouch가 true인 경우

    onTouch에서 터치 이벤트가 모두 처리된다는 것으로 이후에 호출되는 모든 터치 이벤트 처리 메서드들은 호출되지 않는다. ( 뷰의 onTouchEvent나 상위 뷰그룹들의 onTouchEvent등등)

    또한 View에게 꾸준히 사용자의 상호작용에 해당하는 터치 이벤트를 꾸준히 보내라라는 말이 된다.

    터치 이벤트를 계속 받기 때문에 받을 때마다 onTouch메서드도 꾸준히 호출되어 로그가 계속 찍히게 된다.

    손가락을 누르고 있으면 계속 로그가 쫘르르 출력된다.


    • onTouch가 false,  onTouchEvent가 true

     

    onTouch에서 터치 이벤트를 처리하고 이 터치 이벤트의 남은 처리가 있다는 것으로 onTouchEvent에서 터치 이벤트를 처리한다.

    onTouchEvent에서 터치 이벤트를 처리하고 true여서 현재 처리하고 있는 터치 이벤트의 처리가 끝난다. 상위 계층의 ViewGroup들의  onTouchEvent는 호출되지 않는다.

    onTouchEvent도 마찬가지로 View에 해당하므로 View는 계속 후속 이벤트(꾸준히 보내지는)를 받는다


    • onTouchEvent을 오버라이딩하지 않고(항상 false) onTouch를 다음과 같이 변경한다.
    customTextView.setOnTouchListener { view, motionEvent ->
    
                when (motionEvent.actionMasked) {
                    MotionEvent.ACTION_DOWN -> {
                        log("뷰 onTouch 손가락 눌림")
                        true
                    }
                    MotionEvent.ACTION_MOVE -> {
                        log("뷰 onTouch 눌린 상태에서 손가락 움직임")
                        false
                    }
                    MotionEvent.ACTION_UP -> {
                        log("뷰 onTouch 손가락 떼짐")
                        false
                    }
                    else -> false
                }
            }

     

    후속 이벤트를 받을 것인지는 가장 처음인 ACTION_DOWN때의 메서드 리턴값으로 정해진다.

     

    사용자가 처음 터치를 시작하고 ACTION_DOWN으로 가서 true를 리턴한다. 후속 이벤트가 온다.

    ACTION_MOVE에 해당되어 onTouch는 false이고 이후에 상위 뷰그룹도 모두 false이기 때문에 false인데 후속 이벤트가 안오는 것인가 생각될 수 있다.

    ACTION_MOVE의 리턴값은 true로하든 false로 하든 후속 이벤트가 계속오게되고, 단지 이후에 호출될 터치 이벤트 처리 메서드들의 호출여부만 결정하게 된다.

     

    ACTION_UP의 경우 사용자가 손을 뗐을 때 현재 메서드 이외에 onToucheEvent라든가 상위 뷰그룹에 추가적으로 터치 처리작업이 필요하면 false, 아니면 true로 하면된다.

     

    정리하면

     

    1. 후속이벤트를 꾸준히 받을 것인지는 사용자 터치 시작에 처음보내지는 ACTION_DOWN때의 메서드 리턴값으로 정해진다. 

    2. ACTION_MOVE들에 모두 false라도 후속이벤트들은 항상 전달된다

    3. ACTION_MOVE, ACTION_UP에 들어있는 리턴값은 터치 이벤트 처리를 여기서 완료할 것인지 아니면 이후에 줄줄이 호출될 메서들들이 호출될 수 있게만 하는 정하는 역할만 한다.

     

    위 처럼 분기시켜 처리해야하는 이유는 Touch리스너와 클릭등 다양한 이벤트들을 동시에 구현해야하는 경우가 많기 때문이다. 이제 클릭 이벤트를 보자.


    클릭 이벤트의 발생은 View로 보내지는 터치 이벤트 ACTION_DOWN과 ACTION_UP를 가지고 판단된다.

    액션 업일 때 클릭 리스너의 메서드가 호출되게 하여야 하는데 내가 의아해했던 점을 써보려한다.

     

    클릭 이벤트를 확인하자.

     

    클릭 리스너 추가

    //onCreate에 클릭 리스너 설정
    customTextView.setOnClickListener { view -> log("뷰 클릭됨") }

     

    setOnTouchListener를 주석 처리하고 onTouchEvent에서 다뤄보자. (onTouch에서 하려면 더 주의해야할 것이 있어서)

    class CustomTextView(context: Context) : AppCompatTextView(context) {
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {
    
            return when (event?.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    log("뷰 onTouchEvent 손가락 눌림")
                    true
                }
                MotionEvent.ACTION_MOVE -> {
                    log("뷰 onTouchEvent 눌린 상태에서 손가락 움직임")
                    true
                }
                MotionEvent.ACTION_UP -> {
                    log("뷰 onTouchEvent 손가락 떼짐")
                    true
                }
                else -> false
            }
        }
    
        fun log(str: String) {
            Log.d("TouchEventTest2", str)
        }
    }

    앱을 실행하고 터치 및 클릭

    클릭 로그가 출력이 되지 않는다.

    보면 View의 onTouchEvent의 ACTION_DOWN에 리턴값이 true여서 터치후 계속 발생하는 후속 이벤트는 계속 받고 있지만 클릭이벤트를 발생시키는 코드는 기존 onTouchEvent에 들어있기 때문이다.

    현재 onTouchEvent를 오버라이딩후 super클래스를 호출하지 않았고 재정의해서 클릭이벤트가 발생하지 않았다.

     

    기본적인 View의 onTouchEvent메서드의 동작에는 ACTION_DOWN, ACTION_UP을 가지고 클릭이벤트를 발생시키는 코드가 들어가 있다. onTouchEvent를 오버라이딩 하였기 때문에 직접 클릭 이벤트를 발생시키는 performClick()을 넣어줘야한다.

     

    기존 onTouchEvent의 동작으로 확인하면 다음과 같이 클릭 로그가 뜰 것이다.


    그렇기 때문에 onTouchEvent 에서 직접 ACTION_UP일 때 클릭 이벤트가 발생할 수 있도록 performClick()메서드를 호출하여야 한다.


     

    onTouchEvent에서 했던 것을 똑같이 onTouch에서 해보자.

    클릭 작업이 포함되는 경우 주의할점이 많다. 여기서부터 많이 헤맸다. 

     

    onTouchEvent를 오버라이딩하지 않고 주석 처리되었던 onTouch를 다시 변경하여 이렇게 설정하면

    customTextView.setOnTouchListener { view, motionEvent ->
    
                when (motionEvent.actionMasked) {
                    MotionEvent.ACTION_DOWN -> {
                        log("뷰 onTouch 손가락 눌림")
                        true
                    }
                    MotionEvent.ACTION_MOVE -> {
                        log("뷰 onTouch 눌린 상태에서 손가락 움직임")
                        false
                    }
                    MotionEvent.ACTION_UP -> {
                        log("뷰 onTouch 손가락 떼짐")
                        view.performClick()
                        true
                    }
                    else -> false
                }
            }

    onTouchEvent때와 마찬가지로 클릭이벤트가 발생한다.

     

    기본적으로 View에서 클릭 이벤트의 감지는 onTouchEvent에서 한다. 

    ACTION_DOWN과 ACTION_UP에 해당하는 터치 이벤트 두개를 받고 클릭이벤트를 발생시킬지 결정한다.

    clickable한 뷰는 클릭을 감지할 수 있어야 하기 때문에(ACTION_DOWN, ACTION_UP 필요) 후속이벤트를 계속 받을 수 밖에없다.

    setOnCLickListener의 등록을 통해 뷰가 자동으로 clickable한 view로 바뀌었고 onTouch의 ACTION_DOWN때의 return값이 false라도 onTouchEvent의 리턴값이 true로 변하게 되어 자동으로 후속이벤트를 받을 수 있게 변경된다.

    항상 후속 이벤트를 받는다( 현재 onTouchEvent는 오버라이딩 되어있지 않다 기본값 false이지만 clickable한뷰는 true)

     

    ACTION_DOWN의 리턴값을 false로 하고 확인해보자.

    false로 해놨어도 후속이벤트가 계속와 로그에 쫘르르 출력된다.(클릭가능한 뷰는 onTouchEvent가 true로 변경된다)

    로깅을 안해놔서 그렇지 onTouchEvent도 계속 호출되고 있을 것이다.

     

    ACTION_DOWN, ACTION_UP에 false로 바꿔주고 앱을 실행하여 터치를 해보면 클릭이벤트가 두번 발생하는데

    ACTION_DOWN일때의 터치 이벤트가 onTouchEvent로 넘어가고 

    ACTION_UP일때의 터치 이벤트가 onTouchEvent로 넘어가서 자동으로 클릭을 발생시키기 때문에

    클릭이 한번 더 된다.

    둘중 하나라도 onTouchEvent로 넘어가지 않으면 (둘중하나라도 true) 면 onTouchEvent에서 클릭 이벤트를 판단하지 못하기 때문에 onTouchEvent에서 클릭 이벤트 발생시키지 않음


    롱 클릭에 대해서 알아보자.

     

    롱 클릭만 확인하기 위해 ACTION_UP 제거 및 클릭 리스너 제거 

    View의 onTouchEvent에서는 ACTION_DOWN과 ACTION_UP으로 클릭 이벤트를 감지하였다.

    마찬가지로 상식적으로 생각해보면 롱클릭을 길게 터치되는 것이기 때문에 onTouchEvent의 메서드에 ACTION_DOWN과 ACTION_MOVE에 해당하는 터치 이벤트 두개를 받게되면 롱클릭이 발생하는 것을 알수있다.

    또한 clickable과 마찬가지로 longClickable이 true인 View도 항상 후속 이벤트를 받을까도 확인해봤는데 ACTION_DOWN때의 onTouch가 false일때 onTouchEvent의 리턴값이 true로 변경되어 계속 받는 것을 확인된다.

    ACTION_DOWN, ACTION_MOVE 둘중 하나라도 true로 변경하면 onTouchEvent에서 롱클릭을 감지하지 못하여 롱클릭이 발생하지 않는다.

     

    롱클릭, 클릭 리스너 모두 설정하고 onTouchEvent에서 감지할수 있도록 하자.

    onTouchEvent오버라이딩x

    import android.graphics.Color
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Log
    import android.view.Gravity
    import android.view.MotionEvent
    import android.view.View
    import android.view.ViewGroup
    import com.source.viewevent.eventtest.R
    
    class TestActivity2 : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test2)
    
            val root: View = findViewById(R.id.container)
    
            root as ViewGroup
    
            //text, gravity, 배경색 설정
            val customTextView: CustomTextView = CustomTextView(this)
            customTextView.text = "longCLickable,clickable TextView"
            customTextView.gravity = Gravity.CENTER
            customTextView.setBackgroundColor(Color.GRAY)
    
            //root의 자식으로 추가
            root.addView(
                customTextView,
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
    
            customTextView.setOnTouchListener { view, motionEvent ->
    
                when (motionEvent.actionMasked) {
                    MotionEvent.ACTION_DOWN -> {
                        log("뷰 onTouch 손가락 눌림")
                        false
                    }
                    MotionEvent.ACTION_MOVE -> {
                        //log("뷰 onTouch 눌린 상태에서 손가락 움직임") //로그출력때문에 생략
                        false
                    }
                    MotionEvent.ACTION_UP -> {
                        log("뷰 onTouch 손가락 떼짐")
                        false
                    }
                    else -> false
                }
            }
            customTextView.setOnLongClickListener { view ->
                log("뷰 롱클릭(길게 터치)됨")
                false
            }
            customTextView.setOnClickListener { view ->
                log("뷰 클릭됨")
            }
            
        }
    
        fun log(str: String) {
            Log.d("TouchEventTest2", str)
        }
    }

    View.onLongClickListener의 onLongClick의 리턴값을 true로 변경하면 

    이벤트 처리가 여기서 끝나므로 onClick의 이벤트 처리는 진행되지 않는다.

     

    더 깊게 알아보려면 MotionEvent의 마스크를 이해하고 로깅해봐야겠다.

    'Android > UI' 카테고리의 다른 글

    [UI] 스타일과 테마 - 1  (0) 2021.09.24
    [UI] Context Menu 사용법  (0) 2021.09.18
    [UI] Touch Event (1) - 터치 이벤트 전달 과정  (0) 2021.09.15
    [UI] RecyclerView 공부  (0) 2021.07.31
    [UI] 이미지 관련  (0) 2021.07.28

    댓글

Designed by Tistory.