๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘/Android

[Android] Pointer input - Scroll

interfacer_han 2025. 2. 17. 23:54

#1 ๊ฐœ์š”

 

์Šคํฌ๋กค  |  Jetpack Compose  |  Android Developers

์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์Šคํฌ๋กค ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถ„๋ฅ˜ํ•˜์„ธ์š”. ์ฐธ๊ณ : ํ•ญ๋ชฉ ๋ชฉ๋ก์„ ํ‘œ์‹œํ•˜๋ ค๋ฉด ์ด๋Ÿฌํ•œ API ๋Œ€์‹ 

developer.android.com

์œ„ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ๋‚˜์˜ ์–ธ์–ด๋กœ ์ •๋ฆฌํ–ˆ๋‹ค.

 

#2 ์Šคํฌ๋กค'๋˜๋Š”' Modifier

#2-1 Modifier.verticalScroll()

fun Modifier.verticalScroll(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false
): Modifier

๋‹น์‹ ์ด ํ˜„์žฌ ๋ณด๊ณ  ์žˆ๋Š” ์ด ํŽ˜์ด์ง€๋Š” ๋ชจ๋‹ˆํ„ฐ๋ณด๋‹ค ํด ๊ฒƒ์ด๋‹ค. ๊ทธ๋ž˜์„œ ๋‹น์‹ ์€ ๋งˆ์šฐ์Šคํœ ๋กœ ์ด ํŽ˜์ด์ง€๋ฅผ ์Šคํฌ๋กคํ•˜๊ณ  ์žˆ๋‹ค. verticalScroll()์€ ์–ด๋–ค ์ฝ˜ํ…์ธ ๊ฐ€ ํ•ด๋‹น ์ฝ˜ํ…์ธ ๊ฐ€ ๋‹ด๊ธด ์ปจํ…Œ์ด๋„ˆ๋ณด๋‹ค (์„ธ๋กœ ๋ฐฉํ–ฅ์œผ๋กœ) ํด ๋•Œ, ๋ณธ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ˆ˜์ง ์Šคํฌ๋กคํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ค€๋‹ค. ๋ฐ˜๋Œ€๋กœ verticalScroll()์ด ๋ถ€์žฌํ•˜๋‹ค๋ฉด, ์ปจํ…Œ์ด๋„ˆ๋ณด๋‹ค ์ฝ˜ํ…์ธ ๊ฐ€ ํผ์—๋„ ์Šคํฌ๋กคํ•  ์ˆ˜ ์—†๋‹ค.

 

#2-2 Modifier.horizontalScroll()

fun Modifier.horizontalScroll(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false
): Modifier

verticalScroll()์˜ ์ˆ˜ํ‰ ๋ฒ„์ „.

 

#3 ์Šคํฌ๋กค '๊ฐ€๋Šฅํ•œ' Modifier

#3-1 Modifier.scrollable()

fun Modifier.scrollable(
    state: ScrollableState,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    flingBehavior: FlingBehavior? = null,
    interactionSource: MutableInteractionSource? = null
): Modifier

์ด๋ฆ„ ํ•œ ๋ฒˆ ๋ชป ์ง€์—ˆ๋‹ค. verticalScroll()๊ณผ ๋Šฌ์•™์Šค๊ฐ€ ๋„ˆ๋ฌด๋‚˜๋„ ๊ฒน์น˜์ง€ ์•Š๋Š”๊ฐ€? ๋ช…๋ฐฑํ•˜๊ฒŒ ๋‹ค๋ฅธ ๋™์ž‘์„ ํ•˜๋Š” ํ•จ์ˆ˜์ž„์—๋„ ๋ง์ด๋‹ค. "'์•„' ๋‹ค๋ฅด๊ณ  '์–ด' ๋‹ค๋ฅด๋‹ค"๋ผ๋Š” ๋ง์„ ๋“ค์–ด๋ณธ ์ ์ด ์žˆ์„ ๊ฒƒ์ด๋‹ค. ๊ทธ ๋ง๋งŒํผ ์ด ํ•จ์ˆ˜๋ฅผ ์ž˜ ์„ค๋ช…ํ•˜๋Š” ๋ง์€ ์ฐพ๊ธฐ ์–ด๋ ค์šธ ๊ฒƒ์ด๋‹ค. ๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜์ž๋ฉด scrollable() ํ•จ์ˆ˜๋Š”,

 

