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

[Android] Pointer input - Drag

interfacer_han 2025. 5. 13. 13:23

#1 ๊ฐœ์š”

#1-1 ๊ณต์‹ ๋ฌธ์„œ

 

๋“œ๋ž˜๊ทธ, ์Šค์™€์ดํ”„, ํ”Œ๋ง  |  Jetpack Compose  |  Android Developers

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

developer.android.com

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

 

#1-2 AnchoredDraggable

 

Swipeable์—์„œ AnchoredDraggable๋กœ ์ด์ „  |  Jetpack Compose  |  Android Developers

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

developer.android.com

Swipeable์ด AnchoredDraggable๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ๋˜์—ˆ๋‹ค๊ณ  ํ•œ๋‹ค. ์œ„ ๋งํฌ๋Š” ํ•ด๋‹น ์—…๋ฐ์ดํŠธ๋ฅผ ์ „ํ•˜๋Š” ํŽ˜์ด์ง€๋‹ค.

 

#1-3 ์œ ํŠœ๋ธŒ ์˜์ƒ

AnchoredDraggable์— ๋Œ€ํ•œ ๊ฐœ๋ก ์„ ๋‹ด์€ ๊ณต์‹ ์˜์ƒ์ด๋‹ค. ์™œ์ธ์ง€๋Š” ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ, ์˜์ƒ ์ธ๋„ค์ผ์— ์•ˆ๋“œ๋กœ์ด๋“œ ์บ๋ฆญํ„ฐ๊ฐ€ ํ•ด์ ์ด ์“ธ ๋ฒ•ํ•œ ์•ˆ๋Œ€๋ฅผ ์“ฐ๊ณ  ์žˆ๋‹ค.

 

#2 ๋“œ๋ž˜๊ทธ (drag)

#2-1 ๊ฐœ์š”

Modifier.draggable()์€ ์ปดํฌ๋„ŒํŠธ์— ๋“œ๋ž˜๊ทธ๋ฅผ '๊ฐ€๋Šฅํ•˜๊ฒŒ' ํ•œ๋‹ค. Modifier.draggable() ๋งŒ์œผ๋กœ ๋“œ๋ž˜๊ทธ๊ฐ€ '๋˜๋Š”' ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค. Modifier.scrollable()๊ณผ ๊ฐ™์€ ๋งฅ๋ฝ์ด๋‹ค. Modifier.scrollable()์— ๋Œ€ํ•ด ๋‹ค๋ฃฌ ์•„๋ž˜ ๋งํฌ์˜ ๊ฒŒ์‹œ๊ธ€์„ ์ฝ์œผ๋ฉด ์ดํ•ด๊ฐ€ ์‰ฌ์šธ ๊ฒƒ์ด๋‹ค.

 

 

[Android] Pointer input - Scroll

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

kenel.tistory.com

์œ„ ๊ฒŒ์‹œ๊ธ€์—์„œ, ์Šคํฌ๋กค์„ '๊ฐ€๋Šฅํ•˜๊ฒŒ' ํ•˜๋Š” ๊ฒƒ์€ Modifier.scrollable()์ด์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์Šคํฌ๋กค์ด '๋˜๊ฒŒ' ํ•˜๋Š” ๊ฒƒ์€ Modifier.verticalScroll() ๋˜๋Š” Modifier.horizontalScroll()์ด์—ˆ๋‹ค. ์ด์™€ ๋น„์Šทํ•˜๊ฒŒ ๋“œ๋ž˜๊ทธ๋ฅผ '๊ฐ€๋Šฅํ•˜๊ฒŒ' ํ•˜๋Š” ๊ฒƒ์€ Modifier.draggable()์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋“œ๋ž˜๊ทธ๊ฐ€ '๋˜๊ฒŒ' ํ•˜๋Š” ๊ฒƒ์€ detectDragGestures()๋‹ค.

 

#2-2 Modifier.draggable()๊ฐ€ ์‚ฌ์šฉ๋œ ์ฝ”๋“œ ์˜ˆ์‹œ

