layered

11. 이벤트(Event) 본문

안드로이드/안드로이드 앱 프로그래밍

11. 이벤트(Event)

스윗푸들 2023. 4. 14. 12:14

개념


버튼을 클릭하거나 화면을 터치하게 되면 이벤트라는 것이 만들어지는데, 이는 화면의 어느 부분에 어떻게 동작이 일어났는지에 대한 정보를 담게 된다.

 

이 이벤트를 가로채는 방법은 크게 두 가지가 있다.

제대로 이해한 게 맞는지 모르겠다...

 

1 콜백 메서드 재정의


 

View 클래스는 onTouchEvent()처럼 안드로이드 프레임워크에 의해 호출되는 콜백 메서드를 기본적으로 가지고 있다.

그리고 이를 이용해 이벤트를 가로채려면 클래스를 상속해서 메서드를 재정의해 줘야 한다.

 

하지만 이벤트 하나 처리하자고 매번 상속하고 재정의하는 것이 번거롭기 때문에, 안드로이드에서는 2와 같은 이벤트 리스너 기능을 지원한다. 뷰 클래스를 어쩔 수 없이 상속해야 하는 경우엔 이벤트 핸들러라는 것을 별도로 지원한다!

 

메모

음... Activity에도 onTouchEvent()가 있긴 한데 쪼금 다르다...

Activity 클래스는 이미 한 번 상속이 된 거라서 그냥 내부에서 바로 재정의해 주면 된다.

다만 이건 View 클래스와는 다르게 Activity에 들어 있는 모든 뷰들에게 적용이 된다.

 

public boolean onTouchEvent(MotionEvent event) {
        return true;
}

 

여기서 true는 이벤트를 처리했으며 여기에서 중단해야 한다는 것을 의미하고, false는 이벤트가 제대로 처리되지 않아서(또는 소모되지 않아서) 다른 뷰에게 넘겨야 한다는 것을 의미한다.

조금 유연하게 생각하자면, 이 이벤트를 다른 곳에서 처리하고 싶을 때 일부러 false를 쓸 수도 있다.

 

2 이벤트 리스너(EventListener)


위임 모델(Delegation Model)이라고도 하는데, 이벤트를 객체에 전달해 이후의 처리 과정을 위임한다는 의미이다.

이렇게 되면 객체마다 개별적으로 이벤트를 처리할 수 있어 객체 지향적인 코드를 만들 수 있다.

 

먼저, View 클래스 내에는 각각의 이벤트를 처리하기 위한 [리스너 인터페이스: 콜백 메서드]가 있다.

 

View.OnTouchListner: boolean onTouch(View v, MotionEvent event)

View.OnKeyListner: boolean onKey(View v, int keyCode, KeyEvent, event)

View.OnClickListner: void onClick(View v)

등등

 

안에 들어 있는 콜백 메서드는 동작이 발생할 때 안드로이드 프레임워크에 의해 호출된다.

따라서 우선적으로 setOn...Listener를 통해 리스너를 설정해 줘야 한다.

 

view.setOnTouchListener(new View.OnTouchListener() {
      @Override
      public boolean onTouch(View view, MotionEvent motionEvent) {
            return true;
      }
});

 

이런 느낌!

일시적으로 사용하는 것이기 때문에 인터페이스를 익명 객체로 구현해서 전달해 주었다.

 

메모

이전에 XML 코드에서 onClick 속성에 메서드 이름을 지정해 준 것과는 비슷하지만 조금 다르다.

onClick 속성은 하나의 메서드만 사용할 수 있지만 OnClickListener는 인터페이스이기 때문에 원하는 만큼 구현할 수 있다.

간단한 이벤트만 처리할 때에는 둘의 차이가 거의 없지만, 이벤트가 복잡해지고 다양해지면 OnClickListener를 사용하는 것이 훨씬 유연하다.

뿐만 아니라 XML의 역할은 레이아웃을 구상하는 것이라는 점에서, 이벤트를 처리하는 자바 코드는 자바 부분에 쓰는 것이 알맞다.

 

