사용자가 화면에서 구성요소를 물리적으로 드래그할 수 있도록 하는 것은 최신 Android 앱의 기본입니다. 맞춤 슬라이더, 스와이프하여 닫기 구성요소, 대화형 캔버스 등 무엇을 빌드하든 Jetpack Compose는 드래그 이벤트를 쉽게 처리할 수 있는 포괄적인 API 집합을 제공합니다.
Compose 드래그 API는 원시 터치 이벤트의 복잡성을 추상화합니다.
pointerInput를 사용하여 터치 좌표를 수동으로 계산하고 동작 수명 주기를 관리하는 대신 Compose의 상태 관리 및 애니메이션 시스템에 직접 연결되는 내장 수정자를 사용할 수 있습니다.
이 페이지에서는 각 드래그 API를 사용하는 방법과 시기를 설명합니다.
올바른 드래그 API 선택
드래그 가능한 콘텐츠를 구현하는 데 사용할 수 있는 API가 여러 개 있습니다.
- 다음과 같은 경우 표준 Material 구성요소 (예:
SwipeToDismissBox또는 탐색 창)를 선택합니다. 기본 Material 3 스와이프 또는 드래그 동작 (예: 이메일 삭제, 작업 보관처리, 측면 메뉴 / 하단 시트 열기)이 필요한 경우 맞춤 상태나 레이아웃 관리가 필요하지 않은 기본 제공 동작을 제공합니다. Modifier.draggable를 선택하는 경우: 원시 드래그 델타만 필요한 경우 (예: 맞춤 슬라이더 드래그) 자세한 내용은 드래그 가능을 사용하여 단일 축을 따라 드래그를 참고하세요.- 선택하다
Modifier.draggable2D언제 X축과 Y축을 따라 동시에 연속적으로 움직이는 구성 요소가 필요합니다 (예: 지도를 이동하거나 사용자가 화면 어디든 드래그할 수 있는 플로팅 작업 버튼을 만드는 경우). 자세한 내용은Modifier.draggable2D를 사용하여 2차원으로 드래그를 참고하세요.
- 선택하다
Modifier.anchoredDraggable을 선택하는 경우: 두 개 이상의 개별 위치 간에 스냅해야 하는 맞춤 대화형 구성요소 (예: 하단 시트, 탐색 창, 스와이프하여 표시 작업)를 빌드하는 경우 자세한 내용은anchoredDraggable를 사용하여 앵커 포인트 간에 구성요소 드래그를 참고하세요.
draggable 키를 누른 상태에서 하나의 축을 따라 드래그합니다.
draggable 수정자는 동작을 한 방향으로 드래그하는 상위 수준 진입점이며 드래그 거리를 픽셀 단위로 보고합니다.
이 수정자는 동작만 감지한다는 점에서 scrollable과 유사합니다. 예를 들어 offset 수정자를 통해 요소를 이동하여 상태를 유지하고 화면에 표시해야 합니다.
@Composable private fun DraggableText() { var offsetX by remember { mutableFloatStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ), text = "Drag me!" ) }
전체 드래그 동작을 제어해야 하는 경우 대신 pointerInput 수정자를 통해 드래그 동작 감지기를 사용해 보세요.
@Composable private fun DraggableTextLowLevel() { Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(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 } } ) } }