@Composable
private fun DraggableText() {
    var offsetX by remember { mutableStateOf(0f) }
    Text(
        modifier = Modifier
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .draggable(
                orientation = Orientation.Horizontal,
                state = rememberDraggableState { delta ->
                    offsetX += delta
                }
            ),
        text = "Drag me!"
    )
}

 

#2-3 detectDragGestures()๊ฐ€ ์‚ฌ์šฉ๋œ ์ฝ”๋“œ ์˜ˆ์‹œ

@Composable
private fun DraggableTextLowLevel() {
    Box(modifier = Modifier.fillMaxSize()) {
        var offsetX by remember { mutableStateOf(0f) }
        var offsetY by remember { mutableStateOf(0f) }

        Box(
            Modifier
                .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                .background(Color.Blue)
                .size(50.dp)
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consume()
                        offsetX += dragAmount.x
                        offsetY += dragAmount.y
                    }
                }
        )
    }
}

 

#3 ํ”Œ๋ง (fling)

ํ”Œ๋ง(fling)์ด๋ž€, ๋“œ๋ž˜๊ทธ ๋„์ค‘ ์†์„ ๋–ผ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค. ์Šค๋งˆํŠธํฐ์—์„œ ์›น ํŽ˜์ด์ง€๋ฅผ ์Šคํฌ๋กคํ•  ๋•Œ๋ฅผ ๋– ์˜ฌ๋ ค ๋ณด๋ผ. ์†๊ฐ€๋ฝ์œผ๋กœ ํ™”๋ฉด์„ ๋“œ๋ž˜๊ทธํ•˜๋‹ค๊ฐ€ ์†์„ ๊ฐ‘์ž๊ธฐ ํœ™ ๋–ผ๋ฉด, ๋งˆ์น˜ ์Šคํฌ๋กค์— ๊ด€์„ฑ์ด ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์—ฌ์ „ํžˆ ์Šคํฌ๋กค๋˜์ง€ ์•Š๋Š”๊ฐ€? ๊ทธ๊ฑธ์„ ํ”Œ๋ง์ด๋ผ๊ณ  ํ•œ๋‹ค. ์—ญ๋ฐฉํ–ฅ์œผ๋กœ ์ƒ๊ฐํ•ด๋ณด๋ฉด, ํ”Œ๋ง์€ ๋ฐ˜๋“œ์‹œ ๋“œ๋ž˜๊ทธ๊ฐ€ ์„ ํ–‰๋˜์–ด์•ผ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋ผ๋Š” ๊ฒƒ๋„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

 

#4 ์Šค์™€์ดํ”„ (swipe)

https://developer.android.com/static/develop/ui/compose/images/gestures-swipe.gif

์Šค์™€์ดํ”„(swipe)๋ž€, ๋“œ๋ž˜๊ทธ๋‚˜ ํ”Œ๋ง ์ข…๋ฃŒ ์‹œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ (ํ”„๋กœ๊ทธ๋ž˜๋จธ์— ์˜ํ•ด ์ •ํ•ด์ง„) '์•ต์ปค ํฌ์ธํŠธ'๋กœ ์ด๋™ํ•˜๋Š” ์ด๋ฒคํŠธ๋‹ค. ์—ญ๋ฐฉํ–ฅ์œผ๋กœ ์ƒ๊ฐํ•ด๋ณด๋ฉด, ์Šค์™€์ดํ”„๋Š” ๋ฐ˜๋“œ์‹œ ๋“œ๋ž˜๊ทธ ๋˜๋Š” ํ”Œ๋ง์ด ์„ ํ–‰๋˜์–ด์•ผ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋ผ๋Š” ๊ฒƒ๋„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ์Šค์™€์ดํ”„ ๋™์ž‘์„ ์œ„ํ•ด์„  Modifier.swipeable()์ด ํ•„์š”ํ•˜๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ #1-2์—์„œ ๋งํ–ˆ๋“ฏ, Swipeable๋Š” deprecated๋  ์˜ˆ์ •์ด๋‹ค. ๋”ฐ๋ผ์„œ ๋ณธ ๊ฒŒ์‹œ๊ธ€์—์„  Modifier.swipeable() ๋Œ€์‹  Modifier.anchoredDraggable()์„ ์‚ฌ์šฉํ•œ๋‹ค.

 

