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

[Android] Pointer input - Nested Scroll

interfacer_han 2025. 2. 18. 18:52

#1 ์ด์ „ ๊ฒŒ์‹œ๊ธ€

 

[Android] Pointer input - Scroll

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

kenel.tistory.com

์Šคํฌ๋กค์— ๋Œ€ํ•ด ๋‹ค๋ฃฌ ์œ„ ๊ฒŒ์‹œ๊ธ€์—์„œ๋ถ€ํ„ฐ ์ด์–ด์ง„๋‹ค.

 

#2 ์ค‘์ฒฉ(Nested) ์Šคํฌ๋กค

#2-1 ๊ฐœ์š”

์ค‘์ฒฉ(Nested) ์Šคํฌ๋กค์ด๋ž€, ๋ง ๊ทธ๋Œ€๋กœ ๊ฐ™์€ ๋ฐฉํ–ฅ(์ˆ˜์ง ๋˜๋Š” ์ˆ˜ํ‰)์œผ๋กœ ์Šคํฌ๋กค ํ•˜๋Š” ์ปจํ…Œ์ด๋„ˆ 2๊ฐœ๊ฐ€ ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„๋ฅผ ํ˜•์„ฑํ•œ ๊ฒƒ์„ ๋œปํ•œ๋‹ค. ์ฆ‰ verticalScroll()์ด ์ ์šฉ๋œ Column() ์†์— ๋‹ค์‹œ verticalScroll()์ด ์ ์šฉ๋œ Column()์„ ๋„ฃ์œผ๋ฉด, ๋ฐ–์— ์žˆ๋Š” ๋ถ€๋ชจ Column()๊ณผ ์•ˆ์— ์žˆ๋Š” ์ž์‹ Column()์ด ์ค‘์ฒฉ ์Šคํฌ๋กค์„ ์ด๋ฃจ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

#2-2 ์•”์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค

์ฝ”๋“œ

@Composable
private fun AutomaticNestedScroll() {
    val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
    Box(
        modifier = Modifier
            .background(Color.LightGray)
            .height(480.dp)
            .verticalScroll(rememberScrollState())
            .padding(32.dp)
    ) {
        Column {
            repeat(6) {
                Box(
                    modifier = Modifier
                        .height(128.dp)
                        .verticalScroll(rememberScrollState())
                ) {
                    Text(
                        "Scroll here",
                        modifier = Modifier
                            .border(12.dp, Color.DarkGray)
                            .background(brush = gradient)
                            .padding(24.dp)
                            .height(150.dp)
                    )
                }
            }
        }
    }
}

verticalScroll()์ด ์ ์šฉ๋œ Column() ์†์— ๋‹ค์‹œ verticalScroll()์ด ์ ์šฉ๋œ Column()์„ ๋„ฃ์€ ์ฝ”๋“œ๋‹ค. ์ด๋ ‡๊ฒŒ๋งŒ ํ•ด๋„ ์•”์‹œ์ ์œผ๋กœ(= ์•Œ์•„์„œ) ์ค‘์ฒฉ ์Šคํฌ๋กค์ด ์ˆ˜ํ–‰๋œ๋‹ค. Jetpack Compose์—์„œ ์ด๋Ÿฐ ์•”์‹œ์ ์œผ๋กœ ์ค‘์ฒฉ ์Šคํฌ๋กค์ด ๋˜๋Š” Modifier์˜ ํ™•์žฅํ•จ์ˆ˜๋Š” verticalScroll(), horizontalScroll(), scrollable()์ด ์žˆ๊ณ , ์ปดํฌ์ €๋ธ”๋กœ๋Š” Lazy lists, TextField()๊ฐ€ ์žˆ๋‹ค.

 

๊ฒฐ๊ณผ

https://developer.android.com/develop/ui/compose/touch-input/pointer-input/scroll#auto-nested-scrolling

๋ถ€๋ชจ ์˜์—ญ์„ ์Šคํฌ๋กค ํ•  ๋•Œ๋Š” ํ‰๋ฒ”ํ•˜๊ฒŒ ์Šคํฌ๋กค ๋œ๋‹ค. ์ž์‹ ์˜์—ญ์„ ์Šคํฌ๋กค ํ•  ๋•Œ๋Š” ์ž์‹ ์˜์—ญ์ด ์Šคํฌ๋กค ๋˜๋˜, ์ž์‹ ์˜์—ญ์—์„œ ๋” ์ด์ƒ ์Šคํฌ๋กค ํ•  ์˜์—ญ์ด ๋‚จ์•„์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ ๋ถ€๋ชจ ์˜์—ญ์ด ๋Œ€์‹  ์Šคํฌ๋กค ๋œ๋‹ค. ์ด๋Š” ์šฐ๋ฆฌ๊ฐ€ ์ผ์ƒ์—์„œ ์ธํ„ฐ๋„ท ์›น์„œํ•‘ ํ˜น์€ ์Šค๋งˆํŠธํฐ์„ ์‚ฌ์šฉํ•  ๋•Œ ๊ธฐ๋Œ€ํ•˜๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ์Šคํฌ๋กค ์•„๋‹Œ๊ฐ€?

 