์–ด๋–ค ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์Šคํฌ๋กค ๊ฐ€๋Šฅ์ผ€ ๋งŒ๋“ค์ง€๋งŒ, ๊ทธ๋ ‡๋‹ค๊ณ  ์Šคํฌ๋กค์ด ๋˜๊ฒŒ ๋งŒ๋“ค์ง„ ์•Š๋Š” ํ•จ์ˆ˜

๋‹ค. ์ด ์ •์˜๊ฐ€ ํ˜ผ๋ž€์Šค๋Ÿฌ์šธ ์ˆ˜๋ฐ–์— ์—†์„ ๊ฒƒ์ด๋‹ค. ์ด๊ฑด ์˜ˆ์‹œ ์ฝ”๋“œ์™€ ๊ทธ ์˜ˆ์‹œ ์ฝ”๋“œ๊ฐ€ ๋™์ž‘ํ•˜๋Š” GIF ํŒŒ์ผ์„ ๋ณด๋Š” ์ˆ˜๋ฐ–์— ์—†๋‹ค. ๋ฐ”๋กœ ์•„๋ž˜์—์„œ ์ด์–ด์ง„๋‹ค.

 

#3-2 ์˜ˆ์‹œ ์ฝ”๋“œ์™€ ๊ทธ ๊ฒฐ๊ณผ

์ฝ”๋“œ

@Composable
private fun ScrollableSample() {
    // ํ•ฉ์‚ฐ ์Šคํฌ๋กค๋Ÿ‰์„ ์ €์žฅํ•  State
    var offset by remember { mutableStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                // ๋งค ์Šคํฌ๋กค๋งˆ๋‹ค ํ˜ธ์ถœ๋˜๋Š” ์ฝœ๋ฐฑ ํ•จ์ˆ˜
                state = rememberScrollableState { delta ->
                    offset += delta // ์ˆ˜์ง ์Šคํฌ๋กค ๋ณ€ํ™”๋Ÿ‰(delta)๋ฅผ offset์— ๋”ํ•ด๋‚˜๊ฐ
                    delta
                }
            )
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}

Box()์— scrollable()์„ ์ ์šฉํ–ˆ๋‹ค. ์ด์ œ ์ด Box()๋Š” ์œ„์—์„œ ๋งํ•œ๋Œ€๋กœ, '์Šคํฌ๋กค์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๊ทธ๋ ‡๋‹ค๊ณ  ์Šคํฌ๋กค์ด ๋˜์ง€๋Š” ์•Š์•„์•ผ' ํ•œ๋‹ค.

 

๊ฒฐ๊ณผ ๊ทธ๋ฆฌ๊ณ  ๋ชฉ์ 

https://developer.android.com/develop/ui/compose/touch-input/pointer-input/scroll#scrollable-modifier

๋‚œ GIF ์ด๋ฏธ์ง€๋ฅผ ๋ณด๊ณ  ๋‚˜์„œ์•ผ ๋น„๋กœ์†Œ, ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•˜๋‹คํ–ˆ์ง€ ๋œ๋‹ค๊ณ ๋Š” ์•ˆ ํ–ˆ๋‹ค๋Š” ๋ง์˜ ๋Šฌ์•™์Šค๋ฅผ ์•Œ๊ฒŒ ๋๋‹ค. ๋˜, scrollable()์˜ ๋ชฉ์ ๋„ ์ง๊ด€์ ์œผ๋กœ ๋Š๊ปด์ง„๋‹ค. ๋ฐ”๋กœ ์Šคํฌ๋กค ์ธก์ •์ด๋‹ค. ์ธก์ •์˜ ์ „์ œ ์กฐ๊ฑด์€, ์Šคํฌ๋กค์ด ๊ฐ€๋Šฅ(able)ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 