#5 AnchoredDraggable

#5-1 ๊ฐœ์š” 

๊ณต์‹ ๋ฌธ์„œ์— ์žˆ๋Š” ๋‚ด์šฉ์„ ์ž˜ ์ฝ๊ณ  ์†Œํ™”ํ•˜๋ คํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋Šฌ์•™์Šค๋งŒ ์ดํ•ด๋˜์—ˆ๊ณ  ๋‚ด๋ถ€ ๊ธฐ์ œ๋ฅผ ์ •ํ™•ํžˆ ์ดํ•ดํ•  ์ˆœ ์—†์—ˆ๋‹ค. ์ด๋Ÿฐ ๊ฒ‰ํ•ฅ๊ธฐ์‹ ์ดํ•ด๋กœ๋Š”, ์‹ค์ œ ๊ตฌํ˜„์„ ํ•  ์ˆ˜ ์—†๋‹ค. ๋”ฐ๋ผ์„œ ๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด ๊ณต์‹ ๋ฌธ์„œ์˜ ์ฝ”๋“œ๋ฅผ ๋”ฐ๋ผํ•ด๋ณด๊ฒ ๋‹ค. ์šฐ์„  AnchoredDraggableExample์ด๋ผ๋Š” ์ด๋ฆ„์˜ Jetpack Compose ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

 

#5-2 ๊ธฐ๋ณธ ๊ตฌ์กฐ

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AnchoredDraggableBox() {
    val state = remember {
        AnchoredDraggableState(
            // TODO
        )
    }

    Box(
        modifier = Modifier
            .offset { /* TODO */ }
            .width(200.dp)
            .height(200.dp)
            .background(Color.LightGray)
            .anchoredDraggable(
                state = state,
                orientation = /* TODO */
            )
    ) {
        Text("Swipe me")
    }
}

AnchoredDrag๋ฅผ ์ ์šฉํ•  ์ปดํฌ๋„ŒํŠธ์— Modifier.anchoredDraggable() ๋ฐ Modifier.offset()์„ ๋ถ™์ธ๋‹ค. ๊ทธ๋ฆฌ๊ณ  AnchoredDrag์˜ ํ•ต์‹ฌ์ธ AnchoredDraggableState๋ฅผ ์„ ์–ธํ•˜์—ฌ anchoredDraggable() ๋ฐ offset() ์†์— ๋„ฃ์–ด์ฃผ๋Š” ๊ฒƒ์œผ๋กœ AnchoredDrag๊ฐ€ ๋™์ž‘ํ•˜๊ฒŒ ๋œ๋‹ค.

 

#5-3 AnchoredDraggableState

@ExperimentalFoundationApi public constructor AnchoredDraggableState<T>(
    initialValue: T,
    anchors: DraggableAnchors<T>,
    positionalThreshold: (totalDistance: Float) -> Float,
    velocityThreshold: () -> Float,
    animationSpec: AnimationSpec<Float>,
    confirmValueChange: (newValue: T) -> Boolean = { true }
)

AnchoredDraggableState๋Š” AnchoredDrag๋ฅผ ์œ„ํ•œ ๋ชจ๋“  ์ •๋ณด๊ฐ€ ๋“ค์–ด์žˆ๋‹ค. ๊ฐ ์ธ์ˆ˜์— ๋Œ€ํ•œ ์„ค๋ช…์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

initialValue: T

์˜ˆ๋ฅผ ๋“ค์–ด, #5-2 ์ฝ”๋“œ ์† Box()์˜ ์ดˆ๊ธฐ ์œ„์น˜๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

 

anchors: DraggableAnchors<T>