#2-3 ๋ช…์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค ์‚ฌ์šฉ ์‹œ๊ธฐ

์•”์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์—์„œ ๋ณด์•˜๋“ฏ ์ž์‹ ์ปจํ…Œ์ด๋„ˆ → ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ์˜ ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋ฒคํŠธ๊ฐ€ ์ „ํŒŒ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ผ๋Š” ๊ฒŒ ์ผ๋ฐ˜์ ์ด๋ฉฐ ์ž์—ฐ์Šค๋Ÿฌ์šด ๊ฒฝ์šฐ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ๋ช…์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์€ ์–ธ์ œ ์“ฐ๋Š”๊ฐ€?

 

์ฒซ์งธ๋กœ ์ž์—ฐ์Šค๋Ÿฝ์ง€ ์•Š์€ ๊ฒฝ์šฐ, ๋ฐ”๋กœ ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ → ์ž์‹ ์ปจํ…Œ์ด๋„ˆ์˜ ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ์ „ํŒŒ์‹œํ‚ค๊ณ  ์‹ถ์„ ๋•Œ๋‹ค. ์•”์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์ด ์ž์‹ ์ปจํ…Œ์ด๋„ˆ → ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ์˜ ๋ฐฉํ–ฅ์œผ๋กœ ๊ณ ์ •๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ๋ช…์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์„ ํ†ตํ•ด ์ด ๋ฐฉํ–ฅ์—์„œ ๋ฒ—์–ด๋‚˜๋ ค๋Š” ๊ฒƒ์ด๋‹ค.

 

๋‘˜์งธ๋กœ๋Š” ์•”์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ž์‹ ์ปจํ…Œ์ด๋„ˆ → ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ์˜ ๋ฐฉํ–ฅ์€ ์œ ์ง€ํ•˜์ง€๋งŒ, ๊ทธ ์ด๋ฒคํŠธ ์ „ํŒŒ ๊ณผ์ • ์ค‘๊ฐ„์— ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ ํŠน์ •ํ•œ ๋กœ์ง์„ ๋„ฃ๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ๋‹ค.

 

#3 ๋ช…์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค Modifier

#3-1 ์ „์ œ ์กฐ๊ฑด

์•”์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์ด ๋˜๋Š” ๊ตฌ์กฐ์—ฌ์•ผ ํ•œ๋‹ค. ์ฆ‰, #2์— ์žˆ๋Š” ๊ตฌ์กฐ์—ฌ์•ผ ํ›„์ˆ ํ•  Modifier.nestedScroll()์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ง์ด๋‹ค. ์•”์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์ด ๋˜์ง€ ์•Š๋Š” ๊ตฌ์กฐ์ธ๋ฐ, Modifier.nestedScroll()๋งŒ ์ ์šฉํ•˜๋ฉด ์Šคํฌ๋กค ์ž์ฒด๊ฐ€ ๋˜์ง€ ์•Š๋Š”๋‹ค.  

 

#3-2 Modifier.nestedScroll()

fun Modifier.nestedScroll(
    connection: NestedScrollConnection,
    dispatcher: NestedScrollDispatcher? = null
): Modifier

Modifier.nestedScroll()์€ ๋ช…์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋„๊ตฌ๋‹ค. Modifier.nestedScroll()์˜ ์ฝ”๋“œ ์ž์ฒด๋Š” ๋ณ„ ๋ณผ์ผ์ด ์—†๋‹ค. ๋ช…์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค ๊ตฌํ˜„์˜ ๋ณธ ๊ฒŒ์ž„์€ NestedScrollConnection์—์„œ ์ผ์–ด๋‚œ๋‹ค.

 

#3-3 NestedScrollConnection

/**
 * ์ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ Modifier.nestedScroll()์— ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•˜์—ฌ
 * ์ค‘์ฒฉ ์Šคํฌ๋กค ๊ณ„์ธต ๊ตฌ์กฐ์— '์ฐธ์—ฌ'ํ•  ์ˆ˜ ์žˆ๋‹ค. '์ฐธ์—ฌ'ํ•œ ์ดํ›„๋ถ€ํ„ฐ๋Š”
 * '์Šคํฌ๋กค ์ž์‹'์˜ ์Šคํฌ๋กค ํ˜น์€ NestedScrollDispatcher๋ฅผ ํ†ตํ•ด ํŠธ๋ฆฌ๊ฑฐ๋œ
 * ์Šคํฌ๋กค ์ด๋ฒคํŠธ๋ฅผ '๋ถ€๋ชจ'๋กœ์„œ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ๋‹ค.
 */
