From 6c55b9eeec29deb6f05cda15c5242d5eb2bafa9f Mon Sep 17 00:00:00 2001 From: sanjeok77 <1+sanjeok77@noreply.localhost> Date: Thu, 20 Aug 2026 23:59:44 +0000 Subject: [PATCH] v0.2.9: True Touch Drag & Drop Reordering & Fix OTA Auto-Install Bug --- .../components/DragDropListState.kt | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 app/src/main/java/com/hotdeal/alarm/presentation/components/DragDropListState.kt diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/components/DragDropListState.kt b/app/src/main/java/com/hotdeal/alarm/presentation/components/DragDropListState.kt new file mode 100644 index 0000000..1029672 --- /dev/null +++ b/app/src/main/java/com/hotdeal/alarm/presentation/components/DragDropListState.kt @@ -0,0 +1,138 @@ +package com.hotdeal.alarm.presentation.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.zIndex +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Compose LazyColumn Drag & Drop Reordering State + */ +@Composable +fun rememberDragDropListState( + lazyListState: LazyListState, + onMove: (Int, Int) -> Unit, + onDragEnd: () -> Unit = {} +): DragDropListState { + val scope = rememberCoroutineScope() + val state = remember(lazyListState) { + DragDropListState( + lazyListState = lazyListState, + onMove = onMove, + onDragEnd = onDragEnd, + scope = scope + ) + } + return state +} + +class DragDropListState( + val lazyListState: LazyListState, + private val onMove: (Int, Int) -> Unit, + private val onDragEnd: () -> Unit, + private val scope: CoroutineScope +) { + var initiallyDraggedElement by mutableStateOf(null) + private set + + var currentIndexOfDraggedItem by mutableStateOf(null) + private set + + private val dragOffset = Animatable(0f) + + val elementOffset: Float + get() = dragOffset.value + + fun onDragStart(offset: Offset) { + lazyListState.layoutInfo.visibleItemsInfo + .firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) } + ?.also { item -> + currentIndexOfDraggedItem = item.index + initiallyDraggedElement = item + } + } + + fun onDragInterrupted() { + initiallyDraggedElement = null + currentIndexOfDraggedItem = null + scope.launch { + dragOffset.snapTo(0f) + } + } + + fun onDrag(change: Offset) { + scope.launch { + dragOffset.snapTo(dragOffset.value + change.y) + + val currentElement = initiallyDraggedElement ?: return@launch + val startOffset = currentElement.offset + dragOffset.value + val endOffset = startOffset + currentElement.size + + val hoveredItem = lazyListState.layoutInfo.visibleItemsInfo + .firstOrNull { item -> + val itemMid = item.offset + item.size / 2 + val isMidInRange = itemMid in startOffset.toInt()..endOffset.toInt() + isMidInRange && item.index != currentIndexOfDraggedItem + } + + if (hoveredItem != null) { + val currentIndex = currentIndexOfDraggedItem ?: return@launch + val targetIndex = hoveredItem.index + onMove(currentIndex, targetIndex) + currentIndexOfDraggedItem = targetIndex + } + } + } + + fun onDragStop() { + onDragEnd() + scope.launch { + dragOffset.animateTo(0f, spring(stiffness = Spring.StiffnessMediumLow)) + initiallyDraggedElement = null + currentIndexOfDraggedItem = null + } + } +} + +fun Modifier.dragDropGesture( + dragDropState: DragDropListState +): Modifier = this.pointerInput(dragDropState) { + detectDragGesturesAfterLongPress( + onDragStart = { offset -> dragDropState.onDragStart(offset) }, + onDragEnd = { dragDropState.onDragStop() }, + onDragCancel = { dragDropState.onDragInterrupted() }, + onDrag = { change, dragAmount -> + change.consume() + dragDropState.onDrag(dragAmount) + } + ) +} + +fun Modifier.dragDropItem( + index: Int, + dragDropState: DragDropListState +): Modifier = this.then( + if (index == dragDropState.currentIndexOfDraggedItem) { + Modifier + .zIndex(10f) + .graphicsLayer { + translationY = dragDropState.elementOffset + scaleX = 1.03f + scaleY = 1.03f + shadowElevation = 8f + } + } else { + Modifier + } +)