์—ฌ๋Ÿฌ '์•ต์ปค ํฌ์ธํŠธ'์˜ ๋ชฉ๋ก์ด๋‹ค.

 

positionalThreshold: (totalDistance: Float) -> Float

๋‹ค๋ฅธ '์•ต์ปค ํฌ์ธํŠธ'๋กœ ์ด๋™๋˜๊ธฐ ์œ„ํ•œ ์ตœ์†Œ ๊ฑฐ๋ฆฌ. ์ฆ‰ ์ด ๊ฐ’์ด ์ž‘๋‹ค๋ฉด ์‚ด์ง๋งŒ ์Šค์™€์ดํ”„ํ•ด๋„ ๋‹ค๋ฅธ ์•ต์ปค ํฌ์ธํŠธ๋กœ ๋‚ ์•„๊ฐ€๋ฒ„๋ฆด ๊ฒƒ์ด๋‹ค.

 

velocityThreshold: () -> Float

positionalThreshold์˜ ๊ฐ’์„ ๋„˜์ง€ ์•Š์•˜์–ด๋„, ๋“œ๋ž˜๊ทธ ๋™์ž‘์˜ ์†๋„(velocity)๊ฐ€ ๋†’๋‹ค๋ฉด ๋‹ค๋ฅธ '์•ต์ปค ํฌ์ธํŠธ'๋กœ ์ด๋™ํ•˜๋Š”๊ฒŒ ์ž์—ฐ์Šค๋Ÿฝ๋‹ค. ์ด ์ธ์ˆ˜๋Š” ๊ทธ ์†๋„ ๊ฐ’์„ ์ •์˜ํ•œ๋‹ค.

 

animationSpec: AnimationSpec<Float>

์–ด๋–ป๊ฒŒ(how) ๋‹ค๋ฅธ ์•ต์ปค ํฌ์ธํŠธ๋กœ ์ด๋™ํ•˜๋Š” ์ง€ ์ •์˜ํ•œ๋‹ค. ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋™์ž‘์„ ์ •์˜ํ•œ๋‹ค๋Š” ๋ง์ด๋‹ค.

 

confirmValueChange: (newValue: T) -> Boolean

confirmValueChange๋ฅผ ๋‹จ์ˆœ ํ•ด์„ํ•˜๋ฉด '์•ต์ปค ํฌ์ธํŠธ'๋กœ ์ด๋™ํ•จ์„ ํ—ˆ์šฉํ•œ๋‹ค๋Š” ๋ง์ด๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ true๋กœ ๋˜์–ด์žˆ๊ธฐ์— ํŠน์ • ์•ต์ปค ํฌ์ธํŠธ๋กœ ์ด๋™ํ•˜๋Š” ๊ฒŒ ๋ง‰ํž ์ผ์ด ์—†์ง€๋งŒ, ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ confirmValueChange๋ฅผ ํ†ตํ•ด์„œ ๋งค์šฐ ์„ธ์„ธํ•œ ํ†ต์ œ๋ฅผ ๊ฐ€ํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ๊ฐ€๋ น ์–ด๋–ค ๋ณ€์ˆ˜๊ฐ€ ์–ด๋–ค ๊ฐ’์ธ ๊ฒฝ์šฐ์—๋งŒ ์ด๋™์„ ํ—ˆ์šฉํ•˜๋Š” ์‹์ด๋‹ค.

 

#5-4 ์˜ˆ์‹œ ์ฝ”๋“œ

enum class DragValue { Start, Center, End }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AnchoredDraggableBox() {
    val density = LocalDensity.current

    val anchors = with(density) {
        DraggableAnchors {
            DragValue.Start at -250.dp.toPx()
            DragValue.Center at 0f
            DragValue.End at 250.dp.toPx()
        }
    }

    val state = remember {
        AnchoredDraggableState(
            initialValue = DragValue.Center,
            anchors = anchors,
            positionalThreshold = { distance -> distance * 0.5f }, // (1)
            velocityThreshold = { with(density) { 125.dp.toPx() } }, // (2)
            animationSpec = spring(), // (3)
        )
    }

