[Compose] LazyColumn Drag And Drop Reordering

2024. 9. 9. 01:23·📂 Android/Compose
목차
  1. draggableItems
  2. dragContainer
  3. rememberDragDropState
  4. DragDropState
  5. ReorderableScreen
  6. Item
  7. 전체 코드
  8. 참고
반응형

이번 블로그 포스트에서는 Jetpack Compose를 사용하여 드래그 앤 드롭 기능이 포함된 재정렬 가능한 리스트를 만드는 방법을 소개합니다. 이 예제는 사용자가 리스트 아이템을 재정렬하고 제거할 수 있는 기능을 간단 애니메이션과 함께 제공합니다.

 

draggableItems

이 함수는 LazyListScope에 대한 확장 함수로, 리스트의 아이템을 드래그할 수 있도록 표시합니다.

@OptIn(ExperimentalFoundationApi::class)
inline fun <T : Any> LazyListScope.draggableItems(
items: List<T>,
dragDropState: DragDropState,
crossinline content: @Composable (Modifier, T) -> Unit,
) {
itemsIndexed(
items = items,
contentType = { index, _ -> DraggableItem(index = index) })
{ index, item ->
val modifier = if (dragDropState.draggingItemIndex == index) {
Modifier
.zIndex(1f)
.graphicsLayer {
translationY = dragDropState.delta
}
} else {
Modifier.animateItemPlacement(
animationSpec = tween(
durationMillis = 500,
easing = LinearOutSlowInEasing
)
)
}
content(modifier, item)
}
}

 

목적: 이 함수는 드래그 상태에 따라 아이템의 애니메이션과 시각적 표현을 처리합니다.

매개변수

  • items: 표시할 아이템 목록입니다.
  • dragDropState: 드래그 앤 드롭 작업을 관리하는 상태 객체입니다.
  • content: 각 아이템이 어떻게 표시될지를 정의하는 람다입니다.

기능: 드래그 중인 아이템은 시각적으로 들어 올려지며 (zIndex와 graphicsLayer 사용), 다른 아이템은 위치가 변경될 때 애니메이션을 적용합니다.

 

 

dragContainer

이 함수는 Modifier에 드래그 처리를 추가합니다.

fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
return this.then(pointerInput(dragDropState) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
// 사용자의 터치 이벤트를 소비함으로써 다른 핸들러로 전달되지 않도록 함
change.consume()
// DragDropState 객체의 onDrag 함수를 호출하여 현재 드래그 중임을 알리고, 이동된 거리를 전달
dragDropState.onDrag(offset = offset)
},
onDragStart = { offset ->
// DragDropState 객체의 onDragStart 함수를 호출하여 드래그 시작을 알림
dragDropState.onDragStart(offset)
},
onDragEnd = {
// DragDropState 객체의 onDragInterrupted 함수를 호출하여 드래그 동작이 중단됨을 알림
dragDropState.onDragInterrupted()
},
onDragCancel = {
// DragDropState 객체의 onDragInterrupted 함수를 호출하여 드래그 동작이 중단됨을 알림
dragDropState.onDragInterrupted()
}
)
})
}

 

목적: 이 함수는 드래그 동작을 감지하고 처리하기 위해 pointerInput을 사용합니다.

매개변수:

  • dragDropState: 드래그 앤 드롭 상태를 관리하는 객체입니다.

기능: 아이템을 드래그할 때, 시작, 진행, 종료, 취소 이벤트를 처리하여 상태를 업데이트합니다.

 

pointerInput: 터치 이벤트를 감지하기 위해 사용되는 Modifier

detectDragGesturesAfterLongPress:

    사용자가 컴포넌트를 길게 누른 후 드래그 제스처를 감지

    드래그 중 또는 드래그 완료 시 발생하는 다양한 이벤트에 대한 콜백을 제공

 

 

rememberDragDropState

이 함수는 드래그 앤 드롭 상태를 기억하고 관리합니다.

@Composable
fun rememberDragDropState(
lazyListState: LazyListState,
onMove: (Int, Int) -> Unit,
draggableItemsNum: Int
): DragDropState {
val state = remember(lazyListState) {
DragDropState(
draggableItemsNum = draggableItemsNum,
stateList = lazyListState,
onMove = onMove,
)
}
LaunchedEffect(state) {
while (true) {
val diff = state.scrollChannel.receive()
lazyListState.scrollBy(diff)
}
}
return state
}

 

