안드로이드는 두가지의 이벤트가 존재한다.
- 터치 이벤트 : 손가락으로 화면을 터치시 발생
- 키 이벤트 : 실제 버튼이나 소프트 키패드를 누를시 발생
안드로이드에서는 터치 이벤트를 쉽게 처리할 수 있도록 '클릭 이벤트'를 별도로 제공한다. 예를 들면 XML을 이용해 버튼에 onClick attribute에 처리기 함수를 등록하는것처럼 말이다. 버튼의 onClick은 코드적으로 setOnClickListener() 를 통해서 등록할 수 있다.
이러한 처리 방식을 "위임 모델(Delegation Model)"이라고 부른다. 위임모델은, 화면에서 발생하는 이벤트를 위젯객체에 전달한 후 이 처리과정을 위젯에 위임하는 구조이기 때문에 이렇게 부른다. 그리고 이 위임모델은 각각 이벤트를 처리할 수 있는 리스너 인터페이스를 등록할 수 있다. 안드로이드의 대표적인 이벤트는 아래와 같은 것들이 있다.
< 정리 >
- 위젯과 사용자 간의 상호작용이 필요
- 각각 이벤트를 처리할 수 있는 리스너 인터페이스를 등록한다.
속성 | 설명 |
touch event | 화면을 손가락으로 누르는 것을 의미 |
key event | 키패드나 하드웨어 버튼을 누를때 발생 |
gesture event | 터치 이벤트 중 스크롤과 같이 일정 패턴으로 구분되는 이벤트 |
focus | 뷰마다 순서대로 주어지는 포커스 |
change screen location | 화면의 방향이 가로와 세로로 바뀜으로서 생기는 이벤트 |
우선 이 포스트에서는 '터치이벤트'와 '제스처 이벤트'에 대해 알아보자.
Touch Event
터치 이벤트는 사용자가 손가락으로 화면을 터치할 때 발생하는 이벤트이다. 터치 이벤트 같은 경우 "setOnTouchListener()"메소드를 호출하여 리스너를 등록한다. 매개변수로는 앞에서도 말했듯이 이벤트를 처리할 수 있는 "인터페이스"를 등록해 주어야 하는데, 터치 이벤트는 "View.OnTouchListener" 인터페이스를 구현하여 넘겨준다. View.OnTouchListener인터페이스는 'OnTouch()'메소드를 구현해야한다. 화면을 터치했을때 자동적으로 OnTouch()메소드를 호출하게되는것이다.
OnTouch()메소드는 MotionEvent객체를 매개변수를 파라미터로 받게 된다. 이 객체는 아래와 같은 정보들을 담고있다.
- 액션정보 : getAction()메소드를 통해 확인할 수 있으며,
- 터치한 곳의 좌표(X,Y) : getX(), getY()메소드를 사용해서 얻을 수 있다.
getAction()메소드는 정수 자료형 값이 반환되는데, 이 값과 MotionEvent클래스에 정의된 상수 값을 비교하여 액션 상태를 알 수 있다. 상수의 종류는 아래와 같다.
상수 | 설명 |
MotionEvent.ACTION_DOWN | 손가락이 눌렀을때 |
MotionEvent.ACTION_MOVE | 손가락이 눌린 상태로 움직일 때 |
MotionEvent.ACTION_UP | 손가락이 떼졌을 때 |
예시로 작성한 Touch Listener 코드는 아래와 같다.
View view = findViewById(R.id.view);
view.setOnTouchListener(new View.OnTouchListener(){
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
int action = motionEvent.getAction();
float axisX = motionEvent.getX();
float axisY = motionEvent.getY();
if(action == MotionEvent.ACTION_DOWN){
println(new StringBuilder("Panel Pressed : ").append(axisX).append(axisY).toString());
}
else if(action == MotionEvent.ACTION_MOVE){
println(new StringBuilder("Panel Move : ").append(axisX).append(axisY).toString());
}
else if(action == MotionEvent.ACTION_UP){
println(new StringBuilder("Touch stopped : ").append(axisX).append(axisY).toString());
}
return true;
}
});
//메소드 선언
public void println(String data){
textView.append(data + "\n");
}
제스처 이벤트 처리하기
제스처 이벤트는 터치 이벤트 중 스크롤 등을 구별한 후 알려주는 이벤트이다. 제스처 이벤트를 처리해 주는 클래스는 "GestureDetector"클래스 이다. GestureDetector객체는 GestureDetector.OnGestureListener 인터페이스를 매개변수로 받아 인스턴스를 생성한다. OnGestureListener인터페이스로 구현해야하는 추상 메소드들은 아래 같다.
메소드 | 설명 |
onDown() | 일반적인 터치이다 |
onShowPress() | onDown()보다 길게 터치 |
onSingleTapUp() | 터치가 끝날때를 의미한다 |
onScroll() | 스크롤 제스쳐를 의미한다 |
onLongPress() | onShowPress()메소드보다 길게 터치 |
onFling() | 스크롤과 비슷하지만, 손가락으로 튕기는 모션 의미 |
제스처 이벤트도 TouchEvent처럼 setOnTouchListener() 메소드에 View.OnTouchListener()인터페이스를 넘겨서 이벤트 처리기를 등록한다. 다만 차이점이라면, TouchEvent는 순수히 OnTouch()메소드에 기능을 구현하였다면, GestureEvent는 OnTouch()메소드를 구현한다는 점은 동일하지만, GestureDetector객체에 "onTouchEvent(MotionEvent event)" 메소드를 사용하여 MotionEvent객체(OnTouch() 메소드의 매개변수로 받는 객체)를 넘긴 후, 생성된 GestureDetector객체에서 이벤트를 처리한다는 점에서 다른점을 보인다.
예시로 작성한 GestureEvent 처리기는 아래와 같다.
private GestureDetector detector;
// 중략
detector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent motionEvent) {
println("ondown() called");
return false;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
println("onShowPress() called");
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
println("onSingleTapUp() called");
return false;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
println((new StringBuilder("onScoll() called").append(v).append(v1)).toString());
return false;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
println("onLongPress() called");
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvents1, float v, float v1) {
println((new StringBuilder("onFling() callled").append(v).append(v1)).toString());
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;
}
});
우선적으로 이 코드를 실행시켜보면 아래와 같은 결과가 나오는것을 볼 수 있다.
무언가 이상하다. 분명, onFling, onScroll, onSingleTapUp 메소드들도 선언을 했는데 호출이 되지 않는다.
이벤트 처리기에서의 boolean return값이 미치는 영향
이 이유는, Document에서 찾아보면 알 수 있다.
https://developer.android.com/reference/android/view/GestureDetector.OnGestureListener?hl=ko
이게 무슨 소린지 살펴보면, 반환값에 따라 터치 이벤트를 소비했는지의 여부가 결정된다는 것이다. true를 반환하는 경우에는, 호출된 터치 이벤트의 후속 이벤트에도 터치 이벤트가 전달되는것을 의미한다고 한다. 하지만 반대로 false를 반환하게 되면, 다음 후속 이벤트에 터치 이벤트가 전달되지 않는다는 것이다.
위에서 예를 들면, onDown(), onShowPress(), onLongPress()메소드는 모두, 일반적인 터치와 연관이 되어있어 호출이 된것이다. 하지만 기본 호출의 후속 이벤트에 해당하는 onScroll(), onFlig(), onSingleTapUp() 같은 경우 기본 터치 이벤트에서 이벤트 전달이 되지 않기 때문에 호출이 되지 않는것이다.
onTouch() 메소드의 return false또한 영향을 준다. return false로 하게 되면, 다음 터치 이벤트로 이벤트가 넘어가지 않기 때문에, 일반 터치(onDown())메소드의 이벤트가 onScroll()에 넘어가지 않는것이다. 그렇기 때문에 웬만하면, 이벤트 처리기(OnTouchListener 의 onTouch(), OnGestureDetector()등등)에서는 이벤트간의 후속 전달을 위해 return 값을 true로 해주는 것이 좋다.
참고 : https://stackoverflow.com/questions/24287292/returning-true-and-false-in-ontouch
이 점을 고려해서 코드를 모두 return true로 바꾼 후 실행해 보면 아래와 같이 onScroll()과 onFling()이 호출되는것을 볼 수 있다.
이 포스트에서 사용한 실습 코드들은 아래에 있다
MainActivity.java
package com.example.sampleevent;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.text.method.ScrollingMovementMethod;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView textView;
private ScrollView scrollView;
private GestureDetector detector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView3);
detector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent motionEvent) {
println("ondown() called");
return true;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
println("onShowPress() called");
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
println("onSingleTapUp() called");
return true;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
println((new StringBuilder("onScoll() called").append(v).append(v1)).toString());
return true;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
println("onLongPress() called");
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvents1, float v, float v1) {
println((new StringBuilder("onFling() callled").append(v).append(v1)).toString());
return true;
}
});
View view2 = findViewById(R.id.view2);
view2.setOnTouchListener(new View.OnTouchListener(){
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
detector.onTouchEvent(motionEvent);
return true;
}
});
View view = findViewById(R.id.view);
view.setOnTouchListener(new View.OnTouchListener(){
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
int action = motionEvent.getAction();
float axisX = motionEvent.getX();
float axisY = motionEvent.getY();
if(action == MotionEvent.ACTION_DOWN){
println(new StringBuilder("Panel Pressed : ").append(axisX).append(axisY).toString());
}
else if(action == MotionEvent.ACTION_MOVE){
println(new StringBuilder("Panel Move : ").append(axisX).append(axisY).toString());
}
else if(action == MotionEvent.ACTION_UP){
println(new StringBuilder("Touch stopped : ").append(axisX).append(axisY).toString());
}
return true;
}
});
}
public void println(String data){
textView.append(data + "\n");
}
}
activity_main.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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#44CADC" />
<View
android:id="@+id/view2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#FF9800" />
<ScrollView
android:id="@+id/scrollview1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/app_name">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/textView3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical" />
</LinearLayout>
</ScrollView>
</LinearLayout>
'Android' 카테고리의 다른 글
[Android] 알림 대화상자 (0) | 2022.04.07 |
---|---|
[Android] onSaveInstanceState()로 액티비티 변수 보존하기 (0) | 2022.04.06 |
[Android] 예시 SMS 레이아웃 만들기 (0) | 2022.03.27 |
[Android] 텍스트를 클립보드에 복사하기 (0) | 2022.03.27 |
[Android] Table Layout (0) | 2022.03.26 |