Tistory View

Android Develop/Input

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

God Dangchy What should I do? 2019. 12. 23. 07:16

안드로이드에서 사용자가 Touch를 할 경우 그에 상응하는 동작을 필요에 따라 만들어 넣어야 한다. 이 Touch다루는 법을 전체적으로 다루려고 한다. 간단한 것은 어렵지 않게 만들 수 있지만, 때로는 상당히 복잡하여, 정확한 이해가 필요하기 때문에, 이렇게 차근차근 정리하기로 한다. 내용이 앞과 뒤가 서로 서로 엮여있는 방식이라 여러번 반복해서 읽어야 한다.

 

 

Touch를 이해하자

 

안드로이드의 View는 터치이벤트를 받을 수 있고, Activity도 터치이벤드를 받을 수 있다.

View에 이 이벤트를 처리하려면 2가지의 방법이 있는데, 한가지는 View가 기본적으로 가지고 있는 onTouchEvent를 오버라이드하는 것, 다른 한가지는 setOnTouchListener()함수를 통하여 인터페이스가 적용된 객체를 지정하는 방법이 있다.

커스텀뷰를 만들 경우는 이미 구글에서 만들어놓은 많은 View들은 그냥 사용하는 편이 좋기 때문에 onTouchEvent를 오버라이드해서 사용하면 좋을 것이고, 굳이 상속받아서 사용할 필요없이 setOnTouchListener를 사용하면 될 것이다. 이 상속을 받지 않고 사용할 수 있다는 장점( class만으로는 안되는 interface의 장점 ) 때문에 setOnTouchListener가 있다고 봐도 된다.

 

[이제 아래의 내용을 읽을 때, onTouch인지 onTouchEvent인지를 잘 구분하여 읽어야 한다]

 

 

실행순서

View.OnTouchListener를 지정한 경우 onTouch가 먼저실행된 후에 onTouchEvent가 실행되게 된다. 당연히 OnTouchListener가 없는 경우는 onTouchEvent만 호출되게 된다. 이후 자세히 설명하겠지만, OnTouchListener를 지정하여 사용할 경우 onTouchEvent를 호출할 것인지 말것인지를 onTouch에서 결정할 수가 있다.

 

 

 

 

사용자가 손가락을 화면에 대거나, 화면에서 떼거나, 화면에 닿은채로 움직일때마다 이 onTouch나 onTouchEvent가 때로는 두가지 모두 호출된다. 화면에 닿을 때만 호출되는 것이 아니라는 말이다. 따라서 어떤 동작이 전달되었는 지를 파악하여 처리를 해주면 된다.

방법1. onTouchEvent를 오버라이드해서 사용해보자.

View기본적으로 onTouchEvent라는 함수를 포함하고 있기 때문에, 이 View로 파생된 모든 View관련 객체들은 onTouchEvent를 사용할 수가 있다.

 

아래의 코드는 실제 터치관련 이벤트가 발생했을 때, 호출되는 코드이다. 위치에 따라 설명해야 하기에, 설명을 코드의 주석에 넣어두었으니 코드를 천천히 살펴보기 바란다.

class YourClass extends View {

    .
    .
    .


    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        Log.d( TAG, "onTouchEvent onView" );

        boolean ret = false;

        // 터치가 되고 있는 위치
        // 얼마나 움직이는지를 계산하려면 이 값을 계속 유지하여 계산해야 됨
        float x = event.getX();
        float y = event.getY();
        
        switch( event.getActionMasked() )
        {
            // 화면에 손가락이 닿음 // 모든 이벤트의 출발점
            case MotionEvent.ACTION_DOWN:
                Log.d( TAG, "onTouch Down ACTION_DOWN : (" + x +", " + y + ")" );

                // true
                // 다음에 발생할 후속이벤트가 필요하다.
                // 이부분이 제일중요하며, ACTION_UP이 발생할 때까지 
                // 이벤트가 발생하면 이 onTouchEvent가 호출 될 것이다.  
                ret = true; 
                break;

            // 화면에서 손가락을 땜 // 사실상 이벤트의 끝
            case MotionEvent.ACTION_UP:
                Log.d( TAG, "onTouch Down ACTION_UP: (" + x +", " + y + ")" );

                // click이 되었으니, OnClickListener를 호출
                performClick();

                // 다음에 발생할 후속이벤트가 필요하다.(하지만 이벤트의 끝이라 의미는 없다)
                ret = true;
                break;


            // 화면에 손가락이 닿은 채로 움직이고 있음(움직일때마다 호출됨)
            case MotionEvent.ACTION_MOVE:
                Log.d( TAG, "onTouch Down ACTION_MOVE: (" + x +", " + y + ")" );
                // 다음에 발생할 후속이벤트가 필요하다.
                // false를 리턴한다고 해도 다른 ACTION_MOVE나 ACTION_UP이 안오는 것은 아니다.
                // 실제 의미가 없는 리턴값이다.
                ret = true;
                break;
        }