#3-3 ScrollableState

scrollable()์˜ ๋งค๊ฐœ๋ณ€์ˆ˜ ์ค‘์—” ScrollableState๊ฐ€ ์žˆ๋‹ค. ์Šคํฌ๋กค(Scroll) ๊ฐ€๋Šฅํ•œ(able) ์ƒํƒœ(State)๋ผ๋‹ˆ. ๋ฐ”๋กœ ์œ„์—์„œ ์–ธ๊ธ‰ํ–ˆ๋˜ scrollable()์˜ ๋ชฉ์ ์— ๊ธฐ๋ฐ˜ํ•ด ์ƒ๊ฐํ•ด ๋ณด๋ฉด ScrollableState๋Š” ์Šคํฌ๋กค ์ธก์ •๊ณผ ๊ด€๋ จ๋œ ๊ฐ์ฒด์ž„์€ ๋ถ„๋ช…ํ•˜๋‹ค. verticalScroll()์˜ ScrollState๊ฐ€ ํ˜„์žฌ ์–ด๋Š ์ •๋„ ์Šคํฌ๋กค ๋˜์—ˆ๋Š”์ง€์˜ ๊ฐ’์„ ๋ณด์œ ํ•˜๋Š” ๊ฒƒ๊ณผ ๋‹ฌ๋ฆฌ, ScrollableState๋Š” ๊ทธ๋Ÿฐ ๊ฐ’ ๋Œ€์‹  ์–ด๋–ค ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ํ•˜๋‚˜๋งŒ์„ ์ €์žฅํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ์ด ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๋ ค๋ฉด ์ƒ์„ฑ์ž์— ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•ด ์ค˜์•ผ ํ•œ๋‹ค.

 

#3-4 ScrollableState์˜ ์ฝœ๋ฐฑ ํ•จ์ˆ˜

์Šคํฌ๋กค ์ธก์ •์˜ ๋ฐฉ์‹์„ ์ •์˜ํ•œ๋‹ค. '์ธก์ •'์ด๋ผ๋Š” ๋‹จ์–ด๋ฅผ ๋ฌธ์žฅ์œผ๋กœ ๋ฐ”๊พธ๋ฉด '์–ผ๋งˆ๋‚˜ ์Šคํฌ๋กค๋˜์—ˆ๋Š”๊ฐ€?'๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด '์ธก์ •์˜ ๋ฐฉ์‹'์€ "์ธก์ •์„ ์–ด๋–ป๊ฒŒ ํŽธํ–ฅ์‹œํ‚ฌ ๊ฒƒ์ธ๊ฐ€?"๊ฐ€ ๋œ๋‹ค. ์ด๋Š” ๋งˆ์น˜ ์ค‘๋ฆฝ์„ ์ง€ํ‚ค์ง€ ์•Š๋Š” ์–ธ๋ก ์‚ฌ๊ฐ€ ์‹ค์ œ ๋ชจ์Šต๊ณผ ๋ณด๋„ ๋‚ด์šฉ์„ ์ƒ์ดํ•˜๊ฒŒ ํŽธํ–ฅ์‹œํ‚ค๋Š” ๊ฒƒ๊ณผ ๊ฐ™๋‹ค.

 