MotionEvent


상속 관계

 

public final class MotionEvent extends InputEvent implements Parcelable

 

동작에 대한 정보를 담고 있는 클래스로,  ACTION일련의 AXIS 상수 등으로 정보를 표현한다.

여기서 ACTION은 터치를 하거나 떼면서 일어나는 상태 변화를,  AXIS는 위치 및 기타 이동 속성을 의미한다.

고정된 것이므로 static final로 선언되어 MotionEvent.ACTION_UP과 같이 상태에 접근하게 된다.

 

예를 들면 사용자가 화면에 처음 터치를 할 때 상태는 ACTION_DOWN이 된다. 그 외에 AXIS_PRESSURE, AXIS_SIZE 등의 정보도 있을 것이다. 시스템은 이것들을 모아 터치 이벤트와 함께 뷰에게 전달하게 된다.

 

ACTION과 AXIS는 다음과 같이 번호가 할당되어 있다.

 

ACTION_DOWN = 0

ACTION_UP = 1

ACTION_MOVE = 2

AXIS_X = 0AXIS_PRESSURE = 2

등등

 

화면에 어떤 이벤트가 발생할 때 그 움직임을 추적하기 위한 개념을 포인터라고 한다. 손을 떼는 것처럼 움직임이 사라지지 않는 한 포인터는 그 활성 상태를 유지한다.

 

멀티 터치와 같이 여러 개의 움직임이 나타나는 경우엔 각각의 포인터에 발생한 순서대로 id를 부여하며, 인덱스를 이용해 포인터들을 관리한다.

id는 고유의 값이기 때문에 변하지 않지만, 인덱스는 포인터가 생성하고 소멸함에 따라 유동적으로 변한다.

 

메서드

 

메서드 설명
public float getX();
public float getY();
이벤트가 발생한 위치의 좌표 반환
public int getAction()

action의 번호를 반환
public static String actionToString(int action)
action의 번호가 주어지면 문자열로 반환
   
   

 

메모

뭔가 이해하는 것도 설명하는 것도 조금 어려운 것 같다...

사용자가 터치를 하게 되면 그 시점부터 내부적으로는 MotionEvent라는 클래스가 동작을 관리한다.

이 클래스는 포인터를 이용해 사용자의 손가락(?)을 따라다니면서 정보를 저장하게 된다.

정보는 꾹 누른다, 뗀다 등의 상태라든가 방향, 좌표, 압력 등을 포함하며, 고정된 것이기 때문에 클래스 내에서는 static final 변수로 설정되어 있다.

사용자가 손가락을 뗌으로써 동작을 멈추게 되면 포인터도 사라진다.

 

처음에 MotionEvent 문서를 읽을 때에는 axis를 제대로 이해하지 못했다. 번역기를 돌렸더니 축값이라고 나와서 조금 의아하긴 했지만 그렇구나 했는데, 읽다 보니까 여러 상수가 AXIS_로 시작하는 것을 보고 일종의 분류를 의미한다는 것을 깨달았다.

 

상태는 변수로 접근하면서 왜 좌표는 메서드로 하지? 싶었는데, 좌표를 담는 변수를 설정하면 움직임에 따라 수없이 갱신을 해야 하기 때문인 것 같다.

 

멀티 터치의 경우는 포인터가 여러 개이고, 발생한 순서에 따라 id가 부여되며 인덱스로 관리한다.

id랑 인덱스를 따로 두는 이유는 배열처럼 관리하기 위한 것 같다. 인덱스는 연속적인 값이지만 id는 그렇지 않으니까.

그럼 굳이 id가 필요없는 게 아닌가?... 싶지만 특정 포인터를 찾으려면 id로 접근할 수밖에 없다.

그래서 MotionEvent 클래스 내에는 포인터를 id로 찾는 함수도 있고 인덱스로 찾는 함수도 있다.

좌표를 알려주는 getX()나 getY() 같은 경우는 id가 아니라 인덱스로 접근하게 된다.

 

