feat: [#25] drag & drop dans la vue liste du projet (long-press handle, reorderTasks)
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package com.planify.mobile.ui.components
|
||||
|
||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.DragHandle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.zIndex
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** State for a reorderable LazyColumn. */
|
||||
class ReorderState {
|
||||
var draggedIndex by mutableStateOf<Int?>(null)
|
||||
var dragOffsetY by mutableFloatStateOf(0f)
|
||||
var overIndex by mutableIntStateOf(-1)
|
||||
|
||||
fun isDragged(index: Int) = draggedIndex == index
|
||||
fun isOver(index: Int) = overIndex == index
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberReorderState() = remember { ReorderState() }
|
||||
|
||||
/**
|
||||
* Modifier for a drag handle icon that drives a [ReorderState].
|
||||
* Call [onReorder] when the drag ends with the new list order.
|
||||
*/
|
||||
@Composable
|
||||
fun <T> Modifier.reorderDragHandle(
|
||||
item: T,
|
||||
index: Int,
|
||||
items: List<T>,
|
||||
state: ReorderState,
|
||||
listState: LazyListState,
|
||||
onReorder: (List<T>) -> Unit,
|
||||
): Modifier {
|
||||
val scope = rememberCoroutineScope()
|
||||
return this.pointerInput(item, items) {
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDragStart = {
|
||||
state.draggedIndex = index
|
||||
state.dragOffsetY = 0f
|
||||
state.overIndex = index
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
state.dragOffsetY += dragAmount.y
|
||||
val currentInfo: LazyListItemInfo? = listState.layoutInfo.visibleItemsInfo
|
||||
.firstOrNull { it.index == (state.draggedIndex ?: -1) }
|
||||
if (currentInfo != null) {
|
||||
val currentCenter = currentInfo.offset + currentInfo.size / 2 + state.dragOffsetY.toInt()
|
||||
val newOver = listState.layoutInfo.visibleItemsInfo
|
||||
.minByOrNull { kotlin.math.abs(it.offset + it.size / 2 - currentCenter) }
|
||||
?.index ?: state.overIndex
|
||||
state.overIndex = newOver
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
val from = state.draggedIndex ?: return@detectDragGesturesAfterLongPress
|
||||
val to = state.overIndex
|
||||
if (from != to && to >= 0 && to < items.size) {
|
||||
val mutable = items.toMutableList()
|
||||
val moved = mutable.removeAt(from)
|
||||
mutable.add(to, moved)
|
||||
scope.launch { onReorder(mutable) }
|
||||
}
|
||||
state.draggedIndex = null
|
||||
state.dragOffsetY = 0f
|
||||
state.overIndex = -1
|
||||
},
|
||||
onDragCancel = {
|
||||
state.draggedIndex = null
|
||||
state.dragOffsetY = 0f
|
||||
state.overIndex = -1
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Modifier to apply drag visual feedback (elevation + offset) to a dragged item. */
|
||||
fun Modifier.draggedItemModifier(isDragged: Boolean, offsetY: Float): Modifier =
|
||||
if (isDragged) this
|
||||
.zIndex(1f)
|
||||
.graphicsLayer { translationY = offsetY; shadowElevation = 8f }
|
||||
else this
|
||||
|
||||
@Composable
|
||||
fun DragHandleIcon(modifier: Modifier = Modifier) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DragHandle,
|
||||
contentDescription = "Réordonner",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.planify.mobile.ui.project
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -9,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FolderOpen
|
||||
import androidx.compose.material3.Card
|
||||
@@ -21,14 +23,20 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.planify.mobile.domain.model.Task
|
||||
import com.planify.mobile.domain.model.ViewStyle
|
||||
import com.planify.mobile.ui.components.DragHandleIcon
|
||||
import com.planify.mobile.ui.components.EmptyState
|
||||
import com.planify.mobile.ui.components.ReorderState
|
||||
import com.planify.mobile.ui.components.SectionHeader
|
||||
import com.planify.mobile.ui.components.TaskRow
|
||||
import com.planify.mobile.ui.components.draggedItemModifier
|
||||
import com.planify.mobile.ui.components.reorderDragHandle
|
||||
import com.planify.mobile.ui.components.rememberReorderState
|
||||
|
||||
@Composable
|
||||
fun ProjectScreen(
|
||||
@@ -62,6 +70,7 @@ fun ProjectScreen(
|
||||
},
|
||||
onTaskClick = onTaskClick,
|
||||
onCheckedChange = { task -> viewModel.toggleTask(task) },
|
||||
onReorder = { viewModel.reorderTasks(it) },
|
||||
modifier = modifier,
|
||||
)
|
||||
ViewStyle.BOARD -> ProjectBoardView(
|
||||
@@ -80,9 +89,13 @@ private fun ProjectListView(
|
||||
onToggleSection: (String) -> Unit,
|
||||
onTaskClick: (Task) -> Unit,
|
||||
onCheckedChange: (Task) -> Unit,
|
||||
onReorder: (List<Task>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier.fillMaxSize()) {
|
||||
val listState = rememberLazyListState()
|
||||
val reorderState = rememberReorderState()
|
||||
|
||||
LazyColumn(state = listState, modifier = modifier.fillMaxSize()) {
|
||||
state.sections.forEach { group ->
|
||||
val key = group.section?.id ?: "unsectioned"
|
||||
val name = group.section?.name ?: "Sans section"
|
||||
@@ -98,13 +111,34 @@ private fun ProjectListView(
|
||||
}
|
||||
|
||||
if (key !in collapsedSections) {
|
||||
items(group.tasks, key = { it.id }) { task ->
|
||||
itemsIndexed(group.tasks, key = { _, t -> t.id }) { index, task ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.draggedItemModifier(
|
||||
isDragged = reorderState.isDragged(index),
|
||||
offsetY = if (reorderState.isDragged(index)) reorderState.dragOffsetY else 0f,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
DragHandleIcon(
|
||||
modifier = Modifier.reorderDragHandle(
|
||||
item = task,
|
||||
index = index,
|
||||
items = group.tasks,
|
||||
state = reorderState,
|
||||
listState = listState,
|
||||
onReorder = onReorder,
|
||||
)
|
||||
)
|
||||
TaskRow(
|
||||
task = task,
|
||||
onCheckedChange = { onCheckedChange(task) },
|
||||
onClick = { onTaskClick(task) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
item { HorizontalDivider(Modifier.padding(horizontal = 16.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,4 +68,8 @@ class ProjectViewModel @Inject constructor(
|
||||
fun toggleTask(task: Task) {
|
||||
viewModelScope.launch { taskRepository.checkTask(task.id, !task.checked) }
|
||||
}
|
||||
|
||||
fun reorderTasks(reordered: List<Task>) {
|
||||
viewModelScope.launch { taskRepository.reorderTasks(reordered.map { it.id }) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user