    Box(
        Modifier
            .offset { IntOffset(x = 0, y = state.requireOffset().roundToInt()) } // (4)
            .width(200.dp)
            .height(200.dp)
            .background(Color.LightGray)
            .anchoredDraggable(
                state = state,
                orientation = Orientation.Vertical
            )
    ) {
        Text("Swipe me")
    }
}

(1) positionalThreshold = { distance -> distance * 0.5f }

distance๋Š” ๋‘ '์•ต์ปค ํฌ์ธํŠธ' ์‚ฌ์ด์˜ ๊ฑฐ๋ฆฌ๋ฅผ ์˜๋ฏธํ•œ๋‹ค. ๊ทธ ๊ฑฐ๋ฆฌ์˜ ์ ˆ๋ฐ˜(0.5f) ์ด์ƒ ๋“œ๋ž˜๊ทธํ–ˆ๋‹ค๋ฉด ์Šค์™€์ดํ”„๊ฐ€ ์‹คํ–‰๋œ๋‹ค.

 

(2) velocityThreshold = { with(density) { 125.dp.toPx() } }

์‚ฌ์šฉ์ž๊ฐ€ ์†์„ ๋—„ ๋•Œ์˜ ์†๋„๊ฐ€ (125.dp / 1์ดˆ)์ด์ƒ์ด๋ฉด, ์Šค์™€์ดํ”„๊ฐ€ ์‹คํ–‰๋จ์„ ์˜๋ฏธํ•œ๋‹ค. ์†๋„๋Š” Compose ๋‚ด๋ถ€์—์„œ๋Š” (px / ์ดˆ) ๋‹จ์œ„์ด๊ธฐ์—, ํ”ฝ์…€๋กœ ๋ณ€ํ™˜ํ–ˆ๋‹ค. ๊ทธ๋Ÿผ ๊ทธ๋ƒฅ ์ฒ˜์Œ๋ถ€ํ„ฐ (px / ์ดˆ) ๋‹จ์œ„๋กœ ๋„ฃ์–ด์ฃผ๋ฉด ๋˜์ง€์•Š์„๊นŒ? ๊ฐ€๋Šฅ์€ ํ•˜๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ž์—ฐ์Šค๋Ÿฝ์ง€ ์•Š๋‹ค. (px / ์ดˆ) ๋‹จ์œ„๋ฅผ ๋„ฃ์œผ๋ ค๋Š” ์‹œ๋„๋Š” ๋งˆ์น˜, ๊ฑด์ถ•๊ฐ€๋“ค์ด ์•„ํŒŒํŠธ์˜ ๋†’์ด๋ฅผ "์ด ๋ช‡ m์ธ๊ฐ€?"๊ฐ€ ์•„๋‹Œ "์ด ๋ช‡ ์ธต์ธ๊ฐ€?"๋กœ ํŒŒ์•…ํ•˜๋ ค๋Š” ์‹œ๋„์™€ ๊ฐ™๋‹ค. ๋”ฐ๋ผ์„œ ์ฝ”๋“œ์—์„œ๋Š” ์ธ๊ฐ„(ํ”„๋กœ๊ทธ๋ž˜๋จธ) ์ž…์žฅ์—์„œ ์ •ํ™•ํ•œ ๋‹จ์œ„์ธ dp๋กœ ์ ๋Š” ๊ฒƒ์ด๋‹ค.

 

(3) animationSpec = spring()

spring์€ ์•ˆ๋“œ๋กœ์ด๋“œ์˜ ๊ธฐ๋ณธ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋‹ค. spring์— ์ปค์Šคํ…€์„ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

(4) y = state.requireOffset().roundToInt()

requireOffset()์€ Modifier.anchoredDraggable()๊ฐ€ ๋ถ™์€ ์ปดํฌ์ €๋ธ” ๊ฐ์ฒด์˜ offset๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 

#5-5 ๋™์˜์ƒ 1

