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("탭 또는 취소")
        }
    }
}