ScrollalbeState๊ฐ€ ๋ณด์œ ํ•˜๋Š” ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋Š” Floatํ˜• ์ˆซ์ž ํ•˜๋‚˜๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›๊ณ , Floatํ˜• ์ˆซ์ž๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๋งค๊ฐœ๋ณ€์ˆ˜ Float๋Š” ์Šค๋งˆํŠธํฐ ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ œ๋กœ ํ™”๋ฉด์— ์†๊ฐ€๋ฝ์„ ์‹ค์ œ๋กœ ๋ฌธ์ง€๋ฅธ ๊ธธ์ด๊ฐ’์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฐ˜ํ™˜ํ•˜๋Š” Float๋Š” 'ํŽธํ–ฅ'์‹œํ‚จ ์ •๋„๋‹ค. ์‚ฌ์šฉ์ž์˜ ์‹ค์ œ ์Šคํฌ๋กค ๊ธธ์ด์˜ 30%๋ฅผ ์‚ญ๊ฐํ•ด์„œ ๋ฒ„๋ฆฌ๊ณ  ์‹ถ์€ ์ƒํ™ฉ์ด๋ผ๊ณ  ๊ฐ€์ •ํ•œ๋‹ค. ์ด ๊ฒฝ์šฐ ์ฝœ๋ฐฑ ํ•จ์ˆ˜๊ฐ€ 100.0F๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„๋„ 30.0F์„ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ์‹์œผ๋กœ, ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ ์‚ฌ์šฉ์ž์˜ ์‹ค์ œ ์Šคํฌ๋กค์„ 'ํŽธํ–ฅ'์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.

 

์ฃผ์˜ํ•  ์ ์€, ๋ฐ˜ํ™˜๋˜๋Š” Float๋ฅผ 30.0F์•„๋‹ˆ๋ผ ๋ฌด์‹ฌ์ฝ” 70.0F๋ผ๊ณ  ์“ฐ์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. 'ํŽธํ–ฅ์ด ๊ฐ€ํ•ด์ง„ ์›๋ณธ'์ด ์•„๋‹ˆ๋ผ 'ํŽธํ–ฅ์‹œํ‚จ ์ •๋„'๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

 

#3-5 ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ 1

์Šคํฌ๋กค์ด ๋˜๊ฒŒ ํ•˜๋ฉด์„œ delta(์›€์ง์ธ ๊ฑฐ๋ฆฌ)๋ฅผ ์ถ”์ ํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ? ๋ฐ”๋กœ scrollable()๊ณผ verticalScroll() (๋˜๋Š” horizontalScroll())๋ฅผ ์—ฐ๋‹ฌ์•„ ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ ํ•˜๋ฉด ๋œ๋‹ค.

 

val scrollableState = rememberScrollableState { delta ->
    println("delta: $delta") // ← ๋Œ€์ถฉ delta๋ฅผ ์ถ”์ฒ™ํ•˜๋Š” ์ฝ”๋“œ๋ผ๊ณ  ๊ฐ€์ •ํ•œ๋‹ค
    delta * 9 / 10 // ์‹ค์ œ ์Šคํฌ๋กค์˜ 90%๋ฅผ ์‚ญ๊ฐํ–ˆ๋‹ค. ๊ต‰์žฅํžˆ ๋Š๋ฆฌ๊ฒŒ ์Šคํฌ๋กค๋  ๊ฒƒ์ด๋‹ค.
}
val scrollState = rememberScrollState()

Column(
    modifier = Modifier
        .fillMaxWidth()
        .verticalScroll(
            state = scrollState
        )
        .scrollable(
            state = scrollableState,
            orientation = Orientation.Vertical
        )
) {
    val textCount = 20
    repeat(textCount) {
        Text(
            text = "(${it + 1} / ${textCount})",
            fontSize = 32.sp,
        )
        Spacer(modifier = Modifier.height(24.dp))
    }
}

์ฝœ๋ฐฑ ํ•จ์ˆ˜๊ฐ€ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์€ delta์˜ 100%๊ฐ€ ์•„๋‹Œ 90% ์†Œ๋น„ํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๊ฝค ํž˜๋“ค ๊ฒƒ์ด๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๊ฐ™์€ ์–‘์„ ์Šคํฌ๋กค ํ•˜๊ธฐ ์œ„ํ•ด์„œ ํ‰์†Œ๋ณด๋‹ค ์†๊ฐ€๋ฝ์„ 10๋ฐฐ๋‚˜ ๋” ์›€์ง์—ฌ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