์ž˜ ์ž‘๋™ํ•˜์ง€๋งŒ ์ฒซ ์•ต์ปค ํฌ์ธํŠธ์™€ ๋ ์•ต์ปค ํฌ์ธํŠธ์—์„œ ๋” ์Šค์™€์ดํ”„๋  ์ˆ˜ ์—†๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ๋“œ๋ž˜๊ทธํ–ˆ์„ ๋•Œ, Box()๊ฐ€ ์›€์ฐ”๊ฑฐ๋ฆฌ๋Š” ๊ฒŒ ๋งˆ์Œ์— ๋“ค์ง€ ์•Š์•˜๋‹ค.

 

#5-6 ๊น”๋”ํ•˜๊ฒŒ ๋‹ค๋“ฌ๊ธฐ

Box(
    Modifier
        .offset {
            IntOffset(
                x = 0,
                y = state.requireOffset()
                    .coerceIn(anchors.minAnchor(), anchors.maxAnchor())
                    .roundToInt()
            )
        }
        ...
) {
    Text("Swipe me")
}

ํ•˜์ง€๋งŒ offset๋ฅผ ์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝํ•˜๋ฉด,

 

#5-7 ๋™์˜์ƒ 2

์›€์ฐ”๊ฑฐ๋ฆฌ๋Š” ํ˜„์ƒ์„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค.

 

#5-8 ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ

 

Compose ๊ธฐ์ดˆ  |  Jetpack  |  Android Developers

 

developer.android.com

Jetpack compose 1.7.0-alpha01 ๋ฒ„์ „๋ถ€ํ„ฐ, AnchoredDraggableState์˜ ์ธ์ˆ˜ animationSpec์˜ ์ด๋ฆ„์ด snapAnimationSpec์œผ๋กœ ๋ฐ”๋€Œ์—ˆ๊ณ  decayAnimationSpec์ด๋ผ๋Š” ์ธ์ˆ˜๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ๋‹ค.

 

๋˜, 1.8.0-alpha06 ๋ฒ„์ „๋ถ€ํ„ฐ๋Š” confirmValueChange ์ธ์ˆ˜๊ฐ€ ์‚ญ์ œ๋˜์—ˆ๋‹ค. confirmValueChange๋ฅผ ๋Œ€์ฒดํ•˜๋Š” ๊ฑด ์—†๋‹ค. ์ด์ œ ํŠน์ • ์•ต์ปค๋กœ ์Šค์™€์ดํ”„๋˜๋Š” ๊ฒŒ ์‹ซ๋‹ค๋ฉด, confirmValueChange๋กœ ์„ธ๋ถ€์กฐ์ •์„ ํ•  ๊ฒŒ ์•„๋‹ˆ๋ผ ์•ต์ปค ํฌ์ธํŠธ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ๋ฅผ ํ•ด๋ฒ„๋ฆฌ๋ผ๊ณ  ํ•œ๋‹ค.

 

์ด์— ๋งž๊ฒŒ ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ฒ ๋‹ค.

 

enum class DragValue { Start, Center, End }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AnchoredDraggableBox() {
    val density = LocalDensity.current

    // ์•ต์ปค ์ •์˜
    val anchors = with(density) {
        DraggableAnchors {
            DragValue.Start at -250.dp.toPx()
            DragValue.Center at 0f
            DragValue.End at 250.dp.toPx()
        }
    }

    // AnchoredDraggableState ์„ ์–ธ
    val state = remember {
        AnchoredDraggableState(
            initialValue = DragValue.Center
        )
    }

    // AnchoredDraggableState์— ์•ต์ปค ์—ฐ๊ฒฐ (์ดˆ๊ธฐํ™”)
    LaunchedEffect(Unit) {
        state.updateAnchors(anchors)
    }