@JvmDefaultWithCompatibility
interface NestedScrollConnection {

    /**
     * ์ž์‹์—๊ฒŒ ์ „๋‹ฌ๋œ ๋“œ๋ž˜๊ทธ ์ด๋ฒคํŠธ์˜ ์ผ๋ถ€ ๋“œ๋ž˜๊ทธ๋Ÿ‰(delta)์„
     * ์ž์‹ (๋ถ€๋ชจ)์ด ๋ฏธ๋ฆฌ '์†Œ๋น„(consume)'ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜
     * ๋งค๊ฐœ๋ณ€์ˆ˜ available: ์–ผ๋งˆ๋‚˜ ๋ฏธ๋ฆฌ '์†Œ๋น„(consume)'ํ•  ์ˆ˜ ์žˆ๋Š”์ง€์˜ ์ด๋Ÿ‰
     * ๋งค๊ฐœ๋ณ€์ˆ˜ source: ์Šคํฌ๋กค ์ด๋ฒคํŠธ๊ฐ€ ์†๊ฐ€๋ฝ์ธ์ง€ ๋งˆ์šฐ์Šคํœ ์— ์˜ํ•œ ๊ฒƒ์ธ์ง€ ๋“ฑ ๊ตฌ๋ถ„
     * ๋ฐ˜ํ™˜๊ฐ’: ๋ณธ ํ•จ์ˆ˜์—์„œ '์†Œ๋น„(consume)'ํ•œ ์–‘
     */
    fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero

    /**
     * ์‚ฌํ›„ ๋“œ๋ž˜๊ทธ. ์Šคํฌ๋กค์ด ๋‚จ๋Š” ๋ถ€๋ถ„์„ ์ž์‹์œผ๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์•„
     * ์ž์‹ (๋ถ€๋ชจ)์ด '์†Œ๋น„(consume)'ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜
     * ๋งค๊ฐœ๋ณ€์ˆ˜ consumed: ๋ณธ ํ•จ์ˆ˜๊นŒ์ง€ ์˜ค๋Š” ๊ณผ์ •์—์„œ ํ•ฉ์‚ฐ๋œ
     *     (๋ณต์ˆ˜์˜ onPreScroll() ๋ฐ˜ํ™˜๊ฐ’) + (์‹ค์ œ ํ™”๋ฉด์ƒ์˜ ์Šคํฌ๋กค) + (๋ณต์ˆ˜์˜ onPostScroll() ๋ฐ˜ํ™˜๊ฐ’)
     * ๋งค๊ฐœ๋ณ€์ˆ˜ available: ์–ผ๋งˆ๋‚˜ ๋” '์†Œ๋น„(consume)'ํ•  ์ˆ˜ ์žˆ๋Š”์ง€์˜ ์ด๋Ÿ‰
     * ๋งค๊ฐœ๋ณ€์ˆ˜ source: ์Šคํฌ๋กค ์ด๋ฒคํŠธ๊ฐ€ ์†๊ฐ€๋ฝ์ธ์ง€ ๋งˆ์šฐ์Šคํœ ์— ์˜ํ•œ ๊ฒƒ์ธ์ง€ ๋“ฑ ๊ตฌ๋ถ„
     * ๋ฐ˜ํ™˜๊ฐ’: ๋ณธ ํ•จ์ˆ˜์—์„œ '์†Œ๋น„(consume)'ํ•œ ์–‘
     */
    fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset =
        Offset.Zero

    /**
     * ์ž์‹์—๊ฒŒ ์ „๋‹ฌ๋œ ํ”Œ๋ง ์ด๋ฒคํŠธ์˜ ์ผ๋ถ€ ์†๋ ฅ(velocity)์„
     * ์ž์‹ (๋ถ€๋ชจ)์ด ๋ฏธ๋ฆฌ '์†Œ๋น„(consume)'ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜
     * ๋งค๊ฐœ๋ณ€์ˆ˜ available: ์–ผ๋งˆ๋‚˜ ๋ฏธ๋ฆฌ '์†Œ๋น„(consume)'ํ•  ์ˆ˜ ์žˆ๋Š”์ง€์˜ ์ด๋Ÿ‰
     * ๋ฐ˜ํ™˜๊ฐ’: ๋ณธ ํ•จ์ˆ˜์—์„œ '์†Œ๋น„(consume)'ํ•œ ์–‘
     */
    suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero

