Compose에서 드래그 앤 드롭: 재정렬 가능한 리스트 및 스와이프 삭제
Source: Dev.to
드래그 제스처 감지
detectDragGesturesAfterLongPress 수정자는 롱프레스 후에 드래그 상호작용을 가능하게 합니다:
var offsetX by remember { mutableFloatStateOf(0f) }
var offsetY by remember { mutableFloatStateOf(0f) }
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, dragAmount ->
change.consume()
offsetX += dragAmount.x
offsetY += dragAmount.y
},
onDragEnd = {
// Handle drag completion
}
)
}
)
시각적 피드백을 위한 GraphicsLayer
graphicsLayer를 사용하여 드래그 작업 중 실시간 시각적 피드백을 제공합니다:
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.graphicsLayer {
translationX = offsetX
translationY = offsetY
scaleX = if (isDragging) 1.1f else 1f
scaleY = if (isDragging) 1.1f else 1f
}
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = { isDragging = true },
onDrag = { change, dragAmount ->
change.consume()
offsetX += dragAmount.x
offsetY += dragAmount.y
},
onDragEnd = {
isDragging = false
// 원래 위치로 애니메이션
}
)
}
)
드래그 핸들 아이콘
드래그‑핸들 아이콘을 사용하여 항목이 드래그 가능함을 알립니다:
@Composable
fun DraggableListItem(
item: String,
onDrag: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = onDrag,
onDrag = { change, _ -> change.consume() }
)
},
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.DragHandle,
contentDescription = "Drag to reorder",
modifier = Modifier
.padding(end = 8.dp)
.alpha(0.6f)
)
Text(item, modifier = Modifier.weight(1f))
}
}
SwipeToDismissBox for Swipe‑to‑Delete
SwipeToDismissBox 컴포저블은 사용자 정의 가능한 배경 콘텐츠와 함께 스와이프‑투‑디스미스 패턴을 제공합니다:
@Composable
fun SwipeDeleteItem(
item: String,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
SwipeToDismissBox(
modifier = modifier,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red)
.padding(16.dp),
contentAlignment = Alignment.CenterEnd
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.White
)
}
},
content = {
ListItem(
headlineContent = { Text(item) },
modifier = Modifier.background(Color.White)
)
},
onDismissed = { onDelete() }
)
}
전체 재정렬 가능한 리스트
스와이프‑투‑삭제 기능이 포함된 전체 재정렬 가능한 리스트를 위해 이 패턴들을 결합하세요:
@Composable
fun ReorderableListScreen() {
var items by remember { mutableStateOf(listOf("Item 1", "Item 2", "Item 3")) }
var draggedItem by remember { mutableStateOf<String?>(null) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(
items = items,
key = { _, item -> item }
) { index, item ->
SwipeToDismissBox(
modifier = Modifier
.animateItem(
fadeInSpec = tween(300),
fadeOutSpec = tween(300),
placementSpec = spring()
)
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = { draggedItem = item },
onDrag = { change, _ -> change.consume() },
onDragEnd = { draggedItem = null }
)
},
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red)
.padding(16.dp),
contentAlignment = Alignment.CenterEnd
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.White
)
}
},
content = {
ListItem(
headlineContent = { Text(item) },
modifier = Modifier.background(
if (draggedItem == item) Color.LightGray else Color.White
)
)
},
onDismissed = {
items = items.toMutableList().also { it.remove(item) }
}
)
}
}
}
Source: …
Swipe‑to‑Dismiss & Drag‑to‑Reorder 예제
@Composable
fun SwipeAndDragList() {
var items by remember { mutableStateOf(listOf("Item 1", "Item 2", "Item 3")) }
LazyColumn {
itemsIndexed(
items = items,
key = { _, item -> item }
) { index, item ->
SwipeToDismiss(
state = rememberDismissState(),
background = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
contentAlignment = Alignment.CenterEnd
) {
Icon(Icons.Default.Delete, contentDescription = null)
}
},
content = {
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.DragHandle, contentDescription = null)
Text(item, modifier = Modifier.weight(1f))
}
},
onDismissed = {
items = items.filterNot { it == item }
}
)
}
}
}
성능 최적화
itemsIndexed()의 key 매개변수를 사용하여 올바른 컴포지션 추적을 수행합니다:
itemsIndexed(
items = items,
key = { _, item -> item.id } // 고유하고 안정적인 식별자
) { index, item ->
// Your content
}
안정적인 키를 제공하면 리스트 재정렬 중에 애니메이션과 상태 관리가 올바르게 작동합니다.
모범 사례
- 안정적인 키 사용 – 리스트 항목마다 고유하고 안정적인 식별자를 항상 제공하세요.
- 시각적 피드백 – 드래그 중에 스케일/알파 변화를 보여주어 UX를 향상시키세요.
- 접근성 – 드래그 핸들과 삭제 동작에 대한 라벨을 명확히 표시하세요.
- 성능 – 드래그되지 않은 항목은 불필요하게 재컴포즈되지 않도록 하세요.
- 애니메이션 – 부드러운 리스트 전환을 위해
animateItem()을 사용하세요.
Jetpack Compose의 제스처 감지 및 Modifier 시스템을 활용하면 반응성이 뛰어나고 세련된 인터랙티브 리스트를 손쉽게 구현할 수 있습니다.
8 Android App Templates →