anchoredDraggable 키를 사용하여 기준점 간에 구성요소 드래그
anchoredDraggable 수정자를 사용하면 앵커 포인트라고 하는 사전 정의된 위치 집합 간에 구성요소를 드래그할 수 있습니다. 사용자가 드래그 동작을 해제하면 구성요소가 가장 가까운 앵커에 고정됩니다. anchoredDraggable은 커스텀 하단 시트, 복잡한 슬라이더, 드로어 패널과 같은 구성요소를 만드는 데 유용합니다.
API 노출 영역
AnchoredDraggable API는 다음과 같은 주요 부분으로 구성됩니다.
- 앵커 (
DraggableAnchors): 맞춤 상태 (예:Start, Center, End의 enum 클래스)와 이에 상응하는 픽셀 오프셋 값 간의 매핑을 정의하는 빌더입니다. - 상태 (
AnchoredDraggableState<T>): 구성요소의 현재 위치 (오프셋), 고정된 앵커, 드래그 진행 상황을 관리하는 상태 홀더입니다. 구성 변경 시 상태를 유지하려면rememberSaveable및AnchoredDraggableState.Saver<T>를 사용하여 만들어야 합니다. - 동작 수정자 (
Modifier.anchoredDraggable):Orientation를 따라 드래그 동작을 감지하고 드래그 델타로 상태를 업데이트하며 플링 동작을 처리하여 손을 뗄 때 구성요소를 가장 가까운 앵커로 애니메이션 처리하는 수정자입니다.
예시: 커스텀 SwipeToDismiss 구성요소 빌드
이 예에서는 사용자가 왼쪽으로 스와이프하여 닫거나 오른쪽으로 드래그하여 오버스크롤 늘이기 효과를 볼 수 있는 맞춤 SwipeToDismiss 구성요소 (흰색 배너)를 빌드하는 방법을 보여줍니다.
기준값 정의 드래그 가능한 콘텐츠가 정착할 수 있는 개별 위치를 나타내는 enum을 정의합니다.
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
AnchoredDraggableState를 인스턴스화하고 저장합니다.AnchoredDraggableState.Saver()와 함께rememberSaveable를 사용하여 활동 재생성 및 구성 변경 (예: 화면 회전 또는 테마 변경) 전반에서 구성요소의 현재 위치를 유지합니다.val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
layout,offset,draggable수정자를 적용합니다. 구성요소를 드래그 가능하게 만들고 앵커 포인트가 크기에 맞게 조정되도록 하려면 시각적으로 이동해야 하는 콘텐츠에 다음 수정자를 적용합니다.Modifier.layout: 앵커 위치는 구성요소의 크기에 따라 달라지는 경우가 많으며, 이는 초기 컴포지션 중에 알 수 없습니다.layout수정자를 사용하여 구성요소를 측정하고, 너비에 따라DraggableAnchors맵을 동적으로 계산하고,state.updateAnchors()를 사용하여 상태를 업데이트합니다.
Modifier.anchoredDraggable: 상태에 연결하고, 지정된 방향으로 드래그 델타를 전달하고, 손을 떼면 구성요소를 가장 가까운 앵커로 애니메이션 처리하는 플링 동작을 처리합니다. 드래그 동작에 응답해야 하는 구성요소에 이 수정자를 추가하고state및 드래그orientation(선택사항으로overscrollEffect)를 전달합니다.Modifier.offset: 오프셋 람다 내에서state.requireOffset()의 오프셋을 읽습니다. 이 람다 내에서 오프셋을 읽으면 배치 단계에서만 값이 읽히므로 드래그 중에 불필요한 리컴포지션이 방지됩니다. 시각적으로 이동해야 하는 콘텐츠에 이 수정자를 적용합니다.
Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() }
[선택사항] 오버스크롤을 통합합니다.
overscrollEffectAPI는 스크롤 또는 플링으로 스크롤 컨테이너의 가장자리에 도달했을 때 표시되는 시각 효과를 구현합니다. 사용자가 오버스크롤 효과 (일반적으로 늘이기 또는 빛나기)를 사용하여 정의된 앵커 경계를 지나 드래그할 때 시각적 피드백을 제공할 수 있습니다.- 전달:
overscrollEffect인스턴스를anchoredDraggable수정자에 전달합니다. 이렇게 하면 동작이 앵커 경계 내부에 있는지 외부에 있는지에 따라 드래그 델타가AnchoredDraggable상태 또는 오버스크롤 효과로 전달됩니다. - 렌더링: 동일한
OverscrollEffect인스턴스를 사용하여Modifier.overscroll를 효과를 시각적으로 렌더링해야 하는 구성요소에 적용합니다.
- 전달:
다음 스니펫은 표준 늘이기 효과를 추가하는 방법을 보여줍니다.
val overscrollEffect = rememberOverscrollEffect() Box( modifier.fillMaxWidth() ) { Box(Modifier.fillMaxWidth()) { background() } Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() } }
결과
위의 코드 스니펫은 'Drag me to dismiss!'라는 텍스트가 있는 흰색 배너를 생성합니다. 사용자는 왼쪽으로 스와이프하여 배너를 닫거나 오른쪽으로 드래그하여 오버스크롤 늘이기 효과를 볼 수 있습니다.
예: 맞춤 동작 기반 탐색 창 만들기
이 예에서는 맞춤 동작 기반 탐색 창을 빌드하는 방법을 보여줍니다. 사용자가 가로로 드래그하면 포그라운드 화면 (ScreenContents)이 오른쪽으로 슬라이드되고 1.0f에서 0.8f로 축소되며 모서리가 둥글게 처리되어 기본 탐색 창이 표시됩니다.
앵커 상태 및 위치를 정의합니다. 이 예시에서 앵커는 창의 열림 및 닫힘 상태입니다.
Open및Closed상태의 enum을 정의하고DraggableAnchors를 사용하여 픽셀 오프셋에 매핑합니다.enum class DrawerState { Open, Closed } val anchors = DraggableAnchors { DrawerState.Open at drawerWidth DrawerState.Closed at 0f } val state = remember { AnchoredDraggableState( initialValue = DrawerState.Closed, anchors = anchors, ) }
항목을 클릭하면
anchoredDraggableState에 애니메이션 적용HomeScreenDrawer를Box하단에 배치하고ScreenContents를 상단에 배치합니다. 창은 고정된 상태로 유지되고 상단 레이어가 그 위에서 이동하고 크기가 조정됩니다.사용자가 창 항목을 선택하면 활성 화면 상태를 업데이트하고
state.animateTo(DrawerState.Closed)를 사용하여 창을 자동으로 닫습니다.@Composable private fun AnchoredDraggableDrawerOnClickSample( modifier: Modifier = Modifier, state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { var screenState by remember { mutableStateOf(ScreenState.Home) } Box(modifier = modifier.fillMaxSize()) { HomeScreenDrawer( selectedScreen = screenState, onScreenSelected = { screen -> screenState = screen // Dismiss drawer when a destination is selected coroutineScope.launch { state.animateTo(DrawerState.Closed) } } ) // ScreenContents(...) } }
state.requireOffset()를 사용하여 여러 시각적 속성 변경 콘텐츠만 오프셋하는 대신Modifier.graphicsLayer를 사용하여 여러 속성을 변경하여 드래그 진행 상황에 따라 변환, 배율, 모서리 반경에 동시에 애니메이션을 적용할 수 있습니다.state.requireOffset()를 사용하여 현재 드래그 오프셋을 가져옵니다.lerp()(선형 보간법)을 사용하여 오프셋 및 창 너비에 상대적인 비례 배율 (1f~0.8f)과 모서리 반경 (0.dp~32.dp)을 계산합니다.Modifier.graphicsLayer는 그리기 단계에서 변경사항을 실행하여 드래그 중에 재구성을 트리거하지 않고도 고성능 애니메이션을 보장합니다.
@Composable private fun AnchoredDraggableDrawerGraphicsLayerSample( state: AnchoredDraggableState<DrawerState>, drawerWidth: Float, selectedScreen: ScreenState, onDrawerClicked: () -> Unit ) { ScreenContents( selectedScreen = selectedScreen, onDrawerClicked = onDrawerClicked, modifier = Modifier .graphicsLayer { this.translationX = state.requireOffset() val scale = lerp(1f, 0.8f, state.requireOffset() / drawerWidth) this.scaleX = scale this.scaleY = scale val cornerRadius = lerp(0f, 32f, state.requireOffset() / drawerWidth) this.shape = RoundedCornerShape(cornerRadius.dp) this.clip = true } .anchoredDraggable(state, Orientation.Horizontal) ) }
animateTo로 프로그래매틱 애니메이션 트리거 코루틴 범위 내에서state.animateTo()를 실행하여 프로그래매틱 방식으로 창을 제어합니다 (예: 메뉴 아이콘을 탭하거나 창 항목을 선택할 때).fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
결과
Modifier.draggable2D를 사용하여 두 방향으로 드래그
draggable2D 수정자를 사용하여 개별 UI 요소를 X축과 Y축 모두에서 동시에 이동합니다.
컴포저블 엘리먼트 드래그
이 예에서는 draggable2D의 가장 일반적인 사용 사례를 보여줍니다. 즉, 사용자가 UI 요소를 선택하여 상위 컨테이너 내의 아무 곳에나 재배치할 수 있습니다.
@Composable private fun DraggableComposableElement() { // 1. Track the position of the floating window var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) { Box( modifier = Modifier // 2. Apply the offset to the box's position .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } // ... // 3. Attach the 2D drag logic .draggable2D( state = rememberDraggable2DState { delta -> // 4. Update the position based on the movement delta offset += delta } ), contentAlignment = Alignment.Center ) { Text("Video Preview", color = Color.White, fontSize = 12.sp) } } }
위의 코드 스니펫에는 다음이 포함됩니다.
offset상태를 사용하여 상자의 위치를 추적합니다.offset수정자를 사용하여 드래그 델타에 따라 구성요소의 위치를 이동합니다.- 플링 지원이 없으므로 사용자가 손가락을 떼는 순간 상자가 움직임을 멈춥니다.
상위 요소의 드래그 영역을 기반으로 하위 컴포저블 드래그
이 예시에서는 draggable2D를 사용하여 선택기 노브가 특정 표면 내로 제한되는 2D 입력 영역을 만드는 방법을 보여줍니다. 구성요소 자체를 이동하는 드래그 가능한 요소 예와 달리 이 구현에서는 2D 델타를 사용하여 색상 선택 도구에서 하위 컴포저블 '선택기'를 이동합니다.
@Composable private fun ExampleColorSelector( // ... ) { // 1. Maintain the 2D position of the selector in state. var selectorOffset by remember { mutableStateOf(Offset.Zero) } // 2. Track the size of the background container. var containerSize by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .size(300.dp, 200.dp) // Capture the actual pixel dimensions of the container when it's laid out. .onSizeChanged { containerSize = it } .clip(RoundedCornerShape(12.dp)) .background( brush = remember(hue) { // Create a simple gradient representing Saturation and Value for the given Hue. Brush.linearGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f))) } ) ) { Box( modifier = Modifier .size(24.dp) .graphicsLayer { // Center the selector on the finger by subtracting half its size. translationX = selectorOffset.x - (24.dp.toPx() / 2) translationY = selectorOffset.y - (24.dp.toPx() / 2) } // ... // 3. Configure 2D touch dragging. .draggable2D( state = rememberDraggable2DState { delta -> // 4. Calculate the new position and clamp it to the container bounds val newX = (selectorOffset.x + delta.x) .coerceIn(0f, containerSize.width.toFloat()) val newY = (selectorOffset.y + delta.y) .coerceIn(0f, containerSize.height.toFloat()) selectorOffset = Offset(newX, newY) } ) ) } }
위 스니펫에는 다음이 포함됩니다.
onSizeChanged수정자를 사용하여 그라데이션 컨테이너의 실제 크기를 캡처합니다. 선택기는 가장자리가 어디에 있는지 정확히 알고 있습니다.graphicsLayer내에서translationX및translationY를 조정하여 드래그하는 동안 선택기가 중앙에 유지되도록 합니다.
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- 동작 이해하기
- 고급 애니메이션 예시: 동작 {:#gesture-and-animation}
- 가치 기반 애니메이션