    /**
     * ์‚ฌํ›„ ํ”Œ๋ง. ํ”Œ๋ง ์†๋ ฅ(velocity)์ด ๋‚จ๋Š” ์ •๋„๋ฅผ ์ž์‹์œผ๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์•„
     * ์ž์‹ (๋ถ€๋ชจ)์ด '์†Œ๋น„(consume)'ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜
     * ๋งค๊ฐœ๋ณ€์ˆ˜ consumed: ๋ณธ ํ•จ์ˆ˜๊นŒ์ง€ ์˜ค๋Š” ๊ณผ์ •์—์„œ ํ•ฉ์‚ฐ๋œ
     *     (๋ณต์ˆ˜์˜ onPreFling() ๋ฐ˜ํ™˜๊ฐ’) + (์‹ค์ œ ํ™”๋ฉด์ƒ์˜ ํ”Œ๋ง) + (๋ณต์ˆ˜์˜ onPreFling() ๋ฐ˜ํ™˜๊ฐ’)
     * ๋งค๊ฐœ๋ณ€์ˆ˜ available: ์–ผ๋งˆ๋‚˜ ๋” '์†Œ๋น„(consume)'ํ•  ์ˆ˜ ์žˆ๋Š”์ง€์˜ ์ด๋Ÿ‰
     * ๋ฐ˜ํ™˜๊ฐ’: ๋ณธ ํ•จ์ˆ˜์—์„œ '์†Œ๋น„(consume)'ํ•œ ์–‘
     */
    suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
        return Velocity.Zero
    }
}

์ด NestedScrollConnection์˜ 4๊ฐ€์ง€ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด, ๋ช…์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์ด ๊ตฌํ˜„๋œ๋‹ค. ๋‹ค๋งŒ, ์ฒ˜์Œ์—๋Š” ๊ฝค ๋ณต์žกํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ช…์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์˜ ๊ธฐ์ œ๋ฅผ ์„ ์ œ์ ์œผ๋กœ ์•Œ์•„๋‘˜ ํ•„์š”๊ฐ€ ์žˆ๋‹ค. ์ฃผ์„์€ ๊ณต์‹ ์ฝ”๋“œ์— ๋ถ™์–ด์žˆ๋˜ ๊ฑธ ๋ฒˆ์—ญํ•œ ๊ฒƒ์ด๋‹ค. ์—ฌ๊ธฐ์„œ ๊ผผ๊ผผํžˆ ์ฝ์œผ๋ฉฐ ์ดํ•ดํ•  ํ•„์š”๋Š” ์—†๋‹ค. ์–ด์ฐจํ”ผ ์•„๋ž˜์—์„œ๋ถ€ํ„ฐ ์„ค๋ช…ํ•œ๋‹ค.

 

#3-4 NestedScrollConnection์˜ ๊ธฐ์ œ

NestedScrollConnection์˜ ๊ธฐ์ œ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ๋„์‹๋„๋กœ, ๋งˆ์น˜ ์•กํ‹ฐ๋น„ํ‹ฐ์˜ ์ƒ๋ช…์ฃผ๊ธฐ์ฒ˜๋Ÿผ ์ค‘์ฒฉ ์Šคํฌ๋กค์ด ์–ด๋–ค ๊ณผ์ •์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋Š”์ง€๋ฅผ ํ‘œํ˜„ํ•œ ๊ฒƒ์ด๋‹ค. Modifier์— verticalScroll() ๋ฐ nestedScroll()์ด ๋ถ™์€ ์ปดํฌ์ €๋ธ” Parent2, Parent1, Child๊ฐ€ ์กด์žฌํ•œ๋‹ค๊ณ  ํ•ด๋ณด์ž. Parent2 ๋‚ด๋ถ€์—๋Š” Parent1์ด ์žˆ๊ณ , Parent1 ๋‚ด๋ถ€์—๋Š” Child๊ฐ€ ์žˆ๋Š” ์ƒํ™ฉ์ด๋‹ค. ์ด ๊ตฌ์กฐ์—์„œ ์Šค๋งˆํŠธํฐ ์‚ฌ์šฉ์ž๊ฐ€ Child์— ์Šคํฌ๋กค์„ ๊ฐ€ํ•œ ์ƒํ™ฉ์ด๋ผ ๊ฐ€์ •ํ•œ๋‹ค.

 

onPreScroll() ์‹คํ–‰๋งŒ์„ ์œ„ํ•œ ํŠน๋ณ„ํ•œ ๋ฒ„๋ธ”๋ง

