[Android] 안드로이드 정리 (3) - 이벤트 처리
Android

[Android] 안드로이드 정리 (3) - 이벤트 처리

728x90

 

 

출처 : Do it! 안드로이드 앱 프로그래밍 / 정재곤

 

이벤트 처리 방식

버튼 태그에 onClick 속성을 추가하면 버튼을 클릭했을 때 발생하는 이벤트를 처리할 수 있다. 다른 방식으로는 XML이 아니라 소스 코드에서 setOnClickListener() 메소드를 이용해 클릭 이벤트를 처리하는 방식이다.

 이와 같은 이벤트 처리 방식은 화면에서 발생하는 이벤트를 버튼과 같은 위젯 객체에 전달한 후 그 이후의 처리 과정을 버튼에 위임한다고 해서 '위임 모델'이라고 부른다. 위임 모델은 각각의 이벤트를 처리할 수 있는 리스너(Listener) 인터페이스를 등록할 수 있어야 한다. 다음은 대표적인 이벤트 처리 메소드이다.

boolean onTouchEvent (MotionEvent event)
boolean onKeyDown (int keyCode, KeyEvent event)
boolean onKeyUp (int keyCode, KeyEvent event)

 이벤트가 발생하면 파라미터로 필요한 정보들이 전달되고 필요한 작업이 수행된다.

 그런데 이 메소드들은 뷰를 상속하여 새로운 클래스를 정의할 때 재정의할 수 있다. 예를 들어, Button 클래스를 상속하여 MyButton과 같은 새로운 클래스를 만들면 이 메소드들을 재정의할 수 있다.

 

터치 이벤트 처리하기

터치 이벤트가 어떻게 동작하는지 살펴보기 위해 아래의 그림과 같이 XML을 작성한 뒤, 소스 코드를 추가한다.

package com.example.project00_java;

import androidx.appcompat.app.AppCompatActivity;

import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    TextView textView;

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

        textView = findViewById(R.id.textView3);

        View view = findViewById(R.id.view);
        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int action = motionEvent.getAction();

                float curX = motionEvent.getX();
                float curY = motionEvent.getY();

                if (action == MotionEvent.ACTION_DOWN) {
                    println("손가락 눌림 : " + curX + ", " + curY);
                } else if (action == MotionEvent.ACTION_MOVE) {
                    println("손가락 움직임 : " + curX + ", " + curY);
                } else if (action == MotionEvent.ACTION_UP) {
                    println("손가락 뗌 : " + curX + ", " + curY);
                }

                return true;
            }
        });
    }

    public void println(String data) {
        textView.append(data + "\n");
    }
}

 

 

 화면 위쪽에 배치한 View를 찾아 setOnTouchListener() 메소드를 호출하여 리스너를 등록한다. 이 메소드를 호출할 때 파라미터로 OnTouchListener 객체를 생성하면서 전달한다. 그러면 뷰가 터치되었을 때 이 리스너 객체의 onTouch() 메소드가 자동으로 호출된다.

 onTouch() 메소드에는 MotionEvent가 파라미터로 전달되는 데, 이 객체에는 액션 정보나 터치한 곳의 좌표 등이 들어 있다. getAction() 메소드를 호출하면 손가락이 눌렸는지, 눌린 상태로 움직이는지, 손가락이 떼졌는지를 알 수 있다.

MotionEvent.ACTION_DOWN : 손가락이 눌렸을 때
MotionEvent.ACTION_MOVE : 손가락이 눌린 상태로 움직일 때
MotionEvent.ACTION_UP : 손가락이 떼졌을 때

 

 

제스처 이벤트 처리하기

제스처 이벤트는 터치 이벤트 중에서 스크롤 등을 구혈한 후 알려주는 이벤트다. 제스처 이벤트를 처리해주는 클래스는 GestureDetector이며, 이 객체를 만들고 터치 이벤트를 전달하면 GestureDetector 객체에서 각 상황에 맞는 메소드를 호출한다. 

package com.example.project00_java;

import androidx.appcompat.app.AppCompatActivity;

import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    TextView textView;
    GestureDetector detector;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_menu);

        textView = findViewById(R.id.textView3);

        View view = findViewById(R.id.view);
        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int action = motionEvent.getAction();

                float curX = motionEvent.getX();
                float curY = motionEvent.getY();

                if (action == MotionEvent.ACTION_DOWN) {
                    println("손가락 눌림 : " + curX + ", " + curY);
                } else if (action == MotionEvent.ACTION_MOVE) {
                    println("손가락 움직임 : " + curX + ", " + curY);
                } else if (action == MotionEvent.ACTION_UP) {
                    println("손가락 뗌 : " + curX + ", " + curY);
                }

                return true;
            }
        });

        detector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) { // 아주 살짝 터치
                println("onDown() 호출");
                return false;
            }

            @Override
            public void onShowPress(MotionEvent e) {
                println("onShowPress 호출");
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                println("onSingleTapUp 호출");
                return false;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                println("onScroll 호출");
                return false;
            }

            @Override
            public void onLongPress(MotionEvent e) {
                println("onLongPress 호출");
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                println("onFling 호출");
                return false;
            }
        });

        View view2 = findViewById(R.id.view2);

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

    public void println(String data) {
        textView.append(data + "\n");
    }
}
  • onDown : 터치했을 때
  • onShowPress : onDown보다 조금 더 길게 터치했을 때
  • onSingleTapUp : 터치가 끝날 때
  • onScroll : 스크롤 할 때
  • onLongPress : 길게 터치했을 때
  • onFling : 스크롤과 비슷하지만 손가락을 튕길 때, 빠른 속도로 스크롤을 할 때

 

 

