#1 ๊ฐ์
#1-1 ์ด์ ๊ฒ์๊ธ
[Android] Pointer input - PointerInputChange, PointerEvent
#1 ๊ฐ์ ๋์ ์ดํดํ๊ธฐ | Jetpack Compose | Android Developers์ด ํ์ด์ง๋ Cloud Translation API๋ฅผ ํตํด ๋ฒ์ญ๋์์ต๋๋ค. ๋์ ์ดํดํ๊ธฐ ์ปฌ๋ ์ ์ ์ฌ์ฉํด ์ ๋ฆฌํ๊ธฐ ๋ด ํ๊ฒฝ์ค์ ์ ๊ธฐ์ค์ผ๋ก ์ฝํ ์ธ ๋ฅผ ์
kenel.tistory.com
์์ ๊ฒ์๊ธ์์ ์ด์ด์ง๋ค. ํด๋น ๊ฒ์๊ธ์์ ๋จผ์ PointerInputChange ๋ฐ PointerEvent์ ๋ํ ์ดํด๋ฅผ ํด์ผ ๋ณธ ๊ฒ์๊ธ์ ์ดํดํ ์ ์๋ค.
#1-2 ๊ณต์ ๋ฌธ์
๋์ ์ดํดํ๊ธฐ | Jetpack Compose | Android Developers
์ด ํ์ด์ง๋ Cloud Translation API๋ฅผ ํตํด ๋ฒ์ญ๋์์ต๋๋ค. ๋์ ์ดํดํ๊ธฐ ์ปฌ๋ ์ ์ ์ฌ์ฉํด ์ ๋ฆฌํ๊ธฐ ๋ด ํ๊ฒฝ์ค์ ์ ๊ธฐ์ค์ผ๋ก ์ฝํ ์ธ ๋ฅผ ์ ์ฅํ๊ณ ๋ถ๋ฅํ์ธ์. ์ดํดํด์ผ ํ ๋ช ๊ฐ์ง ์ฉ์ด์ ๊ฐ๋ ์ด ์
developer.android.com
์ด์ ๊ฒ์๊ธ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก, ๊ณต์ ๋ฌธ์๋ฅผ ๋์ ์ธ์ด๋ก ์ ๋ฆฌํ๊ณ , ์ํ ์ฑ๋ ๋ง๋ค์ด๋ดค๋ค.
#2 Gesture
#2-1 Gesture๋ ๋ณ๋ช ์ด๋ค
2๊ฐ ์ด์์ ์ผ๋ จ์ PointerEvent ์กฐํฉ์ ๋ถ์ด๋ ๋ณ๋ช
์ด๋ค. ๋ณ๋์ ํด๋์ค๋ก ์กด์ฌํ์ง๋ ์๋๋ค. ์๋ฅผ ๋ค์ด, Tab Gesture๋ PointerEvent.type.toString() == Press์ธ PointerEvent์ ๋ค์ด์ด PointerEvent.type.toString() == Release์ธ PointerEvent๊ฐ ๋ฐ์ํ๋ค๋ฉด ์ด๋ฅผ ํน๋ณํ Tab Gesture๋ผ ๋ถ๋ฅธ๋ค. ์ด์ ๋น์ทํ๊ฒ Drag Gesture๋ Transform Gesture ๋ฑ์ด ์์ผ๋ฉฐ, ์ผ๋ จ์ PointerEvent ์กฐํฉ์ ๊ฐ์งํ๋ ์ฌ์ฉ์ ์ง์ Gesture๋ ๋ง๋ค ์ ์๋ค.
#2-2 ๊ธฐ๋ณธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๊ณต ์ ์ค์ฒ
Gesture(์ ์ค์ฒ) ์ ํ | ์ ์ค์ฒ๋ฅผ ๋ถ์ฌํ๋ Modifier์ ํ์ฅ ํจ์ |
Tap, Press | clickable, combinedClickable, selectable, toggleable, triStateToggleable |
Scrolling | horizontalScroll, verticalScroll, scrollable |
Dragging | draggable, anchoredDraggable |
Multi-touch Gesture | transformable |
ํ์ 2์ด์ ์๋ ๋ชจ๋ ์์๋ Modifier์ ํ์ฅ ํจ์๋ค. ์ฆ, @Composable์ด ๊ฐ์ง๋ modifier ์ธ์์ ์ ํ์ฅ ํจ์๋ค์ ๋ฉ์๋ ์ฒด์ด๋ํ์ฌ ๋ฃ์ด์ฃผ๋ฉด Gesture๋ฅผ '๋ถ์ฌ'(= ํน์ PointerEvent์ ์กฐํฉ์ ๊ฐ์ง + ์ํ๋ ๋์ ๋ช
์ธ)ํ ์ ์๋ค. ํ์ ์๋ transformable์ ์ด๋ฆ์์ ์ด๋ค ๊ธฐ๋ฅ์ธ์ง ์ง๊ด์ ํ์
์ด ํ๋ค๋ค (์๋ฏธ๊ถ ์ฌ๋์ ๊ฐ๋ฅํ๋ ค๋?). transformable์ ์ง๋ ์ฑ์์ ์ง๋ ํ์, ์ปดํฌ๋ํธ ํ์ , ์ค ์ธ/์์ ๋ฑ์ ๋ฉํฐ ํฐ์น๋ฅผ ์ํ๋ ์ ์ค์ฒ ๊ตฌํ์ ์ํด ์ฌ์ฉ๋๋ค๊ณ ํ๋ค.
#2-3 ์ฌ์ฉ์ ์ ์ ์ ์ค์ฒ
Text(
text = componentName,
modifier = Modifier
.background(Color.LightGray)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
// event๋ฅผ ๊ฐ์ง๊ณ ๋ฌด์ธ๊ฐ ํ๋ ์ฝ๋
}
}
},
fontSize = 48.sp,
)
์ ์ฝ๋๋ ์ด์ ๊ฒ์๊ธ์์ ์ด ์ฝ๋์ ์ผ๋ถ๋ถ์ด๋ค. ์์ ๊ฐ์ ํํ๋ก ์ฌ์ฉ์ ์ ์ ์ ์ค์ฒ๋ ๋ง๋ค ์ ์๋ค. ๋ด๊ฐ ์ฒ๋ฆฌํ๊ณ ์ถ์ ํน์ PointerEvent์ ์กฐํฉ์ ๊ฐ์งํ๊ณ , ๊ฐ์ง๋์ ๋์ ๋์์ ์ ์ฝ๋์ ์ฃผ์ ๋ถ๋ถ์ ๋ฃ๋ ์์ผ๋ก ๋ง์ด๋ค.
ํ์ง๋ง, awaintPointerEvent()๋ฅผ ์ด์ฉํด ์ฌ์ฉ์ ์ ์ ์ ์ค์ฒ๋ฅผ ๋ง๋๋ ๊ฑด, ๋๋ฌด๋ ์์(raw)์ ์ด๋ผ ๊ตฌํ์ด ๋ณต์กํ๋ค. ์ด๋ฐ ๋ถ๋ด์ ๋์ด์ฃผ๊ธฐ ์ํด์ Jetpack Compose์์ detectTapGesture(), detectDragGestures()๊ฐ์ Gesture ๊ฐ์ง๋ฅผ ์ํ ํจ์๋ฅผ ์ ๊ณตํ๋ค.
#2-4 Gesture ๊ฐ์ง ํจ์ ๋ชฉ๋ก
Gesture(์ ์ค์ฒ) ์ ํ | ์ ์ค์ฒ๋ฅผ ๊ฐ์งํ๋ PointerInputScope์ ํ์ฅ ํจ์ |
Tap, Double Tap, Press, Long-press | detectTapGestures() |
Drag | detectDragGestures(), detectVerticalDragGestures(), detectHorizontalDragGestures(), detectDragGesturesAfterLongPress() |
Transform | detectTransformGestures() |
๊ฐ detect...Gestures() ํจ์๋ ๋ด๋ถ์ ์ผ๋ก ํ์ ํ awaitEachGesture()๋ก ๊ฐ์ธ์ฌ์๋ค.
#2-5 awaitEachGesture()
Jetpack Compose์์ ์ ๊ณตํ๋ ๊ฑด, Gesture ๊ฐ์ง ํจ์ ๋ฟ๋ง์ด ์๋๋ค. ์์์ ํ๋ฒ ๋ณด์ฌ์คฌ๋ ์ฝ๋๋ฅผ ๋ค์ ์จ๋ณด๊ฒ ๋ค.
Text(
text = componentName,
modifier = Modifier
.background(Color.LightGray)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
// event๋ฅผ ๊ฐ์ง๊ณ ๋ฌด์ธ๊ฐ ํ๋ ์ฝ๋
}
}
},
fontSize = 48.sp,
)
์ด ์ฝ๋๋ฅผ ์๋์ ๊ฐ์ด ๊ฐ์ํํ ์ ์๋ค.
Text(
text = componentName,
modifier = Modifier
.background(Color.LightGray)
.pointerInput(Unit) {
awaitEachGesture {
val event = awaitPointerEvent()
// event๋ฅผ ๊ฐ์ง๊ณ ๋ฌด์ธ๊ฐ ํ๋ ์ฝ๋
}
},
fontSize = 48.sp,
)
์ฆ, awaitEachGesture()๋ awaitPointerEventScope() ๋ฐ while(true)๋ฅผ ๋์ฒดํ๋ค. ํ์ง๋ง, awaitEachGesture()๋ ๋ณด๋ค ๊ณ ์์ค์ ์์ญ(Scope)์ผ๋ก์, ๋ ๊ฐํธํ(์์์ ์ธ) ์ฝ๋๋ฅผ ์งค ์ ์๊ฒ ๋์์ค๋ค. ์ด๋ฆ์์ ์ ์ ์๋ฏ, awaitEachGesure() ์์์ ๊ฐ Gesture๊ฐ ์ข
๋ฃ๋๋ฉด ์๋์ผ๋ก ๋ค์ Gesture๋ฅผ ๊ธฐ๋ค๋ฆฐ๋ค. awaitPointerEventScope() ๋ฐ while(true) ๊ตฌ์กฐ์์ ํ๋ก๊ทธ๋๋จธ๊ฐ Gesture๊ฐ ์ธ์ ์์ํ๊ณ ์ธ์ ๋๋๋ ์ง์ ๋ํ ์ฝ๋๋ฅผ ์ผ์ผํ ์ง์ผํ๋ ๊ฒ๊ณผ ๋์กฐ์ ์ด๋ค.
๋ฌผ๋ก , ๋ ์์์ ์ธ ํ๊ฒฝ์ ์ ๊ณตํ๋ค๋ ๊ฒ์ ๋ ์ธ์ฌํ๋ค๋ ์๊ธฐ๋ ๋๋ค. ์์ฃผ ๋ฏธ์ธํ PointerEvent ์ ์ด๊ฐ ํ์ํ๋ค๋ฉด awaitPointerEventScope()๋ฅผ ๋ค์ ์จ์ผํ ์๋ ์์ ํ
๋ค.
#2-6 Gesture ๊ฐ์ง ํจ์์ ์ฌ์ฉ๋ก
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Red)
.pointerInput(Unit) {
detectTapGestures(onTap = { offset -> log = "Tapped at $offset" })
}
.pointerInput(Unit) {
detectDragGestures { _, _ -> log = "Dragging" }
}
) {
Text("box content")
}
๋ฉ์๋ ์ฒด์ด๋
Gesture ๊ฐ์ง ํจ์๋ฅผ ์ฌ์ฉํ ์ฝ๋๋ ์์ ๊ฐ๋ค. ํ๋์ pointerInput()์๋ ํ๋์ Gesture ๊ฐ์ง ํจ์๋ง์ ๋ฃ์ด์ผ ํ๋ค (์ฐธ์กฐ: pointerInput()์ ์ค๋ณต ๋ฉ์๋ ์ฒด์ด๋).
์์ ์ฝ๋์๋ ๋ฌ๋ฆฌ, ํ๋์ pointerInput()์, detectTapGestures()์ detectDragGestures()๋ฅผ ์์๋๋ก ๋ชฝ๋
๋ฃ์๋ค๊ณ ์น์. ์ด๋ฌ๋ฉด, detectDragGestures()๋ ์คํ๋์ง ์๊ณ ์ค์ง detectTapGestures()๋ง ์คํ๋๋ค. ์์์ ์ค๋ช
ํ ๋ฐ์ ๊ฐ์ด detect...Gestures() ํจ์๋ ๋ด๋ถ์ ์ผ๋ก awaitEachGesture()๋ก ๊ฐ์ธ์ฌ์๊ณ , ๊ทธ awaitEachGesture๋ while(true)์ ๋์์ ํฌํจํ๊ณ ์๊ธฐ ๋๋ฌธ์ด๋ค. ๋ฐ๋ผ์ ์ด ๊ฒฝ์ฐ์ pointerInput() ๋ด์์ ์ฌ์ฉ์์ 'Drag' ๋์์ ๊ฐ์งํ ์ ์๋ค. ์ด ๊ฒฝ์ฐ์ pointerInput()์ ์ค์ง 'Tap' ๋์์ ๊ฐ์ง๋ง์ ์ํด ๋๊ธฐํ ๊ฒ์ด๋ค.
์์์ ์๋น
๋ ๊ฐ detect...Gesture() ํจ์๋ ํจ์ ์ข
๋ฃ ์ , PointerEvent๋ฅผ '์๋น๋จ' ์ํ๋ก ์
๋ฐ์ดํธํ๋ค (ํ๋ก๊ทธ๋๋จธ ์
์ฅ์์๋ PointerEvent๊ฐ ์์์ ์ผ๋ก ์๋น๋๋ ์
์ด๋ค).
์ฌ๋ด
pointerInput()์ ์ ๋ ๋ณํ์ง ์๋ ๊ฐ์ธ Unit์ ๋ฃ์์ผ๋ฏ๋ก, pointerInput() ์ฝ๋ ๋ธ๋ก์ Recomposition๋๋ ์ฌ์คํ๋์ง ์๊ณ ์ญ ์ ์ง๋ ๊ฒ์ด๋ค.
#3 ์ฌ์ฉ์ ์ง์ Gesture ๊ตฌํ
#3-1 ๊ฐ์
๋๋ธ ํด๋ฆญ ์์ ๋์์ Jetpack Compose์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ์ง๋ง, ํธ๋ฆฌํ ํด๋ฆญ์ ์ ๊ณตํ์ง ์๋๋ค. ๋ณธ ๊ฒ์๊ธ์์ ๋ฐฐ์ด ์ง์์ ๋ฐํ์ผ๋ก ์ปค์คํ
Gesture๋ก์ 'ํธ๋ฆฌํ ํด๋ฆญ'์ ๊ตฌํํด๋ณด๊ฒ ๋ค.
#3-2 ํต์ฌ ์ฝ๋
@SuppressLint("ModifierFactoryUnreferencedReceiver")
fun Modifier.addTripleClickGesture1(context: Context): Modifier {
return Modifier.composed { // composed๋ State๋ฅผ ๊ฐ์ง๋ Modifier์ ํ์ฅ ํจ์๋ฅผ ๋ง๋ค ์ ์๊ฒํจ
var tapCount by remember { mutableIntStateOf(0) } // ํด๋ฆญ ํ์
var lastTapTime by remember { mutableLongStateOf(0L) } // ๋ง์ง๋ง ํด๋ฆญ ์๊ฐ
Modifier.pointerInput(Unit) {
detectTapGestures {
val currentTime = System.currentTimeMillis()
tapCount = if (currentTime - lastTapTime < 500) { // 500๋ฐ๋ฆฌ์ด ์ด๋ด
++tapCount
} else {
1
}
lastTapTime = currentTime
if (tapCount >= 3) { // ํธ๋ฆฌํ ํด๋ฆญ ๊ฐ์ง
Toast.makeText(context, "Triple Clicked!", Toast.LENGTH_SHORT).show()
tapCount = 0 // ์ด๊ธฐํ
}
}
}
}
}
addTripleClickGesture1()์ onClick ํ๋กํผํฐ๋ฅผ ๋ณด์ ํ Button() ๋ฑ์ ์ ์ฉํ๋ฉด ์ ๋๋ค. ์๋ํ๋ฉด, Button()์์ ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ์ฐ์ ์ ์ผ๋ก ์๋นํ๊ธฐ์, detectTapGestures()์ ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ๋จ์ด์ง์ง ์๊ธฐ ๋๋ฌธ์ด๋ค. ๊ทธ๋์ ๋๋ addTripleClickGesture1()๋ฅผ Box()์ modifier์ ์ ์ฉํ๋ค.
ChatGPT๊ฐ ์๋ ค์ค, Coroutines์ ํ์ฉํ ๊ตฌํ
@SuppressLint("ModifierFactoryUnreferencedReceiver")
fun Modifier.addTripleClickGesture2(context: Context): Modifier {
return Modifier.composed {
var tapCount by remember { mutableIntStateOf(0) } // ํด๋ฆญ ํ์
val coroutineScope = rememberCoroutineScope()
Modifier.pointerInput(Unit) {
detectTapGestures {
tapCount++
coroutineScope.launch {
delay(500) // 500๋ฐ๋ฆฌ ๋์ ๊ธฐ๋ค๋ฆผ (์ฐ์ ํด๋ฆญ ํ์ธ)
if (tapCount >= 3) { // ํธ๋ฆฌํ ํด๋ฆญ ๊ฐ์ง
Toast.makeText(context, "Triple Clicked!", Toast.LENGTH_SHORT).show()
}
tapCount = 0 // ์ด๊ธฐํ
}
}
}
}
}
addTripleClickGesture2()๋ onClick ํ๋กํผํฐ๋ฅผ ๋ณด์ ํ Button() ๋ฑ์ ์ ์ฉํ๋ฉด ์ ๋๋ค. ์๋ํ๋ฉด, Button()์์ ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ์ฐ์ ์ ์ผ๋ก ์๋นํ๊ธฐ์, detectTapGestures()์ ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ๋จ์ด์ง์ง ์๊ธฐ ๋๋ฌธ์ด๋ค. ๊ทธ๋์ ๋๋ addTripleClickGesture1()๋ฅผ Box()์ modifier์ ์ ์ฉํ๋ค.
#3-3 ์์ฑ๋ ์ฑ
android-practice/pointer-input/TripleClickGesture at master · Kanmanemone/android-practice
Contribute to Kanmanemone/android-practice development by creating an account on GitHub.
github.com
#4 ์์ฝ
Gesture๋ ํน์ PointerEvent ์กฐํฉ์ ๋ณ๋ช ์ด๋ฉฐ, Gesture ์ ์ด๋ฅผ ์ํ ์ฌ๋ฌ ํจ์๊ฐ ์กด์ฌํ๋ค.
'๊นจ์ ๊ฐ๋ ๐ > Android' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Android] Pointer input - Nested Scroll (0) | 2025.02.18 |
---|---|
[Android] Pointer input - Scroll (0) | 2025.02.17 |
[Android] Pointer input - AwaitPointerEventScope()์ ๋ฉ์๋๋ค (0) | 2025.02.08 |
[Android] Pointer input - PointerInputChange, PointerEvent (0) | 2025.02.07 |
[Android] Jetpack Compose - Navigation์ Destination ๊ฐ ๋ฐ์ดํฐ ์ ๋ฌ (NavBackStackEntry. (0) | 2024.09.13 |