Jetpack Compose์—์„œ ์ปดํฌ์ €๋ธ” ๊ฐ„ ์ด๋ฒคํŠธ ์ „ํŒŒ์˜ ์ˆœ์„œ ๊ทธ๋ฆฌ๊ณ  ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ ๋œ Modifier ๊ฐ„ ์ด๋ฒคํŠธ ์ „ํŒŒ์˜ ์ˆœ์„œ๋Š” ๊ฐ๊ฐ ์„ค๊ณ„์˜ ์—ญ๋ฐฉํ–ฅ, ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ์ž์‹ → ๋ถ€๋ชจ ๊ทธ๋ฆฌ๊ณ  (ํ›„์— ์ฒด์ด๋‹๋œ ๋ฉ”์†Œ๋“œ) → (์ „์— ์ฒด์ด๋‹๋œ ๋ฉ”์†Œ๋“œ)๋‹ค. ์ด๋Š” #2-2์—์„œ ๋งํ•œ, ์šฐ๋ฆฌ๊ฐ€ ์ผ์ƒ์ ์œผ๋กœ ์›น๋ธŒ๋ผ์šฐ์ €๋‚˜ ์Šค๋งˆํŠธํฐ์„ ์‚ฌ์šฉํ•˜๋ฉฐ ๊ธฐ๋Œ€ํ•˜๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฐฉํ–ฅ์ด๋‹ค. ๋˜, ์ด๋ฅผ ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง(Event bubbling)์ด๋ผ๊ณ ๋„ ๋ถ€๋ฅธ๋‹ค.

 

๊ทธ๋Ÿฌ๋‚˜ #2-3์—์„œ ๋งํ–ˆ๋“ฏ, ๋ช…์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์€ (ํ”„๋กœ๊ทธ๋ž˜๋จธ์— ์˜ํ•ด ์˜๋„์ ์œผ๋กœ) ์ž์—ฐ์Šค๋Ÿฝ์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค. ์ฆ‰ ๋ถ€๋ชจ๊ฐ€ ์ž์‹๋ณด๋‹ค ๋จผ์ € ์‚ฌ์šฉ์ž์˜ ์Šคํฌ๋กค์— ๋ฐ˜์‘ํ•ด์•ผ ํ•œ๋‹ค. ๊ทธ '๋ฐ˜์‘'์„ ๋Œ€๋ณ€ํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ ๋ฐ”๋กœ onPreScroll()์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  onPreScroll()์˜ ์‹คํ–‰์„ ๋ถ€๋ชจ → ์ž์‹์˜ ๋ฐฉํ–ฅ์œผ๋กœ ํ•˜๊ธฐ ์œ„ํ•ด์„  ๊ฐ€์žฅ ๊ทน๋‹จ์— ์œ„์น˜ํ•œ ํ•œ ๋ถ€๋ชจ๋ฅผ ์ฐพ์•„์•ผ ํ•  ๊ฒƒ์ด๋‹ค. ๊ทธ๋ž˜์„œ 'ํŠน๋ณ„ํ•œ' ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง์„ ํ†ตํ•ด ๊ฐ€์žฅ ๋์— ์žˆ๋Š” ๋ถ€๋ชจ๊นŒ์ง€ ๊ฐ„ ํ›„, ๋‹ค์‹œ ๋Œ์•„์˜ค๋ฉฐ ์—ญ์ˆœ์œผ๋กœ onPreScroll()์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. 'ํŠน๋ณ„'ํ•˜๋‹ค๊ณ  ํ•œ ์ด์œ ๋Š”, ์ด ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง์ด Modifier.verticalScroll()์ด๋‚˜ Modifier.scrollable() ๋“ฑ์˜ ๋‹ค๋ฅธ ๋ฉ”์†Œ๋“œ๋“ค์€ ๋ฌด์‹œํ•˜๊ณ  ์˜ค์ง Modifier.nestedScroll()๋งŒ์„ ์ถ”์ ํ•ด ์˜ฌ๋ผ๊ฐ€๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ฆ‰ ์ด ๊ณผ์ •์—์„œ nestedScroll()์ด ์•„๋‹Œ ๋‹ค๋ฅธ ์Šคํฌ๋กค ๊ด€๋ จ ๋ฉ”์†Œ๋“œ๋“ค์€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.

 

onPreScroll() ์‹คํ–‰