        return ret;
    }
}

 

 

방법2. setOnTouchListener

이해를 위해 간단히 Activity와 달랑 View만 있는 코드를 만들고, View에 setOnTouchListener로 Activity로 지정한다.

(Activity대신 이름없는 Listener를 지정해도 된다.)

 

class YourActivity extends AppCompatActivity {
    .
    .
    .
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        .
        .
        // Listener를 지정한다.
        findViewById( R.id.vw_touch ).setOnTouchListener( this );
        .
        .
    }

    
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {

        boolean ret = false;

        // 여러뷰에서 터치이벤트가 발생할 수 있으니 분간해서 처리한다.
        // ~~~.setOnTouchListener( new View.OnTouchListener() {...} ) 식으로 처리되었다면,
        // 늘 해당 뷰에서만 호출될 것이기 때문에 분간할 필요가 없다.
        // 현재의 예는 Activity에서 여러 View Listener가 걸려있을 수 있기에 분간한다.
        if( view.getId() == R.id.vw_touch )
        {
            // 터치가 되고 있는 위치 // move나 up등을 계산하려면 이 값을 계속 유지하여 계산해야 됨
            float x = motionEvent.getX();
            float y = motionEvent.getY();

            switch( motionEvent.getActionMasked() )
            {
                // 화면에 손가락이 닿음 // 모든 이벤트의 출발점
                case MotionEvent.ACTION_DOWN:
                    Log.d( TAG, "onTouch Down ACTION_DOWN : (" + x +", " + y + ")" );

                    // 다음에 발생할 후속이벤트가 나는 필요가 없다.
                    // 하지만 onTouchEvent의 값에 따라 후속이벤트를 받을지 말지를 결정하게 된다.
                    // 만약 다른 기능이 있는 View(예:스크롤뷰)를 사용할 경우, false를 리턴해야한다.
                    // onTouchEvent를 호출하라가 되어 스크롤뷰의 기능이 그대로 동작하게 된다.
                    // false가 리턴되어도 스크롤뷰같은 View에서 값이 true로 값을 변경하게 된다.
                    // 따라서 이 onTouch도 이벤트를 받을 수 있게 된다.
                    // 만약 onTouchEvent등에서 false를 리턴하는 경우가 있는 데, 이는 해당 뷰가
                    // 이벤트가 필요없다는 뜻이 된다. 
                    ret = false;
                    break;

                // 화면에서 손가락을 땜 // 사실상 이벤트의 끝
                case MotionEvent.ACTION_UP:
                    Log.d( TAG, "onTouch Down ACTION_UP: (" + x +", " + y + ")" );

                    // click이 되었으니, OnClickListener를 호출
                    // onTouchEvent에서 view.performClick()이 실행되게 되기 때문에
                    // 주석으로 처리해 두었지만, true일 경우 대부분의 경우 따로 호출해야 한다. 
                    //view.performClick();
                    
                    // onTouchEvent를 호출하라. 스크롤뷰 등이 관련 처리를 해야한다.
                    ret = false;
                    break;


                // 화면에 손가락이 닿은 채로 움직이고 있음(움직일때마다 호출됨)
                case MotionEvent.ACTION_MOVE:
                    Log.d( TAG, "onTouch Down ACTION_MOVE: (" + x +", " + y + ")" );
                    // 다음에 발생할 후속이벤트가 필요하다.
                    // false를 리턴한다고 해도 다른 ACTION_MOVE나 ACTION_UP이 안오는 것은 아니다.
                    // onTouchEvent를 호출하라. 스크롤뷰 등이 관련 처리를 해야한다.
                    ret = true;
                    break;
            }

        }

        return ret;
    }

}

 

