1. 개요
- 컴포즈에서 터치/마우스클릭/스타일러스(펜) 등 장치의 입력 이벤트를 직접 처리하기 위한 Modifier
- 단순히 클릭하면 특정 동작을 하는 경우 Modifer.clickable을 사용할 수 있지만, 클릭한 후 일정 거리 드래그한다거나 두손가락으로 핀치줌하는 경우 특정 동작을 하도록 할때는 Modifier.pointerInput 사용
2. 포인터란?
- 화면을 가리키는 모든 입력장치
- 손가락, 마우스 커서, 스타일러스 등
- 멀티터치처럼 동시에 여러 포인터가 존재할 수 있고, 각각의 포인터는 고유한 id를 가짐
3. 포인터 이벤트의 흐름
- Down -> Move -> Up(또는 Cancel)
- pointerInput은 위 흐름을 코루틴으로 처리함
- 이벤트를 기다리는 동작이 메인 스레드를 중단시키면 안되므로, 각각의 코루틴에서 suspend된 채로 처리됨
4. 자주 쓰는 고수준 감지 함수
1) 탭/롱프레스/더블탭 감지
Modifier.pointerInput(Unit) {
detectTapGestures {
onTap = { offset -> // 손가락을 빠르게 눌렀다 뗐을 때
println("탭 위치 : $offset")
},
onLongPress = { offset -> // 오래 누르고 있을 때
println("롱프레스 위치 : $offset")
},
onDoubleTap = { offset -> // 빠르게 두번 탭 했을 때
println("더블탭 위치 : $offset")
},
onPress = { offset -> // Down 이벤트 발생 즉시
println("누르는 순간 위치 : $offset")
},
}
}
- 특이사항
- onDoubleTap : 설정되어 있으면 첫번째 탭 이후 두번째 탭이 오는지 기다려야하므로 onTap 호출이 약간 지연됨
- 유사한 기능
- detectTapAndPress : onPress와 onTap만 지원하며 성능적으로 좀 더 가벼움
2) 드래그 감지
Modifier.pointerInput(Unit) {
detectDragGestures {
onDragStart = { offset ->
println("드래그 시작 : $offset") // 드래그가 시작된 위치
},
onDrag = { change, dragAmount -> // change : 현재 포인터의 상세정보
println("드래그 중 : $dragAmount") // dragAmount : 이번 프레임에 이동한 거리(Offset)
change.consume() // 이벤트 소비(다른 곳에 전달 안함)
},
onDragEnd = {
println("드래그 종료") // 손가락을 뗐을 때
},
onDragCancel = {
println("드래그 취소") // 시스템에 의해 취소됐을 때
}
}
}
- 특이사항
- 터치 후 일정 거리(Touch Slop) 이상 움직여야 시작됨
- Touch Slop은 ViewConfiguration.touchSlop에 정의되어 있음
- 유사한 기능
- detectDragGesturesAfterLongPress : 롱프레스 후 드래그 시 감지
- detectVerticalDragGestures : onDrag 대신 onVerticalDrag를 지원하며, 오직 위아래 드래그만 감지
- detectHorizontalDragGestures : onDrag 대신 onHorizontalDrag를 지원하며, 오직 좌우 드래그만 감지
3) 핀치줌/회전 감지
Modifier.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotation ->
println("중심점 : $centroid") // 두 손가락의 중간좌표
println("이동 : $pan") // 전체 이동량
println("확대/축소 배율 : $zoom") // 1.0 기준, 1.2면 20% 확대
println("회전 각도 : $rotation") // 도(degree) 단위
}
}
5. 자주 쓰는 저수준 감지 함수
- awaitEachGesture와 AwaitPointerEventScope의 내부함수 사용
- detectTapGestures같은 고수준 함수로 해결되지 않는 커스텀 제스처는 저수준 함수로 처리
1) awaitEachGesture
- pointerInput 블록 내에서 Down -> Move -> Up 한번의 제스처 사이클을 처리하는 블록을 반복실행하는 루프 헬퍼
- 내부적으로 while(true) + awaitPointerEventScope 조합으로 구성되어 있음
- awaitEachGesture안에서 사용하는 함수들은 awaitPointerEventScope의 멤버함수들
2) awaitEachGesture 내부 함수
- awaitFirstDown : Down 이벤트 기다리기, 그동안 suspend, 이벤트 발생하면 PointerInputChange 반환
- waitForUpOrCancellation : Up 또는 Cancel 기다리기, Up이면 PointerInputChange 반환하고 Cancel이면 null 반환
- awaitPointerEvent : 모든 포인터 이벤트를 순서대로 하나씩 받기, down/up/move 등 구분없이 발생하는 모든 이벤트를 순서대로 수신, Drag 중에 누적 드래그양을 계산하는 등에 사용
- awaitDragOrCancellation : 드래그가 이미 확정된 이후 매프레임마다 반복호출, 드래그 진행 중 이동량을 처리하는 루프
- awaitTouchSlopOrCancellation : 드래그가 시작됐는지 판단(Touch Slop 초과 시점 딱 한번 호출), Down 이후 일정 거리 이상 이동했을때만 드래그로 판정
- 그 외 awaitLongPressOrCancellation, awaitHorizontalTouchSlopOrCancellation, awaitVerticalTouchSlopOrCancellation, awaitHorizontalDragOrCancellation, awaitVerticalDragOrCancellation가 있음
3) 내부 로직 순서
- awaitFirstDown() : Down 대기
- awaitTouchSlopOrCancellation : 드래그 확정 판단(선택)
- awaitDragOrCancellation : Move 반복 처리(선택)
또는 waitForUpOrCancellation : Up/Cancel 대기
4) PointerInputChange
- onDrag같은 곳에서 전달받는 change가 PointerInputChange 자료형
- 포인터의 이전/현재 위치, 눌림 여부, 이벤트 소비여부등이 담겨있음
- 손가락 하나의 정보
5) PointerEvent
- awaitPointerEvent()가 반환하는 값
- 한 프레임에 발생한 모든 손가락들의 모음
- PointerEvent의 changes 리스트를 확인해야 저수준에서 멀티터치를 정밀하게 제어할 수 있음
6) 사용 예시
Modifier.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
// 1) 드래그 확정 판단(딱 한번 호출)
val firstDrag = awaitTouchSlopOrCancellation(down.id) { change, _ ->
change.consume() // 여기서 소비해줘야 드래그가 시작됐다고 판단함
}
if (firstDrag != null) {
// 2) 드래그 진행 처리(드래그가 끝날때까지 반복 호출
var drag : PointerInputChange? = firstDrag
while (drag != null) {
val amount = drag.position - drag.previousPosition // 이번 프레임 이동량
println("이동량 : $amount")
drag.consume()
drag = awaitDragOrCancellation(down.id) // 다음 이동 이벤트 대기
}
println("드래그 종료")
} else {
println("탭 또는 취소")
}
}
}