์•ž์„œ ์ˆ˜ํ–‰ํ•œ 'ํŠน๋ณ„' ๋ฒ„๋ธ”๋ง์€ onPreScroll()์˜ ์ˆ˜ํ–‰ ์ˆœ์„œ๋ฅผ ์•Œ์•„๋‚ด๊ธฐ ์œ„ํ•œ ์ž‘์—…์ด์—ˆ๋‹ค. ๋ฒ„๋ธ”๋ง์€ ์ž์‹ → ๋ถ€๋ชจ ๋ฐฉํ–ฅ์œผ๋กœ ์ „ํŒŒ๋˜๋Š”๋ฐ, Jetpack Compose ๋Ÿฐํƒ€์ž„์€ ์ด ์ˆœ์„œ๋ฅผ ๋’ค์ง‘์–ด ๋ถ€๋ชจ → ์ž์‹ ๋ฐฉํ–ฅ์œผ๋กœ onPreScroll()์„ ์‹คํ–‰ํ•ด ๋‚˜๊ฐ„๋‹ค. onPreScoll()์€ ์•”์‹œ์  ์ค‘์ฒฉ ์Šคํฌ๋กค์—์„œ๋ผ๋ฉด ์ž์‹์ด ํ–ˆ์„ ์Šคํฌ๋กค์„, ๋ถ€๋ชจ์—๊ฒŒ "ํ˜น์‹œ ์ด ์Šคํฌ๋กค๋Ÿ‰ ์ค‘ ์ผ๋ถ€๋ฅผ ์—„๋งˆใ†์•„๋น ๊ฐ€ ๊ฐ€์ ธ๊ฐˆ๋ž˜('์†Œ๋น„'ํ• ๋ž˜)?"๋ผ๊ณ  ๋ฌป๊ฒŒ ๋งŒ๋“ ๋‹ค (์ฐธ๊ณ ๋กœ "์†Œ๋น„๋œ๋‹ค"๋Š” ๋ง์ด "๋ถ€๋ชจ๊ฐ€ ์Šคํฌ๋กค๋œ๋‹ค"๋Š” ๋œป์€ ์•„๋‹ˆ๋‹ค. ๋ถ€๋ชจ๋ฅผ ์Šคํฌ๋กค์‹œํ‚ค๋ ค๋ฉด, onPreScroll() ๋‚ด๋ถ€์—์„œ ๋ณ„๋„๋กœ scrollState.scrollBy()๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ ๊ตฌํ˜„ํ•˜๋ฉด ๋œ๋‹ค).

 

์‹ค์ œ ์Šคํฌ๋กค

P2, P1, C์˜ onPreScroll()๋“ค์„ ์ˆœ์„œ๋Œ€๋กœ ๊ฑฐ์น˜๊ณ  ๋‚จ์€ ์Šคํฌ๋กค๋Ÿ‰์„ ์‹ค์ œ๋กœ ์Šคํฌ๋กคํ•˜๋Š” ๋ถ€๋ถ„์ด๋‹ค (๋˜, ์‹ค์ œ๋กœ ์Šคํฌ๋กค๋œ๋งŒํผ ์Šคํฌ๋กค๋Ÿ‰์ด '์†Œ๋น„'๋œ๋‹ค).

 

onPostScroll() ์‹คํ–‰

์‹ค์ œ ์Šคํฌ๋กค์„ ํ•˜๊ณ ๋„ ๋‚จ์€ ๋ถ€๋ถ„์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํ™”์‚ดํ‘œ๋‹ค. '์Šคํฌ๋กค์„ ํ•˜๊ณ  ๋‚จ์€ ๋ถ€๋ถ„'์ด๋ผ๋Š” ๋ง์€ ์ง๊ด€์ ์œผ๋กœ ๋‚ฉ๋“๋˜์ง€ ์•Š๋Š” ๋ง์ด๋‹ค. ๋งˆ์น˜ '์†Œ๋ฆฌ์—†๋Š” ์•„์šฐ์„ฑ' ๋”ฐ์œ„์˜ ์‹œ์  ํ‘œํ˜„์ฒ˜๋Ÿผ ๋А๊ปด์ง€์ง€ ์•Š๋Š”๊ฐ€? ํ•˜์ง€๋งŒ ์Šคํฌ๋กค์„ ํ•˜๊ณ ๋„ ์ •๋ง๋กœ '๋‚จ๋Š”' ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค. ์–ด๋–ค ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋๊นŒ์ง€ ์Šคํฌ๋กค ํ–ˆ์Œ์—๋„ ์‚ฌ์šฉ์ž๊ฐ€ ์†๊ฐ€๋ฝ์„ ์—ฌ์ „ํžˆ ์›€์ง์ด๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ๋ฅผ ๋– ์˜ฌ๋ ค๋ณด์ž. ๊ทธ๋Ÿฐ ๋•Œ '์Šคํฌ๋กค์ด ๋‚จ๋Š”๋‹ค'๋ผ๊ณ  ํ‘œํ˜„ํ•œ๋‹ค.

 

#4 ์ˆœ์„œ ํ™•์ธ์šฉ ์ƒ˜ํ”Œ ์•ฑ

#4-1 ๊ฐœ์š”

์œ„์—์„œ ์„ค๋ช…ํ•œ ๋‚ด์šฉ์ด ์ •๋ง๋กœ ๋งž๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์ƒ˜ํ”Œ ์•ฑ์„ ๋งŒ๋“ค์–ด๋ดค๋‹ค.

 