onTouch와 onTouchEvent의 리턴값 설정

기본적으로 리턴값의 의미는 여러의미를 갖는다.

true로 설정을 한다는 것은 (지금 도착한)이벤트에 "내가 관심이 있으니, 후속이벤트를 보내라"라는 의미와 "내가 (지금 도착한)이벤트를 처리했으니 이번 이벤트에 대한 추가 작업을 하지말아라"라는 두가지 의미를 갖는다.

false로 설정하면 당연히 true로 설정했을 경우와 반대되는 의미를 이야기한다. 이 정도로는 이해가 좀 어려우니 예를 들어 보겠다.

 

setOnTouchListener로 핸들러를 지정한 경우에 기본적으로 지정한 핸들러(onTouch함수)가 먼저 호출이 된 후에, View의 onTouchEvent가 호출이 된다.

 

지정한 핸들러 함수(onTouch)에 ACTION_DOWN이 도착을 했을 때

 

true를 리턴한 경우(onTouch의 리턴값이다)

View의 onTouchEvent함수는 호출되지 않는다.( 이번 이벤트에대한 추가작업을 하지말라 )

true를 리턴했으므로, 안드로이드 OS는 다음에 당연히 발생할 ACTION_MOVE와 ACTION_UP 등의 이벤트를 받으면 다시 onTouch를 호출해 줄 것이다.(후속이벤트를 보내라)

 

false를 리턴한 경우(onTouch의 리턴값이다)

View의 onTouchEvent함수가 호출이 된다.

 

false를 리턴했지만, onTouchEvent와 다른 작업에서 최종적으로 true가 되면, 다음의 관련 이벤트( ACTION_MOVE와 ACTION_UP등)가 다시 발생하면 다음번 onTouch도 호출되게 된다.

(onTouchEvent에서 true를 리턴하게된다는 의미가 onTouchEvent에서 다음의 관련이벤트가 필요하다는 뜻이다, 따라서 다음의 관련이벤트도착시에 false를 리턴했어도 onTouch가 호출되게 된다. ScrollView같은 경우 Scroll하려면 다음 이벤트가 필요하기에 )

 

false를 리턴했고, onTouch등의 다른 작업에서도 false가 되어 최종적으로 false가 되면, OS는 다음의 관련 이벤트( ACTION_MOVE와 ACTION_UP등)에 대해 onTouch도 onTouchEvent도 호출하지 않는다.

 

 

예를 ACTION_DOWN으로 든 이유가, 실제 ACTION_DOWN이 다음에 발생할 이벤트의 시작이기 때문에 이 때의 리턴값이 가장 중요하기에 ACTION_DOWN을 예로 들었다. ACTION_DOWN의 최종결과가 false인 경우 "아무것도 안하겠다"라는 의미가 되기때문에 이후에 발생할 이벤트들은 도착하지 않게 된다.

 

onTouch에서 ACTION_DOWN이외에 이벤트의 리턴값은 사실상 onTouchEvent를 호출할지 말지를 결정하는 역할만 한다고 봐도 무방하다.

onTouch에서 모든 리턴값을 true로 한다는 것은 "여기 onTouch에서 모든 것을 알아서 할테니 아무것도 하지마~"라는 의미가 된다.

 

 

setClickable() 트릭

onTouch를 사용하려하는 데, 상속한 뷰가 onTouchEvent에서 false를 리턴하게 된다면, 후속이벤트를 받을 수 없게 된다. 그렇다고 onTouch에서 true를 리턴하며 onTouchEvent가 호출되지 않아 상위 class의 기본작업이 수행되지 않게 된다.

이 때, setClickable(true)로 강제지정을 하면 click을 처리하기위해 최종 결과(후속이벤트를 받을지말지)가 true로 변경이 된다. click을 발생시키려면 ACTION_DOWN에서 ACTION_UP까지 이벤트가 필요하기 때문에 어쩔수없이 어딘가(1)에서 true가 되게 된다. 이렇게 하면 상속한 뷰의 기본작업과 onTouch를 동시에 처리할 수가 있다.

모든 상황에서 될지는 필자도 모르겠다.

