안드로이드 터치 이벤트 완벽 가이드: 발생 순서부터 처리 방법까지
안드로이드 앱 개발에서 사용자의 터치 입력을 올바르게 처리하는 것은 매우 중요합니다. 특히 복잡한 제스처나 커스텀 뷰를 구현할 때는 터치 이벤트의 발생 순서와 처리 메커니즘을 정확히 이해해야 합니다. 이 글에서는 안드로이드에서 화면 터치 시 발생하는 이벤트 순서와 처리 방법을 초보자도 이해하기 쉽게 설명하겠습니다.
목차
- 안드로이드 터치 이벤트란?
- 터치 이벤트의 발생 순서
- MotionEvent 객체 살펴보기
- 터치 이벤트 전달 과정
- onTouchEvent() 메서드 구현하기
- OnTouchListener 사용하기
- 멀티 터치 처리하기
- 제스처 감지기(GestureDetector) 활용하기
- 터치 이벤트 충돌 해결하기
- 실전 예제: 커스텀 터치 뷰 구현하기
#1. 안드로이드 터치 이벤트란?
안드로이드에서 터치 이벤트는 사용자가 화면을 터치했을 때 발생하는 입력 이벤트입니다. 이 이벤트는 MotionEvent
객체로 표현되며, 터치의 종류(누르기, 움직이기, 떼기 등)와 좌표 정보를 담고 있습니다.
public boolean onTouchEvent(MotionEvent event) {
// 여기서 터치 이벤트를 처리
return super.onTouchEvent(event);
}
터치 이벤트는 Android UI 시스템의 기본이 되는 요소로, 버튼 클릭부터 복잡한 제스처까지 모든 터치 기반 상호작용의 기반이 됩니다.
#2. 터치 이벤트의 발생 순서
안드로이드에서 화면을 터치할 때 발생하는 이벤트의 기본 순서는 다음과 같습니다:
- ACTION_DOWN: 사용자가 화면을 처음 터치했을 때 발생
- ACTION_MOVE: 터치한 상태에서 손가락을 움직일 때 발생 (여러 번 연속해서 발생할 수 있음)
- ACTION_UP: 사용자가 화면에서 손가락을 뗄 때 발생
이 기본 순서를 코드로 확인해보겠습니다:
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
Log.d("TouchEvent", "손가락이 화면에 닿았습니다. (ACTION_DOWN)")
return true // true를 반환하여 이후 이벤트도 받겠다고 선언
}
MotionEvent.ACTION_MOVE -> {
Log.d("TouchEvent", "손가락이 움직입니다. (ACTION_MOVE) x: ${event.x}, y: ${event.y}")
}
MotionEvent.ACTION_UP -> {
Log.d("TouchEvent", "손가락을 화면에서 뗐습니다. (ACTION_UP)")
}
}
return super.onTouchEvent(event)
}
💡 중요: ACTION_DOWN에서
true
를 반환해야 이후의 ACTION_MOVE와 ACTION_UP 이벤트를 받을 수 있습니다.false
를 반환하면 해당 터치 시퀀스의 이후 이벤트는 더 이상 받지 않습니다.
#3. MotionEvent 객체 살펴보기
MotionEvent
객체는 터치 이벤트에 관한 모든 정보를 담고 있습니다. 주요 메서드는 다음과 같습니다:
- getAction(): 현재 이벤트의 액션 코드 (ACTION_DOWN, ACTION_MOVE, ACTION_UP 등)
- getX(), getY(): 터치 포인트의 X, Y 좌표 (뷰 기준)
- getRawX(), getRawY(): 터치 포인트의 X, Y 좌표 (화면 기준)
- getPointerCount(): 현재 터치하고 있는 손가락 수
- getPointerId(int): 특정 인덱스의 포인터 ID
- getPressure(): 터치 압력 (지원되는 기기에서만)
- getSize(): 터치 면적 (지원되는 기기에서만)
- getEventTime(): 이벤트 발생 시간 (밀리초)
다음은 MotionEvent에서 기본 정보를 추출하는 예시입니다:
fun extractMotionEventInfo(event: MotionEvent) {
val action = event.action
val x = event.x
val y = event.y
val pressure = event.pressure
val size = event.size
val eventTime = event.eventTime
Log.d("MotionEvent", """
액션: ${getActionString(action)}
좌표: ($x, $y)
압력: $pressure
크기: $size
시간: $eventTime
""".trimIndent())
}
fun getActionString(action: Int): String {
return when (action) {
MotionEvent.ACTION_DOWN -> "ACTION_DOWN"
MotionEvent.ACTION_MOVE -> "ACTION_MOVE"
MotionEvent.ACTION_UP -> "ACTION_UP"
MotionEvent.ACTION_CANCEL -> "ACTION_CANCEL"
else -> "기타 액션: $action"
}
}
#4. 터치 이벤트 전달 과정
안드로이드에서 터치 이벤트는 뷰 계층 구조를 따라 전달됩니다. 이 과정을 이해하는 것이 중요합니다:
- 사용자가 화면을 터치하면 시스템이
MotionEvent
생성 - 액티비티의
dispatchTouchEvent()
로 이벤트 전달 - 액티비티는 이벤트를 최상위 뷰의
dispatchTouchEvent()
로 전달 - 이벤트는 터치 지점에 있는 가장 하위 뷰까지 전달됨 (Top-Down 방식)
- 각 뷰는 이벤트를 처리할지 결정 (onTouchEvent() 반환값으로)
- 하위 뷰가 처리하지 않으면 상위 뷰로 이벤트가 다시 전달됨 (Bottom-Up 방식)
이 과정을 단순화된 코드로 표현하면 다음과 같습니다:
// View 클래스 내부 (안드로이드 프레임워크의 간소화된 버전)
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
// 1. OnTouchListener가 있으면 먼저 호출
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 2. OnTouchListener에서 처리되지 않았다면 onTouchEvent() 호출
else if (onTouchEvent(event)) {
result = true;
}
return result;
}
#5. onTouchEvent() 메서드 구현하기
onTouchEvent()
메서드는 View 클래스에 정의된 메서드로, 터치 이벤트를 직접 처리할 수 있습니다. 커스텀 뷰를 만들 때 이 메서드를 오버라이드하여 터치 동작을 구현합니다.
class CustomTouchView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var lastTouchX = 0f
private var lastTouchY = 0f
override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x
val y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 터치 시작 지점 저장
lastTouchX = x
lastTouchY = y
return true // 이후 이벤트도 계속 받기 위해 true 반환
}
MotionEvent.ACTION_MOVE -> {
// 이동 거리 계산
val dx = x - lastTouchX
val dy = y - lastTouchY
// 뷰 위치 이동 (예: 드래그 기능)
translationX += dx
translationY += dy
// 마지막 위치 업데이트
lastTouchX = x
lastTouchY = y
}
MotionEvent.ACTION_UP -> {
// 터치 완료 시 수행할 작업
performClick() // 접근성을 위해 클릭 이벤트도 발생시킴
}
}
return super.onTouchEvent(event)
}
// 접근성을 위해 performClick() 오버라이드
override fun performClick(): Boolean {
super.performClick()
return true
}
}
#6. OnTouchListener 사용하기
뷰의 서브클래스를 만들지 않고도 터치 이벤트를 처리하려면 OnTouchListener
를 사용할 수 있습니다:
val myView = findViewById<View>(R.id.my_view)
myView.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
Log.d("TouchListener", "View가 터치되었습니다")
true // 이벤트 처리됨
}
MotionEvent.ACTION_MOVE -> {
Log.d("TouchListener", "View 위에서 손가락이 움직입니다")
true
}
MotionEvent.ACTION_UP -> {
Log.d("TouchListener", "View에서 손가락을 뗐습니다")
v.performClick() // 접근성을 위해 필요
true
}
else -> false // 다른 이벤트는 처리하지 않음
}
}
OnTouchListener
는 특히 기존 뷰의 터치 동작을 확장하거나 수정할 때 유용합니다.
#7. 멀티 터치 처리하기
안드로이드는 여러 손가락으로 동시에 화면을 터치하는 멀티 터치를 지원합니다. 멀티 터치 처리는 다음과 같은 추가 이벤트 액션을 사용합니다:
- ACTION_POINTER_DOWN: 두 번째 이상의 손가락이 화면에 닿았을 때
- ACTION_POINTER_UP: 두 번째 이상의 손가락을 화면에서 뗐을 때
멀티 터치 이벤트를 처리하는 코드 예시:
override fun onTouchEvent(event: MotionEvent): Boolean {
// 액션 마스크로 기본 액션 추출
val action = event.actionMasked
when (action) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
// 새로운 손가락이 화면에 닿음
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
val x = event.getX(pointerIndex)
val y = event.getY(pointerIndex)
Log.d("MultiTouch", "손가락 #$pointerId DOWN 이벤트 발생: ($x, $y)")
return true
}
MotionEvent.ACTION_MOVE -> {
// 모든 활성 포인터의 위치 추적
for (i in 0 until event.pointerCount) {
val pointerId = event.getPointerId(i)
val x = event.getX(i)
val y = event.getY(i)
Log.d("MultiTouch", "손가락 #$pointerId MOVE: ($x, $y)")
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
// 손가락을 화면에서 뗌
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
Log.d("MultiTouch", "손가락 #$pointerId UP 이벤트 발생")
}
MotionEvent.ACTION_CANCEL -> {
Log.d("MultiTouch", "터치 이벤트가 취소됨")
}
}
return super.onTouchEvent(event)
}
💡 참고: 멀티 터치에서는
event.action
이 아닌event.actionMasked
를 사용하여 액션을 추출해야 합니다.
#8. 제스처 감지기(GestureDetector) 활용하기
복잡한 제스처 처리는 직접 구현하기 어려울 수 있습니다. 안드로이드는 이를 위해 GestureDetector
와 ScaleGestureDetector
같은 도우미 클래스를 제공합니다.
class GestureExampleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val gestureDetector: GestureDetector
init {
// GestureDetector 초기화
gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
// 모든 제스처는 onDown()에서 시작됨
return true
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
Log.d("Gesture", "단일 탭 감지됨")
return true
}
override fun onDoubleTap(e: MotionEvent): Boolean {
Log.d("Gesture", "더블 탭 감지됨")
return true
}
override fun onLongPress(e: MotionEvent) {
Log.d("Gesture", "롱 프레스 감지됨")
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
Log.d("Gesture", "스크롤 감지됨: X=$distanceX, Y=$distanceY")
return true
}
override fun onFling(
e1: MotionEvent,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
Log.d("Gesture", "플링 감지됨: velocityX=$velocityX, velocityY=$velocityY")
return true
}
})
}
override fun onTouchEvent(event: MotionEvent): Boolean {
// GestureDetector에 모든 터치 이벤트 전달
return if (gestureDetector.onTouchEvent(event)) {
true
} else {
super.onTouchEvent(event)
}
}
}
확대/축소 제스처는 ScaleGestureDetector
로 처리할 수 있습니다:
// 확대/축소 제스처 감지 예시
val scaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val scaleFactor = detector.scaleFactor
// 뷰 크기 조절
scaleX *= scaleFactor
scaleY *= scaleFactor
return true
}
})
// onTouchEvent()에서 사용
override fun onTouchEvent(event: MotionEvent): Boolean {
scaleDetector.onTouchEvent(event)
return true
}
#9. 터치 이벤트 충돌 해결하기
복잡한 뷰 계층에서는 여러 뷰가 터치 이벤트를 처리하려고 할 때 충돌이 발생할 수 있습니다. 예를 들어, ScrollView 안에 있는 커스텀 뷰의 제스처 처리가 스크롤과 충돌할 수 있습니다.
이런 문제를 해결하는 방법은 다음과 같습니다:
방법 1: 부모 뷰의 터치 이벤트 가로채기
// 부모 ScrollView가 특정 방향의 스크롤을 처리하지 않도록 설정
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 수평 방향 이동이 감지되면 부모가 이벤트를 가로채지 않음
if (isHorizontalScrolling(ev)) {
return false;
}
return super.onInterceptTouchEvent(ev);
}
방법 2: requestDisallowInterceptTouchEvent() 사용
자식 뷰에서 부모의 이벤트 가로채기를 금지할 수 있습니다:
override fun onTouchEvent(event: MotionEvent): Boolean {
// 부모 뷰가 이 터치 이벤트를 가로채지 못하도록 설정
parent.requestDisallowInterceptTouchEvent(true)
when (event.action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
// 터치 시퀀스가 끝나면 부모의 가로채기 다시 허용
parent.requestDisallowInterceptTouchEvent(false)
}
}
return super.onTouchEvent(event)
}
#10. 실전 예제: 커스텀 터치 뷰 구현하기
이제 지금까지 배운 내용을 활용하여 간단한 드래그 가능한 커스텀 뷰를 구현해 보겠습니다:
class DraggableView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var dX = 0f
private var dY = 0f
private val paint = Paint().apply {
color = Color.BLUE
style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 원 그리기
val radius = width.coerceAtMost(height) / 2f * 0.8f
canvas.drawCircle(width / 2f, height / 2f, radius, paint)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 터치 시작 시 오프셋 계산
dX = x - event.rawX
dY = y - event.rawY
// 애니메이션으로 약간 축소하여 피드백 제공
animate().scaleX(0.95f).scaleY(0.95f).setDuration(100).start()
return true
}
MotionEvent.ACTION_MOVE -> {
// 뷰 위치 업데이트 (드래그)
x = event.rawX + dX
y = event.rawY + dY
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
// 터치 종료 시 원래 크기로 복원
animate().scaleX(1f).scaleY(1f).setDuration(100).start()
performClick()
}
}
return super.onTouchEvent(event)
}
override fun performClick(): Boolean {
// 접근성을 위한 클릭 처리
return super.performClick()
}
}
레이아웃 XML에서 사용:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.app.DraggableView
android:id="@+id/draggable_view"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"/>
</FrameLayout>
요약
안드로이드에서 터치 이벤트는 다음과 같은 순서로 발생합니다:
- ACTION_DOWN: 터치 시작
- ACTION_MOVE: 손가락 이동 (여러 번 발생 가능)
- ACTION_UP: 터치 종료
또한 멀티 터치의 경우:
- ACTION_POINTER_DOWN: 추가 손가락 터치 시작
- ACTION_POINTER_UP: 추가 손가락 터치 종료
터치 이벤트 처리를 위한 주요 방법:
- 뷰의
onTouchEvent()
메서드 오버라이드 OnTouchListener
구현GestureDetector
사용
터치 이벤트를 올바르게 처리하면 직관적이고 반응성 좋은 사용자 인터페이스를 구현할 수 있습니다. 복잡한 제스처나 사용자 정의 컨트롤을 만들 때는 위에서 설명한 개념과 방법을 조합하여 사용하세요.
긴 글 읽어주셔서 감사합니다.
끝.
'Development > Android' 카테고리의 다른 글
[Android] Button의 텍스트에 밑줄을 추가하는 5가지 완벽 (0) | 2019.09.22 |
---|---|
[Android] Android ABI 완벽 가이드 : 초보 개발자를 위한 적용 및 관리 방법 (0) | 2019.09.22 |
[Android] Intent Filter (0) | 2019.09.10 |
[Android] Flexable Fragment UI 구축 (0) | 2019.09.06 |
[Android] App Fragment Test (0) | 2019.09.06 |