일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- GitHub
- Gradle
- Compose
- relay
- 코루틴
- 프로그래머스
- Coroutines
- Codeforces
- MyVoca
- 쿠링
- androidStudio
- 코드포스
- ProGuard
- AWS
- Coroutine
- Python
- Rxjava
- boj
- architecture
- TEST
- Hilt
- textfield
- pandas
- Kotlin
- livedata
- android
- MiTweet
- activity
- 백준
- 암호학
- Today
- Total
이동식 저장소
모든 터치 이벤트의 시작과 끝을 판별하는 방법 본문
Compose로 혼자 뚝딱뚝딱 하던 도중 composable이 어떠한 방식으로든 터치되고 있는지 확인해야 하는 경우가 생겼다. 어떤 방식으로든 focus를 받고 있을 때 아이콘을 보여주고 싶었기 때문이다.
이 기능을 구현하려면 일단 Compose에서의 터치 이벤트를 공부해야 한다.
일단 터치 이벤트가 발생했는지 판단하는 ``isFocused`` 변수를 선언하자. 이 값이 true이면 터치 이벤트가 발생한 것이므로 아이콘을 보여줘야 한다.
@Composable
fun RotatingIndicatorGradient() {
var isFocused by rememberSaveable { mutableStateOf(false) }
RotatingGradient {
AnimatedVisibility(
visible = isFocused,
// ...
) {
Icon(
// ...
)
}
}
}
터치 이벤트
터치나 드래그를 처리하고 싶다면 ``Modifier.clickable``이나 ``Modifier.draggable``을 사용해도 된다. 이 함수들은 이벤트의 발생을 간편하게 확인할 수 있고, 시각적으로 이벤트가 발생했음을 보여주기도 한다. 하지만 지금은 이벤트의 발생 자체가 아닌 이벤트의 시작과 끝을 추적해야 하므로 low level의 함수를 사용해야 한다.
로우 레벨 함수는 ``PointerInputScope``에 정의되어 있다. ``Modifier.pointerInput`` 내부 람다에서 ``PointerInputScope``에 접근할 수 있다. 예를 들어 클릭과 관련된 이벤트는 다음과 같이 처리할 수 있다.
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* Called when the gesture starts */ },
onDoubleTap = { /* Called on Double Tap */ },
onLongPress = { /* Called on Long Press */ },
onTap = { /* Called on Tap */ }
)
}
``pointerInput``의 매개변수 ``Unit``은 일단 무시하자. 밑에서 다시 설명하겠다.
``detect...`` 함수를 사용하면 Tap, Drag, Transform 제스쳐를 개별적으로 추적할 수 있다. 함수 목록은 다음과 같다. Transform 제스쳐는 확대, 축소, 회전 제스쳐를 의미한다.
그런데 지금은 어떠한 형식으로든 UI가 터치되고 있는지를 알고 싶으므로 모든 이벤트를 관찰해야 한다. 물론 위에 있는 ``detect`` 함수를 써도 되겠지만 코드가 너무 복잡해질 것이다.
``awaitPointerEventScope`` 함수를 사용하면 모든 터치 이벤트를 추적할 수 있다. 함수 구조는 다음과 같다.
public abstract suspend fun <R> awaitPointerEventScope(
block: suspend AwaitPointerEventScope.() -> R
): R
``AwaitPointerEventScope``에도 터치 이벤트를 처리할 수 있는 함수가 많이 정의되어 있다. 이 scope 안에서 코드를 완성해 보자.
지금 필요한 알고리즘은 다음과 같다.
- ``isFocused``가 ``true``라면 현재 발생하고 있는 터치 이벤트가 끝날 때까지 기다린 후, ``isFocused``에 ``false``를 할당한다.
- ``isFocused``가 ``false``라면 터치 이벤트가 발생할 때까지 기다린 후, 이벤트가 발생할 때 ``isFocused``에 ``true``를 할당한다.
``AwaitPointerInputScope``의 함수를 활용하면 다음과 같은 코드를 작성할 수 있다.
awaitPointerEventScope {
isFocused = if (isFocused) {
// 모든 터치 이벤트가 끝날 때까지 기다림
while (waitForUpOrCancellation() == null);
false
} else {
awaitFirstDown(false)
true
}
}
Up? Cancel?
터치 이벤트에서 Up은 이벤트가 종료될 때까지 포인터가 움직이지 않았음을 의미한다. 포인터란 화면을 터치할 수 있는 모든 수단(손가락, 마우스, 스타일러스 등)을 의미한다. 예를 들어 어떤 위치를 터치한 후 손가락을 이동하지 않고 그 자리에서 뗀 경우가 해당된다.
반대로 Cancel은 포인터가 움직이고 있음을 의미한다.
지금은 Up 이벤트만을 추적해야 한다. ``waitForUporCancellation()`` 함수는 up 또는 cancel 이벤트가 발생할 때까지 기다리는 suspend 함수이다. 결과가 up이라면 ``PointerInputChange`` 객체를, cancel이라면 ``null``을 반환한다. 따라서 이 함수의 반환값이 ``null``이 아닐 때까지 기다리면 up 이벤트를 받을 수 있다. 나는 body가 없는 while문을 사용하여 구현하였다.
``AwaitPointerEventScope``에서 이벤트를 기다리는 함수들은 터치 이벤트가 cancel됐을 때 공통적으로 null을 반환하는 듯하다.
Down?
Down은 말 그대로 포인터가 UI를 터치하는 순간을 가리킨다. ``awaitFirstDown`` 함수를 사용하여 UI가 첫 번째로 터치되는 순간을 기다릴 수 있다. 매개변수는 이 글의 내용과 무관하므로 생략하겠다.
지금까지 작성한 코드는 다음과 같다.
@Composable
fun RotatingIndicatorGradient() {
var isFocused by rememberSaveable { mutableStateOf(false) }
RotatingGradient(
modifier = modifier.pointerInput(Unit) {
awaitPointerEventScope {
isFocused = if (isFocused) {
// 모든 터치 이벤트가 끝날 때까지 기다림
while (waitForUpOrCancellation() == null);
false
} else {
awaitFirstDown(false)
true
}
}
}
) {
AnimatedVisibility(
visible = isFocused,
// ...
) {
Icon(
// ...
)
}
}
}
그런데 터치를 뗄 때 아이콘이 없어지지 않는다. 이벤트 종료를 제대로 수신되지 못하는 것 같다.
``awaitPointerEventScope``의 API 문서를 읽어보면 힌트를 얻을 수 있다.
A call to awaitPointerEventScope will resume with block's result after it completes.
요약하면 블럭이 한 번 실행된 후에는 다시 실행되지 않는다! 그래서 맨 처음의 down 이벤트만 수신되고 그 후의 up 이벤트는 수신되지 않는 것이다.
``isFocused``의 값이 바뀔 때마다 ``awaitPointerEventScope``를 다시 실행하면 될 것 같다. 고맙게도 이 기능은 ``Modifier.pointerInput``에 이미 구현되어 있다. 역시 API 문서에서 힌트를 얻을 수 있다.
Specifying the captured value as a key parameter will cause block to cancel and restart from the beginning if the value changes
``key``로 지정된 변수의 값이 바뀌면 코드가 다시 실행된다. LaunchedEffect와 비슷하다. 따라서 ``isFocused``를 key로 주면 ``isFocused``의 값에 따라 이벤트를 계속 추적할 수 있다.
완성된 코드는 다음과 같다.
@Composable
fun RotatingIndicatorGradient() {
var isFocused by rememberSaveable { mutableStateOf(false) }
RotatingGradient(
modifier = modifier.pointerInput(isFocused) {
awaitPointerEventScope {
isFocused = if (isFocused) {
// 모든 터치 이벤트가 끝날 때까지 기다림
while (waitForUpOrCancellation() == null);
false
} else {
awaitFirstDown(false)
true
}
}
}
) {
AnimatedVisibility(
visible = isFocused,
// ...
) {
Icon(
// ...
)
}
}
}
잘 된다!
코드 전체를 보고 싶다면 GitHub를 참고하길 바란다.
참고문헌
'Primary > Compose' 카테고리의 다른 글
TalkBack이 Compose TextField를 인식하지 못하는 문제 (1) | 2023.10.02 |
---|---|
Compose에서 이미지가 흑백으로 보일 때 (0) | 2023.06.10 |
Preview 모드인지 확인하는 방법 (0) | 2023.01.06 |
Brush로 다양한 색깔 효과 입히기 (0) | 2023.01.04 |
[Compose] LazyList에서 아이템을 추가했을 때 스크롤 위치를 기억하는 방법 (0) | 2022.12.14 |