키 이벤트 처리하기

키 입력은 onKeyDown() 메소드를 재정의하여 처리할 수 있다. 키 입력 이벤트는 하드웨어 키보드나 소프트 키패드에 상관없이 동일한 이벤트로 전달되며 시스템 버튼 중의 하나인 [BACK] 버튼도 이 이벤트로 처리할 수 있다. 시스템 버튼은 단말 아래쪽에 보이는 버튼으로 앱과 관계없이 단말에 의해 동작하며 [BACK], [HOME], [Recent Apps] 버튼이 있다. 

boolean onKeyDown (int keyCode, KeyEvent event)
boolean onKey (View v, int keyCode, KeyEvent event)

onKey() 메소드는 뷰의 OnKeyListener 인터페이스를 구현할 때 사용된다. keyCode 정수 값으로 구분할 수 있는 대표적인 키 값으로는 KEYCODE_BACK (뒤로 가기 버튼), KEYCODE_VOLUME_UP (소리 크기 증가 버튼), KEYCODE_CALL (통화 버튼) 등이 있다. 

아래의 코드는 [BACK] 버튼이 눌렸을 때 토스트로 알림을 띄워주는 코드다.

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        Toast.makeText(this, "back 버튼 클릭", Toast.LENGTH_LONG).show();
        return true;
    }
    return false;
}

 

 

 

단말 방향을 전환했을 때 이벤트 처리하기

단말의 방향이 바뀌었을 때는 가로와 세로 화면의 비율에 따라 화면이 다시 보이게 된다. 다시 말하면 XML 레이아웃이 다르게 보여야 한다는 것이다. 이 때문에 액티비티는 메모리에서 없어졌다가 다시 만들어진다. 따라서 단말의 방향이 바뀌었을 때 세로 방향의 XML 레이아웃과 가로 방향의 XML 레이아웃을 따로 만들어 둘 필요가 있다. 

가로 방향의 XML 레이아웃은 보통 app/res/layout-land에 저장을 한다. layout-land 폴더를 생성하기 위해 res 폴더에서 New -> Android Resource Directory를 선택해준다. 그 후 폴더 이름을 layout-land로 지정한 뒤 OK 버튼을 눌러주자. (layout-land 폴더 이름은 미리 지정된 것이기 때문에 다른 이름을 사용하면 안된다.)

 그런데 폴더를 만들었음에도 화면에서 보이지 않는다. 그 이유는 왼쪽 프로젝트 창은 실제 폴더나 파일을 보여주는 것이 아니라 필요한 정보만 정리해서 보여주기 때문이다. 왼쪽 프로젝트 창 상단에서 [Project] 탭을 선택하면 다음과 같이 새로 만든 폴더를 확인할 수 있다.

이제 layout 폴더의 activity_main.xml 파일을 열고 가운데에 '세로 방향'이라고 TextView를 이용해 적어주자. 그 후, activity_main.xml 파일을 복사해서 layout-land 폴더에 넣어준 뒤, TextView를 '가로 방향'이라고 수정해준다.

이 때 한 가지 문제가 있다. 단말의 방향이 바뀔 때 액티비티가 메모리에서 없어졌다가 새로 만들어진다는 점이다. 이 경우에 액티비티 안에 선언해 두었던 변수 값이 사라지므로 변수의 값을 저장했다가 다시 복원하는 방법이 있어야 한다. 안드로이드에서는 이런 문제를 해결할 수 있도록 onSaveInstanceState 콜백 메소드가 제공된다. 이 메소드는 액티비티가 종료되기 전의 상태를 저장한다. 그리고 이때 저장한 상태는 oncreate() 메소드가 호출될 때 전달되는 번들 객체로 복원할 수 있다.

 

 

이렇게 시스템이 액티비티를 없앴다가 다시 만들어주는 이유는 가로 방향일 때의 액티비티와 세로 방향일 때의 액티비티가 서로 다를 수 있기 때문이다. 하지만 액티비티는 바뀌지 않고 단순히 화면에 보이는 레이아웃만 바꾸고 싶다면 액티비티를 굳이 없앴다가 다시 만들 필요가 없다.

기본적으로 단말의 방향 전환은 내부 센서에 의해 방향이 바뀌는 시점을 알 수 있다. 단말의 방향이 바뀌는 것을 앱에서 이벤트로 전달받도록 하고 액티비티는 그대로 유지하는 방법을 사용하려면 먼저 매니페스트에 액티비티를 등록할 때 configChanges 속성을 설정해야 한다.

<activity
    android:name=".MainActivity"
    android:configChanges="orientation|screenSize|keyboardHidden">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

그 후, MainActivity에서 onConfigurationChanged() 메소드를 재정의 해준다.

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        showToast("방향 : LANDSCAPE");
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
        showToast("방향 : PORTRAIT");
    }
}
728x90