728x90
안녕하세요 오늘은 Jetpack Compose의 주요 기능 (제스처, 애니메이션, 리스트 렌더링) 들을 활용해
리스트의 아이템들을 드래그 앤 드롭 해보도록 하겠습니다
To-Do List, 이미지 갤러리, 카드 정렬 등 다양한 앱에서 응용 가능합니다
1. ToDoListWithFloatingDrag
리스트의 전체 구조 및 드래그 앤 드롭 로직을 관리하는 함수입니다
@Composable
fun ToDoListWithFloatingDrag() {
var tasks by remember { mutableStateOf(mutableListOf("Task 1", "Task 2", "Task 3", "Task 4")) }
var draggedIndex by remember { mutableStateOf<Int?>(null) }
var offsetY by remember { mutableStateOf(0f) }
val coroutineScope = rememberCoroutineScope()
val dragThreshold = 50f // 이동을 위한 최소 거리
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(tasks) { index, task ->
val isDragging = draggedIndex == index
DraggableTaskItem(
task = task,
isDragging = isDragging,
offsetY = if (isDragging) offsetY else 0f,
onDragStart = {
draggedIndex = index
},
onDrag = { dragAmount ->
if (draggedIndex == null) return@DraggableTaskItem
offsetY += dragAmount
// 새로운 인덱스 계산 (양수: 아래, 음수: 위)
val indexShift = (offsetY / 100).toInt()
val newIndex = (draggedIndex!! + indexShift).coerceIn(0, tasks.size - 1)
// 새로운 인덱스로 이동 (올림/내림 포함)
if (newIndex != draggedIndex && kotlin.math.abs(offsetY) > dragThreshold) {
coroutineScope.launch {
tasks = tasks.toMutableList().apply {
add(newIndex, removeAt(draggedIndex!!))
}
offsetY = 0f // 오프셋 초기화
draggedIndex = newIndex
}
}
},
onDragEnd = {
draggedIndex = null
offsetY = 0f
}
)
}
}
}
드래그 앤 드롭 기능의 핵심은 리스트 데이터(tasks)를 사용자가 드래그한 위치에 따라 재정렬하는 로직입니다. 이를 위해 LazyColumn에 렌더링된 리스트 형태의 UI와 사용자 제스처를 처리하여 동적으로 리스트를 업데이트합니다.
2. DraggableTaskItem
개별 아이템의 제스처 감지와 UI 표현을 담당하는 함수입니다.
@Composable
fun DraggableTaskItem(
task: String,
isDragging: Boolean,
offsetY: Float,
onDragStart: () -> Unit,
onDrag: (Float) -> Unit,
onDragEnd: () -> Unit
) {
val animatedOffsetY by animateFloatAsState(targetValue = if (isDragging) offsetY else 0f)
Box(
modifier = Modifier
.fillMaxWidth()
.background(if (isDragging) Color.Gray else Color.LightGray, shape = MaterialTheme.shapes.medium)
.shadow(if (isDragging) 8.dp else 0.dp)
.offset { IntOffset(0, animatedOffsetY.roundToInt()) }
.zIndex(if (isDragging) 1f else 0f) // 드래그 중인 아이템이 위로 보이도록 설정
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { onDragStart() },
onDragEnd = { onDragEnd() },
onDragCancel = { onDragEnd() },
onDrag = { change, dragAmount ->
change.consumePositionChange()
onDrag(dragAmount.y)
}
)
}
.padding(16.dp),
contentAlignment = Alignment.CenterStart
) {
Text(
text = task,
fontSize = 16.sp,
color = Color.Black
)
}
}
드래그 앤 드롭 기능에서 개별 아이템의 UI와 제스처를 처리하는 핵심은 DraggableTaskItem 컴포저블입니다. 이 함수는 사용자 드래그 제스처를 감지하고, 드래그 중인 아이템을 시각적으로 떨어져 움직이는 효과를 제공합니다.
아래는 메인 액티비티가 포함된 전체 코드입니다
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
ToDoListWithFloatingDrag()
}
}
}
}
}
@Composable
fun ToDoListWithFloatingDrag() {
var tasks by remember { mutableStateOf(mutableListOf("Task 1", "Task 2", "Task 3", "Task 4")) }
var draggedIndex by remember { mutableStateOf<Int?>(null) }
var offsetY by remember { mutableStateOf(0f) }
val coroutineScope = rememberCoroutineScope()
val dragThreshold = 50f // 이동을 위한 최소 거리
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(tasks) { index, task ->
val isDragging = draggedIndex == index
DraggableTaskItem(
task = task,
isDragging = isDragging,
offsetY = if (isDragging) offsetY else 0f,
onDragStart = {
draggedIndex = index
},
onDrag = { dragAmount ->
if (draggedIndex == null) return@DraggableTaskItem
offsetY += dragAmount
// 새로운 인덱스 계산 (양수: 아래, 음수: 위)
val indexShift = (offsetY / 100).toInt()
val newIndex = (draggedIndex!! + indexShift).coerceIn(0, tasks.size - 1)
// 새로운 인덱스로 이동 (올림/내림 포함)
if (newIndex != draggedIndex && kotlin.math.abs(offsetY) > dragThreshold) {
coroutineScope.launch {
tasks = tasks.toMutableList().apply {
add(newIndex, removeAt(draggedIndex!!))
}
offsetY = 0f // 오프셋 초기화
draggedIndex = newIndex
}
}
},
onDragEnd = {
draggedIndex = null
offsetY = 0f
}
)
}
}
}
@Composable
fun DraggableTaskItem(
task: String,
isDragging: Boolean,
offsetY: Float,
onDragStart: () -> Unit,
onDrag: (Float) -> Unit,
onDragEnd: () -> Unit
) {
val animatedOffsetY by animateFloatAsState(targetValue = if (isDragging) offsetY else 0f)
Box(
modifier = Modifier
.fillMaxWidth()
.background(if (isDragging) Color.Gray else Color.LightGray, shape = MaterialTheme.shapes.medium)
.shadow(if (isDragging) 8.dp else 0.dp)
.offset { IntOffset(0, animatedOffsetY.roundToInt()) }
.zIndex(if (isDragging) 1f else 0f) // 드래그 중인 아이템이 위로 보이도록 설정
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { onDragStart() },
onDragEnd = { onDragEnd() },
onDragCancel = { onDragEnd() },
onDrag = { change, dragAmount ->
change.consumePositionChange()
onDrag(dragAmount.y)
}
)
}
.padding(16.dp),
contentAlignment = Alignment.CenterStart
) {
Text(
text = task,
fontSize = 16.sp,
color = Color.Black
)
}
}
시연 영상입니다.
'안드로이드 코틀린' 카테고리의 다른 글
[Kotlin][Android] Jetpack Compose로 검색 필터 기능 만들기 (4) | 2024.12.21 |
---|---|
[Kotlin][Android] 슬라이더와 애니메이션을 활용한 RGB 색상 조합기 만들기 (4) | 2024.12.09 |
[Kotlin][Android] Jetpack Compose를 이용한 부드러운 막대 그래프 애니메이션 구현하기 (0) | 2024.12.03 |
[Kotlin][Android] Jetpack Compose 텍스트 작성 중인 상태에 대해 사용자에게 알리기 (0) | 2024.11.27 |
[Kotlin][Android] 문자열 형식 체크하기 (0) | 2024.11.23 |