#4-2 UI ๋ถ€๋ถ„

// in MainActivity.kt

val p2Count = 1
val p1Count = 5
val cCount = 10

repeat(p2Count) { p2Index ->
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(innerPadding)
            .background(Color.White)
            .nestedScrollWithLogger("1")
            .scrollableWithLogger("2")
            .nestedScrollWithLogger("3")
            .scrollableWithLogger("4")
            .verticalScrollWithLogger("4.5")
            .nestedScrollWithLogger("5")
            .scrollableWithLogger("6")
            .nestedScrollWithLogger("7")
            .scrollableWithLogger("8")
    ) {
        repeat(p1Count) { p1Index ->
            Column(
                modifier = Modifier
                    .padding(
                        start = 24.dp,
                        top = 24.dp,
                        bottom = 24.dp,
                        end = 96.dp
                    )
                    .fillMaxWidth()
                    .height(500.dp)
                    .background(Color.LightGray)
                    .nestedScrollWithLogger("9")
                    .scrollableWithLogger("10")
                    .nestedScrollWithLogger("11")
                    .scrollableWithLogger("12")
                    .verticalScrollWithLogger("12.5")
                    .nestedScrollWithLogger("13")
                    .scrollableWithLogger("14")
                    .nestedScrollWithLogger("15")
                    .scrollableWithLogger("16")
            ) {
                repeat(cCount) { cIndex ->
                    Column(
                        modifier = Modifier
                            .padding(
                                start = 24.dp,
                                top = 24.dp,
                                bottom = 24.dp,
                                end = 96.dp
                            )
                            .height(300.dp)
                            .background(Color.Gray)
                            .nestedScrollWithLogger("17")
                            .scrollableWithLogger("18")
                            .nestedScrollWithLogger("19")
                            .scrollableWithLogger("20")
                            .verticalScrollWithLogger("20.5")
                            .nestedScrollWithLogger("21")
                            .scrollableWithLogger("22")
                            .nestedScrollWithLogger("23")
                            .scrollableWithLogger("24")
                    ) {
                        SampleTexts()
                    }
                }
            }
        }
    }
}

์œ„ ์ฝ”๋“œ์™€ ๊ฐ™์ด ํ•œ ์ปดํฌ์ €๋ธ”์— nestedScroll()์„ ๋‹ฌ ์ˆ˜๋„ ์žˆ๋‹ค. ์ด ๊ฒฝ์šฐ, ๋จผ์ € ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹๋œ nestedScroll()์ด ๋ถ€๋ชจ๊ณ  ๋’ค์ชฝ์— ์ฒด์ด๋‹๋œ nestedScroll()์„ ์ž์‹์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

 

#4-3 scrollableWithLogger()

@Composable
@SuppressLint("ModifierFactoryUnreferencedReceiver")
private fun Modifier.scrollableWithLogger(id: String): Modifier {
    return this.scrollable(
        state = ScrollableState {
            println("$id scrollable() available delta: $it")
            0F
        },
        orientation = Orientation.Vertical
    )
}

๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•˜๋Š” scrollable() ํ•จ์ˆ˜.

 

#4-4 verticalScrollWithLogger()

/* println()์ด ์ข€ ๋Šฆ๊ฒŒ ์ถœ๋ ฅ๋œ๋‹ค
 * ์‹ค์ œ๋กœ verticalScroll()์ด ๋Šฆ๊ฒŒ ์ˆ˜ํ–‰๋œ ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค
 * ScrollState.value์˜ ๋ณ€ํ™”๋ฅผ LaunchedEffect๋กœ ์ถ”์ ํ•˜๊ณ 
 * LaunchedEffect ๋‚ด์—์„œ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•˜๊ธฐ์— ์‹œ์ฐจ๊ฐ€ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค
 * ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ ์ž์ฒด๊ฐ€, ์‹ค์ œ ์Šคํฌ๋กค์ด ๋˜๋Š” ์ˆœ๊ฐ„ ์ถœ๋ ฅ๋˜๊ฒŒ ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š์•˜๋‹ค๋Š” ์–˜๊ธฐ๋‹ค
 */
@Composable
@SuppressLint("ModifierFactoryUnreferencedReceiver")
private fun Modifier.verticalScrollWithLogger(id: String): Modifier {
    val scrollState = remember {
        ScrollState(0)
    }

    LaunchedEffect(scrollState.value) {
        println("$id verticalScroll()")
    }

    return this.verticalScroll(
        state = scrollState
    )
}

๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•˜๋Š” verticalScroll() ํ•จ์ˆ˜.

 