목적: 이 함수는 드래그 앤 드롭 상태를 기억하고 관리합니다.

매개변수:

  • lazyListState: LazyListState 객체입니다.
  • onMove: 아이템이 이동할 때 호출되는 콜백 함수입니다.
  • draggableItemsNum: 드래그 가능한 아이템의 수입니다.

기능: 상태를 기억하고, 드래그 중 스크롤을 처리합니다.

 

 

DragDropState

이 클래스는 드래그 앤 드롭 상태를 관리합니다.

class DragDropState(
private val draggableItemsNum: Int, // 드래그 가능한 항목의 총 개수
private val stateList: LazyListState, // LazyColumn 또는 LazyRow의 상태 객체
private val onMove: (Int, Int) -> Unit, // 항목 이동을 처리하는 람다 함수
) {
var draggingItemIndex: Int? by mutableStateOf(null) // 현재 드래그 중인 항목의 인덱스
var delta by mutableFloatStateOf(0f) // 드래그 시 누적된 Y축 이동 거리
val scrollChannel = Channel<Float>() // 자동 스크롤을 위한 채널
private var draggingItem: LazyListItemInfo? = null // 드래그 중인 항목의 정보
internal fun onDragStart(offset: Offset) {
// 드래그 시작 시 호출됨
stateList.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) }
?.also {
// 터치 위치에 해당하는 항목이 드래그 가능하다면 설정
(it.contentType as? DraggableItem)?.let { draggableItem ->
draggingItem = it
draggingItemIndex = draggableItem.index
}
}
}
internal fun onDragInterrupted() {
// 드래그가 중단되었을 때 호출됨 (예: 터치가 해제된 경우)
draggingItem = null
draggingItemIndex = null
delta = 0f
}
internal fun onDrag(offset: Offset) {
// 드래그 중일 때 반복적으로 호출됨
delta += offset.y // 누적 이동 거리 업데이트
val currentDraggingItemIndex = draggingItemIndex ?: return
val currentDraggingItem = draggingItem ?: return
// 드래그 중인 항목의 시작, 중간, 끝 위치 계산
val startOffset = currentDraggingItem.offset + delta
val endOffset = currentDraggingItem.offset + currentDraggingItem.size + delta
val middleOffset = startOffset + (endOffset - startOffset) / 2
// 드래그 중인 항목과 교환할 수 있는 타겟 항목 찾기
val targetItem = stateList.layoutInfo.visibleItemsInfo.find { item ->
middleOffset.toInt() in item.offset..item.offset + item.size &&
currentDraggingItem.index != item.index &&
item.contentType is DraggableItem
}
if (targetItem != null) {
// 타겟 항목을 찾은 경우 항목 교환 수행
val targetIndex = (targetItem.contentType as DraggableItem).index
onMove(currentDraggingItemIndex, targetIndex) // 항목 이동 처리
draggingItemIndex = targetIndex
delta += currentDraggingItem.offset - targetItem.offset
draggingItem = targetItem
} else {
// 타겟 항목을 찾지 못한 경우 스크롤 처리
val startOffsetToTop = startOffset - stateList.layoutInfo.viewportStartOffset
val endOffsetToBottom = endOffset - stateList.layoutInfo.viewportEndOffset
val scroll = when {
startOffsetToTop < 0 -> startOffsetToTop.coerceAtMost(0f) // 위로 스크롤
endOffsetToBottom > 0 -> endOffsetToBottom.coerceAtLeast(0f) // 아래로 스크롤
else -> 0f
}
// 스크롤이 필요하고, 드래그 중인 항목이 첫 번째 또는 마지막 항목이 아닌 경우
if (scroll != 0f && currentDraggingItemIndex != 0 && currentDraggingItemIndex != draggableItemsNum - 1) {
scrollChannel.trySend(scroll) // 스크롤 명령 전송
}
}
}
}

 

목적: 드래그 앤 드롭의 상태를 관리하고, 드래그 시작, 진행, 종료 이벤트를 처리합니다.

 