콜백 메서드 재정의와 이벤트 리스너의 비교


public class MainActivity extends AppCompatActivity {
    TextView textview;
    View view;
    String[] motionPrompt;

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

        textview = findViewById(R.id.textview);
        view = findViewById(R.id.view);
        motionPrompt = new String[] { MotionEvent.actionToString(0), MotionEvent.actionToString(1), MotionEvent.actionToString(2) };

        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int action = motionEvent.getAction();
                println(motionPrompt[action] + " onTouch() 호출됨");
                return true;
            }
        });
    }

    public boolean onTouchEvent(MotionEvent event){
        int action = event.getAction();
        int y = (int) event.getY();
        String s = (y <= 740 ? "view 1" : "view 2");

        println(motionPrompt[action] + s + " onTouchEvent() 호출됨");
        return true;
    }

    public void println(String data) {
        textview.append(data + '\n');
    }
}

 

간단하게 설명하자면 일단 XML 상에는 연두색 View 1과 하늘색 View 2가 있다.

그리고 Activity에서 OnTouchEvent() 메서드를 재정의하고, View1에는 리스너를 따로 설정해 주었다.

 

사실 View 클래스를 확장하는 OnTouchEvent()를 구현하고 싶었는데 커스텀 뷰라는 새로운 개념이 나와서 다음에 하기로 했다...

 

 

첫 번째는 위 코드를 그대로 실행한 결과이다. View 1에서 발생한 터치 이벤트는 리스너에 의해 실행되고, 리스너를 별도로 지정하지 않은 View 2에는 Activity에서 재정의한 OnTouchEvent()가 실행되는 것을 볼 수 있다.

View 1에는 OnTouchEvent()가 실행되지 않는 이유는 뷰 계층 구조로 인해 이벤트 처리의 우선순위가 리스너에게 있기 때문인데, 리스너에서는 true를 반환하므로 OnTouchEvent()까지 넘어가지 않고 처리가 완료된다.

 

두 번째는 OnTouchEvent()에서 false를 반환한다. 그러면 이벤트가 제대로 처리되지 않아 다음으로 넘기게 되므로, View 1에도 OnTouchEvent()가 호출된 것을 볼 수 있다.

 

이벤트를 넘겼는데 텍스트는 왜 출력되는 걸까 싶어서 코드를 좀 수정한 게 세 번째이다.

 

if (action == MotionEvent.ACTION_DOWN) {
      println(motionPrompt[action] + " onTouch() 호출됨");
      return true;
}
else {
     println(motionPrompt[action] + " onTouch() 호출됨");
     return false;
}

 

이 코드에서는 터치를 했을 때에는 true를 반환하고, 그 외에는 false를 반환한다.

이제 세 번째 결과를 보면 ACTION_DOWN에 대해서는 OnTouch() 이후 더 이상 어떤 호출도 없는 것을 볼 수 있는데, true를 반환함으로써 이벤트를 잘 처리했기 때문이다.

하지만 나머지에 해당하는 ACTION_UP은 false를 반환하기 때문에 이벤트를 넘기고, 그 이벤트에 대해서 다음에 있는 OnTouchEvent()가 호출된 것이다.

텍스트가 그대로 출력되는 것은 true/false를 반환하기 이전까지의 코드는 일단 실행이 되기 때문이다!

 

 

https://developer.android.com/develop/ui/views/touch-and-input/input-events

 

Input events overview  |  Android Developers

Input events overview Stay organized with collections Save and categorize content based on your preferences. On Android, there's more than one way to intercept the events from a user's interaction with your application. When considering events within your

developer.android.com

 

https://developer.android.com/training/gestures/detector?hl=ko

 

일반 동작 인식  |  Android 개발자  |  Android Developers

일반 동작 인식 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. '터치 동작'은 사용자가 터치스크린에 손가락을 한 개 이상 올려놓을 때 발생하며, 애플리케

developer.android.com

 

https://developer.android.com/reference/android/view/MotionEvent.html#getActionButton()

 

MotionEvent  |  Android Developers

 

developer.android.com