#4-5 nestedScrollWithLogger()

@Composable
@SuppressLint("ModifierFactoryUnreferencedReceiver")
fun Modifier.nestedScrollWithLogger(id: String): Modifier {
    return this.nestedScroll(NestedScrollConnectionWithSimpleLogger(id))
}

class NestedScrollConnectionWithSimpleLogger(private val id: String) : NestedScrollConnection {

    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        println("$id onPreScroll available delta: $available")
        return Offset.Zero
    }

    override fun onPostScroll(
        consumed: Offset, available: Offset, source: NestedScrollSource
    ): Offset {
        println("$id onPostScroll available delta: $available")
        return Offset.Zero
    }

    override suspend fun onPreFling(available: Velocity): Velocity {
        //println("$id onPreFling available velocity: $available")
        return Velocity.Zero
    }

    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
        //println("$id onPostFling available velocity: $available")
        return Velocity.Zero
    }
}

๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•˜๋Š” nestedScroll() ํ•จ์ˆ˜. ๊ฐ ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’์€ NestedScrollConnection ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ธฐ๋ณธ๊ฐ’์ด๋‹ค.

 

#4-6 ์Šคํฌ๋ฆฐ์ƒท

 

#4-7 ์ถœ๋ ฅ ๊ฒฐ๊ณผ

1 onPreScroll available delta: Offset(0.0, -13.5)
3 onPreScroll available delta: Offset(0.0, -13.5)
5 onPreScroll available delta: Offset(0.0, -13.5)
7 onPreScroll available delta: Offset(0.0, -13.5)
9 onPreScroll available delta: Offset(0.0, -13.5)
11 onPreScroll available delta: Offset(0.0, -13.5)
13 onPreScroll available delta: Offset(0.0, -13.5)
15 onPreScroll available delta: Offset(0.0, -13.5)
17 onPreScroll available delta: Offset(0.0, -13.5)
19 onPreScroll available delta: Offset(0.0, -13.5)
21 onPreScroll available delta: Offset(0.0, -13.5)
23 onPreScroll available delta: Offset(0.0, -13.5)
24 scrollable() available delta: -13.575195
23 onPostScroll available delta: Offset(0.0, -13.5)
22 scrollable() available delta: -13.575195
21 onPostScroll available delta: Offset(0.0, -13.5)
20 scrollable() available delta: 0.0
19 onPostScroll available delta: Offset(0.0, 0.0)
18 scrollable() available delta: 0.0
17 onPostScroll available delta: Offset(0.0, 0.0)
16 scrollable() available delta: 0.0
15 onPostScroll available delta: Offset(0.0, 0.0)
14 scrollable() available delta: 0.0
13 onPostScroll available delta: Offset(0.0, 0.0)
12 scrollable() available delta: 0.0
11 onPostScroll available delta: Offset(0.0, 0.0)
10 scrollable() available delta: 0.0
9 onPostScroll available delta: Offset(0.0, 0.0)
8 scrollable() available delta: 0.0
7 onPostScroll available delta: Offset(0.0, 0.0)
6 scrollable() available delta: 0.0
5 onPostScroll available delta: Offset(0.0, 0.0)
4 scrollable() available delta: 0.0
3 onPostScroll available delta: Offset(0.0, 0.0)
2 scrollable() available delta: 0.0
1 onPostScroll available delta: Offset(0.0, 0.0)
20.5 verticalScroll()

Child๋ฅผ ์•„์ฃผ ์‚ด์ง๋งŒ ์Šคํฌ๋กคํ–ˆ์„ ๋•Œ ์ถœ๋ ฅ๋˜๋Š” ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๋‹ค. verticalScroll์˜ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๊ฐ€ ์ข€ ๋Šฆ๊ฒŒ ๋œฌ๋‹ค. ์‹ค์ œ๋กœ verticalScroll()์ด ๋Šฆ๊ฒŒ ์ˆ˜ํ–‰๋œ ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค. ScrollState.value์˜ ๋ณ€ํ™”๋ฅผ LaunchedEffect๋กœ ์ถ”์ ํ•˜๊ณ , LaunchedEffect ๋‚ด์—์„œ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•˜๊ธฐ์— ์‹œ์ฐจ๊ฐ€ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค. ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ ์ž์ฒด๊ฐ€, ์‹ค์ œ ์Šคํฌ๋กค์ด ๋˜๋Š” ์ˆœ๊ฐ„ ์ถœ๋ ฅ๋˜๊ฒŒ ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š์•˜๋‹ค๋Š” ์–˜๊ธฐ๋‹ค.

 

#4-8 ์ „์ฒด ์†Œ์Šค์ฝ”๋“œ

 

android-practice/pointer-input/ScrollModifierCallOrder at master · Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com