1. 안드로이드의 이벤트 처리 방식
GUI(Graphic User Interface)를 제공하는 프로그램은 화면과 사용자간의 상호작용에 대해 처리하는 방식을 내부적으로 정의하고 있다. 이 상호작용을 이벤트라고 부르는데, 이벤트에는 터치 이벤트(Touch Event), 클릭 이벤트(Click Event), 키 입력(Key Event) 등이 있다.
크게 안드로이드에서 이벤트를 처리하는 방식은 2가지로 나뉜다.
1) 델리게이션 모델(Delegation Model)
- 레이아웃에 정의된 뷰 객체에 발생하는 이벤트를 처리하는 모델. 위임모델 이라고도 한다.
기본적인 구조는 이벤트 소스(뷰 객체)와 이벤트 핸들러(이벤트 처리방식이 정의된 객체)를 리스너로 연결하는 구조이다. 이 구조는 이벤트를 뷰 객체에 전달한 후 이후의 처리과정을 뷰 객체에 위임한다. 이렇게 하면 각각의 뷰마다 이벤트 처리 루틴이 할당되어 이벤트 루프에서 받아 처리할 때보다 과정이 간소화되고 코드가 더욱 객체지향적이 된다.
2) 하이어라키 모델(Hierachy Model)
- 액티비티에 발생하는 이벤트를 처리하는 모델
델리게이션 모델처럼 리스너를 사용하는 구조가 아니다. 특정 액티비티 내에서 터치 이벤트 등이 발생하면 자동으로 호출되는 콜백 메소드만 재정의 해주면 된다.
2. 이벤트 종류
속성 | 설명 |
---|---|
터치 이벤트 | 화면을 손가락으로 터치 |
키 이벤트 | 키패트, 하드웨어 버튼을 누름 |
제스처 이벤트 | 터치 이벤트 도중 일정 패턴의 움직임 |
포커스 | 뷰 객체가 포커스를 받거나 잃음 |
화면 방향 변경 | 화변 방향(가로,세로)가 변경 |
안드로이드에서 자주 사용하는 이벤트는 터치 이벤트, 키 이벤트이며 이벤트가 발생되면 다음의 메소드가 자동으로 호출된다.
boolean onTouchEvent(MotionEvent event)
boolean onKey(View v, int keyCode, KeyEvent event)
boolean onKeyUp(int keyCode, KeyEvent event)
boolean onKeyDown(int keyCode, KeyEvent event)
특정 뷰 객체에서 위 메소드들을 사용하려면 뷰를 상속하는 클래스를 따로 만들어 메소드를 재정의하는 방법, 혹은 리스너 객체를 설정하는 방식이 있다. 따라서 특정 뷰 객체에서 위 메소드들을 사용하려면 델리게이션 모델로 리스너를 달아주는 것이 간편한 사용법이다.
View.OnTouchListener : boolean onTouch(View v, MotionEvent event)
View.OnKeyListener : boolean onKey(View v, int keyCode, KeyEvent event)
View.OnClickListener : void OnClick(View v)
View.OnFocusChangeListener : void onFocusChange(View v, boolean hasFocus)
실제로 예제를 만들어보자.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/view1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#ffff0000"/>
<View
android:id="@+id/view2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#ff00ff00"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ScrollView>
</LinearLayout>
뷰 2개와 텍스트뷰 하나를 만들었다. 각각의 뷰는 리스너 객체를 달아 터치 이벤트를 처리하기 위함이고 텍스트뷰는 우리가 이벤트 처리를 눈으로 확인하기 위해 만든 것이다.
package io.swnomad.sampleevent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
View view1 = findViewById(R.id.view1);
View view2 = findViewById(R.id.view2);
//view1 객체에 터치 이벤트 발생 시 처리하는 코드
view1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction(); // 액션 정보 획득
float curX = event.getX(); // X좌표
float curY = event.getY(); // Y좌표
if(action==MotionEvent.ACTION_DOWN){// 화면 누르기
textView.append("[view1] 손가락 눌림: "+curX+", "+curY+"\n");
}else if(action==MotionEvent.ACTION_MOVE){// 움직임
textView.append("[view1] 손가락 움직임: "+curX+", "+curY+"\n");
}else if(action==MotionEvent.ACTION_UP){// 화면에서 손 떼기
textView.append("[view1] 손가락 떼짐: "+curX+", "+curY+"\n");
}
return true;
}
});
//view2 객체에 터치 이벤트 발생 시 처리하는 코드
view2.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction(); // 액션 정보 획득
float curX = event.getX(); // X좌표
float curY = event.getY(); // Y좌표
if(action==MotionEvent.ACTION_DOWN){// 화면 누르기
textView.append("[view2] 손가락 눌림: "+curX+", "+curY+"\n");
}else if(action==MotionEvent.ACTION_MOVE){// 움직임
textView.append("[view2] 손가락 움직임: "+curX+", "+curY+"\n");
}else if(action==MotionEvent.ACTION_UP){// 화면에서 손 떼기
textView.append("[view2] 손가락 떼짐: "+curX+", "+curY+"\n");
}
return true;
}
});
}
}
3. 제스처(Gesture) 이벤트
터치 이벤트 중에 발생하는 일정한 패턴을 가지는 움직임을 제스처(Gesture)라고 한다. 대표적으로 화면 스크롤 등의 움직임이 있다. 제스처 이벤트는 터치 이벤트를 받은 후 추가적인 확인을 거쳐 만들어지며, 일반적인 터치 이벤트보다 간단하게 처리할 수 있다.
다음은 제스처 이벤트를 처리할 수 있는 메소드들이다.
메소드 | 이벤트 유형 |
---|---|
onDown() | 화면이 눌림 |
onShowPress() | 화면이 눌렸다 떼어짐 |
onSingleTapUp() | 화면이 한 손가락으로 눌렸다 떼어짐 |
onSingleTapConfirmed() | 화면이 한 손가락으로 눌림 |
onDoubleTap() | 화면이 두 손가락으로 눌림 |
onScroll() | 화면이 눌린 채 일정 속도와 방향으로 움직임 |
onFling() | 화면이 눌린 채 가속도를 붙여 손가락을 움직였다 뗌 |
onLongPress() | 화면이 오랫동안 눌림 |
제스처 이벤트를 처리하려면 GestureDetector 객체를 만들고 여기에 터치 이벤트를 전달해주면 된다. 그러면 제스처 디텍터가 제스처가 일어나면 이를 감지하고 적당한 메소드를 호출하여 이벤트를 처리해준다.
예제를 보면서 이해해보자.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:background="#ff0000ff"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ScrollView>
</LinearLayout>
package io.swnomad.sampleevent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
View view;
TextView textView;
GestureDetector detector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
//GestureDetector 객체 생성하고 메소드 재정의
detector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
textView.append("\nonDown() 호출됨");
return true;
}
@Override
public void onShowPress(MotionEvent e) {
textView.append("\nonShowPress() 호출됨");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
textView.append("\nonSingleTapUp() 호출됨");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
textView.append("\nonScroll 호출됨 : (" + distanceX+", "+distanceY+")");
return true;
}
@Override
public void onLongPress(MotionEvent e) {
textView.append("\nonLongPress() 호출됨");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
textView.append("\nonFling() 호출됨 : (" + velocityX + ", " + velocityY+")");
return true;
}
});
view = findViewById(R.id.view);
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
detector.onTouchEvent(event); //GestureDetector 객체의 onTouchEvent() 메소드 호출하면서 MotionEvent 객체 전달
return true;
}
});
}
}
제스처 이벤트 중에서 대표적인 것이 스크롤(Scroll)과 플링(Fling)이다. 스크롤은 일정한 속도의 스크롤이고 플링은 빠른 속도의 스크롤을 의미한다. 그리고 각각의 제스쳐가 발생했을 때 호출되는 메소드에는 터치의 위치와 속도가 각각 전달된다.
4. 키(Key) 이벤트
키 입력 이벤트는 아래의 메소드들을 재정의하여 처리할 수 있다.
boolean onKey(View v, int keyCode, KeyEvent event) // onKeyListener 리스너를 설정할 때 사용
boolean onKeyDown(int keyCode, KeyEvent event) //액티비티 내에서 메소드를 재정의하여 사용
메소드로 전달되는 파라미터 중에서 keyCode 는 키를 구별할 때 사용한다. 그리고 KeyEvent는 키 입력에 대한 정보를 가지고 있는 객체이다.
아래 링크를 따라가면 모든 keyCode에 대한 내용이 나와있다.
예제로 시스템의 '뒤로가기' 버튼이 눌렸을 때 이벤트를 처리하는 앱을 만들어보자.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="뒤로가기 버튼을 눌러보세요"
android:textSize="24dp"
android:textStyle="bold"
android:layout_marginTop="350dp"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
package io.swnomad.sampleevent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 시스템 [BACK] 버튼이 눌린 경우 토스트 메시지 보여주기
if(keyCode==KeyEvent.KEYCODE_BACK){
Toast.makeText(this, "뒤로가기 버튼이 눌렸습니다", Toast.LENGTH_LONG).show();
}
return false;
}
}
5. 포커스(Focus) 이벤트
포커스란 화면상의 어떠한 뷰 객체가 키 입력 이벤트를 받는지를 나타내는 것이다. 예를 들면 붙여넣기를 한다던지, 키보드로 텍스트 입력을 한다던지 할 때 현재 포커스를 받고있는 뷰에서 그 입력이 일어난다는 것이다. 입력상자가 포커스를 받고 있으면 커서가 깜빡이는 것 또한 예시가 된다. 뷰 객체는 포커스를 받거나 잃거나 하는 변화가 일어나면 onFocusChange() 메소드가 호출된다. 예제를 통해 알아보자.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/editText1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="텍스트를 입력하세요"
android:textSize="20dp"
android:layout_margin="20dp"/>
<EditText
android:id="@+id/editText2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="텍스트를 입력하세요"
android:textSize="20dp"
android:layout_margin="20dp"/>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
android:textStyle="bold"
android:layout_margin="20dp"/>
</LinearLayout>
입력상자를 2개 만들고 텍스트뷰를 하나 만든다. 텍스트뷰에는 두 입력상자 중 어떠한 입력상자가 포커스를 받고있는지 나타내기 위한 것이다.
package io.swnomad.samplefocus;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
EditText editText1 = findViewById(R.id.editText1);
EditText editText2 = findViewById(R.id.editText2);
editText1.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if(hasFocus){
textView.setText("현재 포커스 : 입력상자1");
}
}
});
editText2.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if(hasFocus){
textView.setText("현재 포커스 : 입력상자2");
}
}
});
}
}
6. 단말 방향 전환 이벤트
안드로이드에서 휴대폰의 방향을 가로/세로 전환할 때는 단말 방향 전환 이벤트가 발생한다. 이 때 화면 레이아웃이 달리 보여야 하므로 메모리에서 액티비티를 없앤 후 새로 만든다.
단말의 방향이 세로일 때는 /res/layout 폴더의 xml 파일이, 단말의 방향이 가로일 때는 /res/layout-land 폴더의 xml 파일이 자동으로 사용된다. 따라서 각각의 폴더에 xml 레이아웃 파일을 만들어야한다. 만약 /res/layout-land 폴더가 존재하지 않으면 단말의 방향이 가로일때와 세로일 때 모두 /res/layout 폴더의 xml 파일이 사용된다.
먼저 예제 앱을 만들기 위해 각각의 폴더를 만들고 activity_main.xml 파일을 각각 만든다. /res/layout/activity_main.xml 파일의 TextView의 text는 "세로방향", /res/layout-land/activity_main.xml 파일의 TextView의 text는 "가로방향"으로 설정한다. 그리고 MainActivity.java 파일에는 다음 코드를 입력해준다.
package io.swnomad.sampleorientation;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, "onCreate() 호출됨", Toast.LENGTH_LONG).show();
}
}
onCreate() 메소드의 토스트 메시지는 액티비티 객체가 새로 메모리에 만들어지는 것을 확인하기 위함이다.
앱을 실행하면 다음과 같은 화면을 확인할 수 있다.
그런데 단말의 방향이 달라져도 액티비티가 다를 필요가 없는 경우에는 굳이 액티비티를 다시 만들 필요가 없다. 이 경우에는 매니페스트 파일의 <activity\> 태그에 다음의 속성을 추가한다.
android:configChanges="orientation|screenSize|keyboardHidden"
이 속성이 설정되면 시스템은 단말의 방향전환이 일어날 때 액티비티를 다시 만들지 않고 애플리케이션쪽으로 단말 방향 전환 이벤트가 발생했다고 알려주기만 한다. 그리고 onConfigurationChanged() 메소드를 자동으로 호출한다. 이 메소드안에서 개발자가 직접 처리하는 코드를 작성하면 된다.
위에서 만든 앱에서 MainActivity.java 클래스의 코드만 다음과 같이 바꿔준다.
package io.swnomad.sampleorientation;
import android.content.res.Configuration;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, "onCreate() 호출됨", Toast.LENGTH_LONG).show();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if(newConfig.orientation==Configuration.ORIENTATION_LANDSCAPE){
Toast.makeText(this, "가로방향", Toast.LENGTH_LONG).show();
}else if(newConfig.orientation==Configuration.ORIENTATION_PORTRAIT){
Toast.makeText(this, "세로방향", Toast.LENGTH_LONG).show();
}
}
}
이제는 액티비티가 다시 만들어지지 않기때문에 onCreate() 메소드가 새로 호출되지 않는다.
두 화면의 텍스트가 모두 "세로방향" 인걸로 미루어보아 액티비티를 새로 만들지 않은 것을 알 수 있다. 이 때에는 onConfigurationChanged() 메소드 안에서 레이아웃을 각각 따로 설정해주는 코드를 추가하면 된다.
setContentView(R.layout.activity_main);
이렇게 하면 단말의 방향이 세로일 때는 layout 폴더의 activity_main.xml 파일이, 가로일 때는 layout-land 폴더의 activity_main.xml 파일이 사용된다.
Tip. 화면의 방향을 고정시키고 싶다면?
매니페스트 파일의 <activity\> 태그에 screenOrientation 속성을 설정하면 된다.
android:screenOrientation="landscape"
android:screenOrientation="portrait"
'안드로이드' 카테고리의 다른 글
(안드로이드) 24 - 스낵바(SnackBar) (0) | 2020.05.18 |
---|---|
(안드로이드) 23 - 토스트(Toast) 메시지 (0) | 2020.05.18 |
(안드로이드) 21 - 위험권한 부여 (0) | 2020.05.18 |
(안드로이드) 20 - 브로드캐스트 수신자(Broadcast Receiver) (0) | 2020.05.18 |
(안드로이드) 19 - 서비스(Service) (0) | 2020.05.18 |