사용자가 화면에서 구성요소를 물리적으로 드래그할 수 있도록 하는 것은 최신 Android 앱의 기본입니다. 맞춤 슬라이더, 스와이프하여 닫기 구성요소 또는 대화형 캔버스를 빌드하는 경우 Jetpack Compose는 드래그 이벤트를 쉽게 처리할 수 있는 포괄적인 API 집합을 제공합니다.
Compose 드래그 API는 원시 터치 이벤트의 복잡성을 추상화합니다.
`pointerInput`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, anddraggable수정자를 적용합니다. 구성요소를 드래그 가능하게 만들고 앵커 포인트가 크기에 맞게 조정되도록 하려면 시각적으로 이동해야 하는 콘텐츠에 다음 수정자를 적용합니다.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() } }
결과
위의 코드 스니펫은 사용자가 왼쪽으로 스와이프하여 닫거나 오른쪽으로 드래그하여 오버스크롤 스트레치 효과를 볼 수 있는 '드래그하여 닫기'라는 텍스트가 포함된 흰색 배너를 생성합니다.
예: 동작 기반 맞춤 탐색 창 만들기
이 예에서는 동작 기반 맞춤 탐색 창을 빌드하는 방법을 보여줍니다. 사용자가 가로로 드래그하면 포그라운드 화면 (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를 애니메이션 처리합니다.Box하단에HomeScreenDrawer를 배치하고 상단에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로 2차원 드래그
draggable2D 수정자를 사용하여 X축과 Y축 모두에서 동시에 개별 UI 요소를 이동합니다.
컴포저블 요소 드래그
이 예에서는 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}
- 가치 기반 애니메이션