주요 기능:

  • onDragStart(): 드래그가 시작될 때 호출됩니다. 현재 드래그 중인 아이템을 식별합니다.
  • onDragInterrupted(): 드래그가 중단될 때 호출됩니다. 상태를 초기화합니다.
  • onDrag(): 드래그 중 아이템의 위치를 업데이트합니다. 필요한 경우 리스트를 스크롤합니다.

 

 

ReorderableScreen

@Composable
fun ReorderableScreen() {
var list by remember { mutableStateOf((0 until 20).toList()) }
var scrollToIndex by remember { mutableStateOf<Int?>(null) }
val draggableItems by remember { derivedStateOf { list.size } }
val stateList = rememberLazyListState()
val dragDropState = rememberDragDropState(
lazyListState = stateList,
draggableItemsNum = draggableItems,
onMove = { fromIndex, toIndex ->
val adjustedFromIndex = list.size - 1 - fromIndex
val adjustedToIndex = list.size - 1 - toIndex
list = list.toMutableList().apply {
add(adjustedToIndex, removeAt(adjustedFromIndex))
}
}
)
LaunchedEffect(scrollToIndex) {
scrollToIndex?.let { index ->
stateList.animateScrollToItem(index)
scrollToIndex = null
}
}
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(onClick = {
val newIndex = list.size
list = list.toMutableList().apply { add(newIndex) }
scrollToIndex = 0
}) {
Text("Add Item")
}
}
LazyColumn(
modifier = Modifier
.dragContainer(dragDropState)
.fillMaxSize(),
state = stateList,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text(text = "Memo", fontSize = 30.sp)
}
draggableItems(items = list.reversed(), dragDropState = dragDropState) { modifier, item ->
Item(
modifier = modifier,
index = item,
onRemove = { itemToRemove ->
list = list.toMutableList().filterNot { it == itemToRemove }
}
)
}
}
}
}

 

 

Item

이 함수는 개별 아이템을 구성합니다.

@Composable
private fun Item(modifier: Modifier = Modifier, index: Int, onRemove: (Int) -> Unit) {
Card(
modifier = modifier
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Item $index",
fontSize = 18.sp
)
Box(
modifier = Modifier
.clickable(
onClick = { onRemove(index) },
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
.padding(8.dp)
) {
Icon(
imageVector = Icons.Filled.Clear,
contentDescription = "Remove item"
)
}
}
}
}

 


전체 코드

 

Android-Compose-Study/presentation/week1/ReorderableList at main · moondev03/Android-Compose-Study

Android Jetpack Compose Study. Contribute to moondev03/Android-Compose-Study development by creating an account on GitHub.

github.com

 

 

참고

 

GitHub - Artemake/Reordering-LazyColumn

Contribute to Artemake/Reordering-LazyColumn development by creating an account on GitHub.

github.com

 

반응형

'Android > Compose' 카테고리의 다른 글

[Compose] Modifier Extension - DrawScrollbar  (0) 2024.09.25
[Compose] Modifier Extension - AddFocusCleaner  (0) 2024.09.25
[Compose] Jetpack Compose 상태 관리 기초  (0) 2024.09.08
[Compose] Android Composable Lifecycle  (0) 2024.09.08
[Compose] Slot-based Layouts  (0) 2024.09.06
  1. draggableItems
  2. dragContainer
  3. rememberDragDropState
  4. DragDropState
  5. ReorderableScreen
  6. Item
  7. 전체 코드
  8. 참고
'Android/Compose' 카테고리의 다른 글
  • [Compose] Modifier Extension - DrawScrollbar
  • [Compose] Modifier Extension - AddFocusCleaner
  • [Compose] Jetpack Compose 상태 관리 기초
  • [Compose] Android Composable Lifecycle
Moondev
Moondev
공부 기록장
  • Moondev
    Moondev
    Moondev
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Android
        • ViewSystem
        • Compose
      • Language
        • Kotlin
      • Review
        • 프로젝트 후기
        • Conference 후기
        • 우아한테크코스 후기
      • Etc
        • Git
        • Gradle
  • 링크

    • GitHub
  • 인기 글

  • 300x250
  • hELLO· Designed By정상우.v4.10.3
Moondev
Android / Compose / 
[Compose] LazyColumn Drag And Drop Reordering
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.