    if (!state.offset.isNaN()) { // ๋ถ„๊ธฐ๋ฌธ์œผ๋กœ ์•ต์ปค ์—ฐ๊ฒฐ (์ดˆ๊ธฐํ™”) ์œ ๋ฌด ํ™•์ธ ํ›„ Box() ํ˜ธ์ถœ
        Box(
            Modifier
                .offset {
                    IntOffset(
                        x = 0,
                        y = state.offset.roundToInt()
                    )
                }
                .width(200.dp)
                .height(200.dp)
                .background(Color.LightGray)
                .anchoredDraggable(
                    state = state,
                    orientation = Orientation.Vertical,
                    flingBehavior = AnchoredDraggableDefaults.flingBehavior(
                        state = state,
                        positionalThreshold = { distance -> distance * 0.5f },
                        animationSpec = spring()
                    )
                )
        ) {
            Text(
                """
                Swipe me!
                
                current: ${state.currentValue}
                settled: ${state.settledValue}
                """.trimIndent()
            )
        }
    }
}

Box() ์™ธ๋ถ€์˜ ์ฝ”๋“œ ์„ค๋ช…

AnchoredDraggableState ์ƒ์„ฑ์ด ๋งค์šฐ ๊ฐ„์†Œํ™”๋˜์—ˆ๋‹ค. ๋˜, ์•ต์ปค ํฌ์ธํŠธ๋ฅผ AnchoredDraggableState.updateAnchors()๋ฅผ ํ†ตํ•ด ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒŒ ๊ธฐ๋ณธ๊ฐ’์ด ๋˜์—ˆ๋‹ค (์—ฌ์ „ํžˆ ์ƒ์„ฑ์ž์— ๋„ฃ์–ด๋„ ๋˜๊ธด ํ•œ๋‹ค). ์œ„ ์ฝ”๋“œ์—์„œ๋Š” updateAnchors()๋ฅผ LaunchedEfftect()๋ฅผ ํ†ตํ•ด ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋„ฃ์–ด์ฃผ๊ณ  ์žˆ๋‹ค. ์ด ๋น„๋™๊ธฐ ์ฝ”๋“œ ๋•Œ๋ฌธ์— Box()๋ฅผ if๋ฌธ์œผ๋กœ ๊ฐ์ŒŒ๋‹ค. ์•ต์ปค ํฌ์ธํŠธ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ Box()๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜๋Š” ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

Box() ๋‚ด๋ถ€์˜ ์ฝ”๋“œ ์„ค๋ช…

coerceIn()์„ ์“ฐ์ง€ ์•Š์•„๋„ ์›€์ฐ”๊ฑฐ๋ฆฌ์ง€ ์•Š๊ธฐ์— ์ œ๊ฑฐํ–ˆ๋‹ค. ์ด์ œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ด€๋ จ ์ฝ”๋“œ๋Š” anchoredDraggable() ์†์œผ๋กœ ๋“ค์–ด๊ฐ„๋‹ค. anchoredDraggable()์˜ flingBehavior ํ”„๋กœํผํ‹ฐ๊ฐ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋‹ด๋‹นํ•œ๋‹ค. ์—ฌ๊ธฐ์— AnchoredDraggableDefaults.flingBehavior๋ฅผ ๋„ฃ์–ด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ง€์ •ํ•ด์ฃผ์—ˆ๋‹ค. flingBehavior ํ”„๋กœํผํ‹ฐ๋Š” nullable์ด๋ผ ๋น„์›Œ๋„ ๋™์ž‘ํ•œ๋‹ค. ๋ฆฌํŒฉํ† ๋งํ•˜๋Š” ๊น€์—, Text()์— ํ˜„์žฌ ์ƒํƒœ๋„ ํ‘œ์‹œํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค.

 

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

 

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

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

github.com

#5-8์˜ ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ๋ฅผ ์ ์šฉํ–ˆ๋‹ค.

'๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘ > Android' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Android] Module ๊ตฌ์กฐ  (0) 2025.09.15
[Android] App layout - Custom layouts  (0) 2025.02.27
[Android] UI architecture - Phase์™€ State  (1) 2025.02.27
[Android] UI architecture - Phases  (0) 2025.02.27
[Android] App layout - ๊ธฐ์ดˆ  (0) 2025.02.27