만약 이 방법도 못쓴다면 dispatchTouchEvent을 오버라이드해야한다.

 

(1) : 필자가 소스를 확인한 바로는 View.onTouchEvent 소스에서 처리하고 있다. 하지만 이 것은 소스가 변경될 수 있어 확실히 여기다라고 단언할 수는 없다.

 

performClick()

안드로이드스튜디오를 사용하면 위의 코드에서 performClick를 호출해야된다라고 다음과 함수선언부분에 뭔가 자꾸 나온다.

performClick을 해야된다구~~~? 왜?

내용인 즉슨 click이 일어나는 상황일 경우 performClick을 호출하여 그에 상응하는 동작이 일어나게 해야한다는 뜻인데, 바로 그에 상응하는 동작을 뭔지 설명하려 한다.

뷰를 상속받아 만드는 데, 뷰의 기능중에 click기능을 만들어 넣는다면, 사용자에게 "Click이 되었습니다."라는 인지를 하도록해야한다. (꼭해야되나요? 네~)

사용자가 클릭을 했는 데, "난 클릭을 했는 데, 클릭이 된거야.. 안된거야.."라는 문제로 자꾸 클릭을 할 수가 있다. 이런 문제로 클릭이 된 경우, "클릭했어요~"라는 메세지를 전달해 주는 것인데, 이게 시각장애인에게는 정말 중요한 문제가 된다. 소리로 클릭된 것을 구분해야 되기 때문에, performClick을 해주지 않으면 소리가 나지 않아, 화면에 변화가 있어도 시각장애인은 인지할 수가 없다. (우리나라뿐아니라 유럽, 미국등에서 이거 안하면 불법이다~)

그래서 performClick을 따로 호출해줘야하며, performClick이 하는 역할은 click이 되었다는 소리를 내거나 하는 등에 일을 대신처리해 주게 된다. 그리고 onClick함수를 호출해주는 역할까지 수행하게 된다.

 

사용자가 화면에 손을 대고, 다른 위치로 죽~ 이동한 다음에 손을 때면 이걸 click이라고 생가하지 않는 데면,  performClick을 호출하지말고 그에 상응하는 코드도 수행해서는 안된다. 하지만, 수전증이나 손가락이 장애가 있어 심하게 흔들리는 경우까지 고려하여야하고, fling인지, scroll인지도 구분하여 처리해야 한다.

 

 

 

MotionEventCompat

다른 곳의 코드를 보면 MotionEventCompat을 사용하는 경우가 있는 데, 이는 22.1.0에서 추가 되었으나 26.1.0에서 다시 사용하지 않게 하고 있다. 그냥 원래 사용하던 방식으로 사용하면 된다.

 

요약

1. Activity도 터치이벤트를 받을 수 있다.(그렇게 만들어 놨다)

2. Touch이벤트를 받으려면 setOnTouchListener()를 사용하거나 onTouchEvent를 override하여 사용할 수 있는 2가지 방식이 있다.

3. onTouch와 onTouchEvent의 차이( onTouch vs onTouchEvent )

onTouch By setOnTouchListener(), A member of interface View.OnTouchListener
onTouchEvent Overrideable class member function of the view

기타

onTouch와 onTouchEvent는 dispatchTouchEvent함수에서 호출되기 때문에 dispatchTouchEvent를 Override하면 onTouch, onTouchEvent가 호출되는 것을 막아버릴 수도 있다.

넋두리

필자는 거의 모든 View를 상속받아서 쓰기 때문에 onTouchEvent를 더 많이 쓸 것 같다. 심지어는 아무런 코드를 추가하지 않고 상속받아 쓴다. 이름을 바꾸는 것만으로도 의미가 있다. 실제 상속받지 않고 완성한 앱은 아주 간단한것을 제외하고는 거의 없었다.

 

내가 신발, 구글직원도 아니고 C앙, 구글 문서만드는 거 멍멍이판이라 내가 이 걸 분석하고 정리하고 있는 게, 개 짜증난다. 최소 마이크로소프트만큼은 만들어 줘야 이해를 하지, 내가 왜 이걸 만들고 있어야 하는 건지... 진짜 구글보면 어후~, 그나마 꾸진거라도 있는 걸 다행이라고 생각할 정도니.. 아C

 

 

 

 

 

Replies
Reply Write