#3-6 ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ 2

val scrollableState = rememberScrollableState { delta ->
    println("delta: $delta") // ← ๋Œ€์ถฉ delta๋ฅผ ์ถ”์ฒ™ํ•˜๋Š” ์ฝ”๋“œ๋ผ๊ณ  ๊ฐ€์ •ํ•œ๋‹ค
    delta * 9 / 10 // ์‹ค์ œ ์Šคํฌ๋กค์˜ 90%๋ฅผ ์‚ญ๊ฐํ–ˆ๋‹ค. ๊ต‰์žฅํžˆ ๋Š๋ฆฌ๊ฒŒ ์Šคํฌ๋กค๋  ๊ฒƒ์ด๋‹ค.
}
val scrollState = rememberScrollState()

Column(
    modifier = Modifier
        .fillMaxWidth()
        .scrollable(
            state = scrollableState,
            orientation = Orientation.Vertical
        )
        .verticalScroll(
            state = scrollState
        )
) {
    val textCount = 20
    repeat(textCount) {
        Text(
            text = "(${it + 1} / ${textCount})",
            fontSize = 32.sp,
        )
        Spacer(modifier = Modifier.height(24.dp))
    }
}

#3-5์™€ ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ ์ˆœ์„œ๋ฅผ ์ œ์™ธํ•˜๋ฉด ์™„์ „ํžˆ ๋™์ผํ•œ ์ฝ”๋“œ๋‹ค. ํ•˜์ง€๋งŒ ์Šคํฌ๋กค์ด ํ‰๋ฒ”ํ•˜๊ฒŒ ์ž˜ ๋œ๋‹ค. ์™œ๋ƒํ•˜๋ฉด scrollableState์˜ ์ฝœ๋ฐฑ ํ•จ์ˆ˜๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์•˜๊ธฐ์— ๊ทธ๋ ‡๋‹ค. Modifier์—์„œ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉํ–ฅ์€ (๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ ์ˆœ์„œ์˜) ์—ญ๋ฐฉํ–ฅ์ด๋ผ๋Š” ๊ฒƒ์„ ๊ธฐ์–ตํ•˜์ž. ์‹ค์ œ๋กœ ์Šคํฌ๋กค์ด ๋˜๊ฒŒ ํ•˜๋Š” verticalScroll() ์ž…์žฅ์—์„œ๋Š” ํŽธํ–ฅ๋˜๊ธฐ ์ „์˜ ์Šคํฌ๋กค ์ด๋™๋Ÿ‰(delta)์„ ๋ฐ›์•„๋ณธ ์…ˆ์ด๋‹ˆ ์Šคํฌ๋กค์ด ์ •์ƒ์ ์œผ๋กœ ๋ฉ€์ฉกํžˆ ๋ผ๋ฒ„๋ฆฐ ๊ฒƒ์ด๋‹ค.

 

#4 ์š”์•ฝ

verticalScroll()๊ณผ scrollable() ๋‘˜ ๋‹ค ์ปดํฌ์ €๋ธ”์„ ์Šคํฌ๋กค ๊ฐ€๋Šฅ์ผ€ ๋งŒ๋“ ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ์Šคํฌ๋กค ๋˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฑด verticalScroll() ๋ฟ์ด๋‹ค.

 

#5 ์ด์–ด์ง€๋Š” ๊ธ€

 

[Android] Pointer input - Nested Scroll

#1 ์ด์ „ ๊ฒŒ์‹œ๊ธ€ [Android] Pointer input - Scroll#1 ๊ฐœ์š” ์Šคํฌ๋กค  |  Jetpack Compose  |  Android Developers์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์Šคํฌ๋กค ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •

kenel.tistory.com