admin管理员组文章数量:1025269
I am developing a test application with Jetpack Compose using drag and drop functionalities. I have 3 tasks that are in each assigned column: To Do, In Progress, and Done. When I try to move any task to another column, it changes but then returns to the initial column. What could be the issue?
Here is my code on GitHub: GitHub Repository
DragAndDrop.kt
internal val LocalDragTargetIfo = compositionLocalOf{ DragTargetInfo() }
@Composable
fun DragableScreen(modifier: Modifier = Modifier,content: @Composable BoxScope.() -> Unit){
val state= remember {
DragTargetInfo()
}
CompositionLocalProvider(LocalDragTargetIfo provides state){
Box(modifier=Modifier.fillMaxSize()){
content()
if(state.isDagging){
var targetSize by remember {
mutableStateOf(IntSize.Zero)
}
Box(
modifier = Modifier
.graphicsLayer {
val offset = (state.dragPosition + state.dragOffset)
scaleX = 1.3f
scaleY = 1.3f
alpha = if (targetSize == IntSize.Zero) 0f else .9f
translationX = offset.x.minus(targetSize.width / 2)
translationY = offset.y.minus(targetSize.height / 2)
}
.onGloballyPositioned {
targetSize = it.size
}
){
state.draggableComposable?.invoke()
}
}
}
}
}
@Composable
fun <T> DragTrarget(
modifier: Modifier = Modifier,
datatoDrop: T,
viewModel: MainViewModel,
content: @Composable () -> Unit
) {
var currentPosition by remember { mutableStateOf(Offset.Zero) }
val currentState = LocalDragTargetIfo.current
Box(
modifier = modifier
.onGloballyPositioned { currentPosition = it.localToWindow(Offset.Zero) }
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
viewModel.startDragging()
currentState.dataToDrop = datatoDrop
currentState.isDagging = true
currentState.dragPosition = currentPosition + it
currentState.draggableComposable = content
},
onDrag = { change, dragAmount ->
change.consume()
currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
},
onDragEnd = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
},
onDragCancel = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
}
)
}
) {
content()
}
}
@Composable
fun <T> DropItem(
modifier: Modifier,
content: @Composable BoxScope.(isInBound: Boolean, data: T?) -> Unit
) {
val dragInfo = LocalDragTargetIfo.current
val dragPosition = dragInfo.dragPosition
val dragOffset = dragInfo.dragOffset
var isCurretDropTarget by remember { mutableStateOf(false) }
Box(
modifier = modifier
.background(Color.Red)
.onGloballyPositioned {
it.boundsInWindow().let { rect ->
isCurretDropTarget = rect.contains(dragPosition + dragOffset)
}
}
) {
val data = if (isCurretDropTarget && !dragInfo.isDagging) {
dragInfo.dataToDrop as T?
} else {
null
}
// Solo ejecuta el contenido si el arrastre ha terminado y está en el área de soltar
if (!dragInfo.isDagging && isCurretDropTarget && data != null) {
content(true, data)
} else {
content(false, null)
}
}
}
internal class DragTargetInfo {
var isDagging:Boolean by mutableStateOf(false)
var dragPosition by mutableStateOf(Offset.Zero)
var dragOffset by mutableStateOf(Offset.Zero)
var draggableComposable by mutableStateOf <(@Composable () -> Unit)?>(null)
var dataToDrop by mutableStateOf<Any?>(null)
}
MainViewModel.kt
class MainViewModel : ViewModel() {
var isCurrentlyDragging by mutableStateOf(false)
private set
val items = mutableStateListOf<Task>()
var addedTasks = mutableStateListOf<Task>()
private set
fun addExampleTasks() {
items.addAll(listOf(
Task(1, "Task 1","Description 1", TaskStatus.TO_DO),
Task(2, "Task 2","Description 2", TaskStatus.IN_PROGRESS),
Task(3, "Task 3","Description 3", TaskStatus.DONE)
))
}
fun startDragging() {
isCurrentlyDragging = true
}
fun stopDragging() {
isCurrentlyDragging = false
}
fun addTask(task: Task) {
items.add(task)
}
fun updateTaskStatus(task: Task, newStatus: TaskStatus) {
val index = items.indexOfFirst { it.id == task.id }
if (index != -1) {
val currentTask = items[index]
if (currentTask.status != newStatus) {
println("Updating task ${task.id} from ${currentTask.status} to $newStatus")
// Actualizar el estado directamente
items[index] = currentTask.copy(status = newStatus)
}
}
}
}
MainScreen.kt
@Composable
fun MainScreen(mainViewModel: MainViewModel = viewModel()) {
// Observar directamente el estado de items
val tasks = mainViewModel.items
// Llamar a un método para añadir tareas de ejemplo (solo si es necesario)
LaunchedEffect(Unit) {
if (tasks.isEmpty()) {
mainViewModel.addExampleTasks()
}
}
Row(modifier = Modifier.fillMaxSize()) {
TaskColumn(
title = "To Do",
tasks = tasks.filter { it.status == TaskStatus.TO_DO },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.TO_DO)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "In Progress",
tasks = tasks.filter { it.status == TaskStatus.IN_PROGRESS },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.IN_PROGRESS)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "Done",
tasks = tasks.filter { it.status == TaskStatus.DONE },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.DONE)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
}
}
@Composable
fun TaskColumn(
title: String,
tasks: List<Task>,
onTaskDropped: (Task) -> Unit,
viewModel: MainViewModel, // Asegúrate de recibir el ViewModel
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxHeight()
.padding(8.dp)
.border(1.dp, Color.Black, shape = RoundedCornerShape(8.dp))
) {
Text(title, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(8.dp))
tasks.forEach { task ->
DragTrarget(
modifier = Modifier.wrapContentWidth().padding(4.dp),
datatoDrop = task,
viewModel = viewModel // Pasar el ViewModel aquí
) {
TaskItem(task)
}
}
DropItem<Task>(
modifier = Modifier.fillMaxSize()
) { isInBound, droppedTask ->
if (isInBound && droppedTask != null) {
println("Task dropped: ${droppedTask.title} to $title")
onTaskDropped(droppedTask)
}
}
}
}
@Composable
fun TaskItem(task: Task) {
Box(
modifier = Modifier
.wrapContentWidth()
.padding(8.dp)
.background(Color.LightGray, shape = RoundedCornerShape(8.dp)) // Aquí se aplica el radio
.border(1.dp, Color.LightGray, shape = RoundedCornerShape(8.dp)) // Asegúrate de aplicar el mismo shape al borde
.padding(8.dp)
) {
Column {
Text("${task.title}:")
Text("${task.description}")
}
}
}
Task.kt
data class Task (val id: Int, val title: String, val description: String, var status: TaskStatus)
enum class TaskStatus{
TO_DO,IN_PROGRESS,DONE
}
I am developing a test application with Jetpack Compose using drag and drop functionalities. I have 3 tasks that are in each assigned column: To Do, In Progress, and Done. When I try to move any task to another column, it changes but then returns to the initial column. What could be the issue?
Here is my code on GitHub: GitHub Repository
DragAndDrop.kt
internal val LocalDragTargetIfo = compositionLocalOf{ DragTargetInfo() }
@Composable
fun DragableScreen(modifier: Modifier = Modifier,content: @Composable BoxScope.() -> Unit){
val state= remember {
DragTargetInfo()
}
CompositionLocalProvider(LocalDragTargetIfo provides state){
Box(modifier=Modifier.fillMaxSize()){
content()
if(state.isDagging){
var targetSize by remember {
mutableStateOf(IntSize.Zero)
}
Box(
modifier = Modifier
.graphicsLayer {
val offset = (state.dragPosition + state.dragOffset)
scaleX = 1.3f
scaleY = 1.3f
alpha = if (targetSize == IntSize.Zero) 0f else .9f
translationX = offset.x.minus(targetSize.width / 2)
translationY = offset.y.minus(targetSize.height / 2)
}
.onGloballyPositioned {
targetSize = it.size
}
){
state.draggableComposable?.invoke()
}
}
}
}
}
@Composable
fun <T> DragTrarget(
modifier: Modifier = Modifier,
datatoDrop: T,
viewModel: MainViewModel,
content: @Composable () -> Unit
) {
var currentPosition by remember { mutableStateOf(Offset.Zero) }
val currentState = LocalDragTargetIfo.current
Box(
modifier = modifier
.onGloballyPositioned { currentPosition = it.localToWindow(Offset.Zero) }
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
viewModel.startDragging()
currentState.dataToDrop = datatoDrop
currentState.isDagging = true
currentState.dragPosition = currentPosition + it
currentState.draggableComposable = content
},
onDrag = { change, dragAmount ->
change.consume()
currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
},
onDragEnd = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
},
onDragCancel = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
}
)
}
) {
content()
}
}
@Composable
fun <T> DropItem(
modifier: Modifier,
content: @Composable BoxScope.(isInBound: Boolean, data: T?) -> Unit
) {
val dragInfo = LocalDragTargetIfo.current
val dragPosition = dragInfo.dragPosition
val dragOffset = dragInfo.dragOffset
var isCurretDropTarget by remember { mutableStateOf(false) }
Box(
modifier = modifier
.background(Color.Red)
.onGloballyPositioned {
it.boundsInWindow().let { rect ->
isCurretDropTarget = rect.contains(dragPosition + dragOffset)
}
}
) {
val data = if (isCurretDropTarget && !dragInfo.isDagging) {
dragInfo.dataToDrop as T?
} else {
null
}
// Solo ejecuta el contenido si el arrastre ha terminado y está en el área de soltar
if (!dragInfo.isDagging && isCurretDropTarget && data != null) {
content(true, data)
} else {
content(false, null)
}
}
}
internal class DragTargetInfo {
var isDagging:Boolean by mutableStateOf(false)
var dragPosition by mutableStateOf(Offset.Zero)
var dragOffset by mutableStateOf(Offset.Zero)
var draggableComposable by mutableStateOf <(@Composable () -> Unit)?>(null)
var dataToDrop by mutableStateOf<Any?>(null)
}
MainViewModel.kt
class MainViewModel : ViewModel() {
var isCurrentlyDragging by mutableStateOf(false)
private set
val items = mutableStateListOf<Task>()
var addedTasks = mutableStateListOf<Task>()
private set
fun addExampleTasks() {
items.addAll(listOf(
Task(1, "Task 1","Description 1", TaskStatus.TO_DO),
Task(2, "Task 2","Description 2", TaskStatus.IN_PROGRESS),
Task(3, "Task 3","Description 3", TaskStatus.DONE)
))
}
fun startDragging() {
isCurrentlyDragging = true
}
fun stopDragging() {
isCurrentlyDragging = false
}
fun addTask(task: Task) {
items.add(task)
}
fun updateTaskStatus(task: Task, newStatus: TaskStatus) {
val index = items.indexOfFirst { it.id == task.id }
if (index != -1) {
val currentTask = items[index]
if (currentTask.status != newStatus) {
println("Updating task ${task.id} from ${currentTask.status} to $newStatus")
// Actualizar el estado directamente
items[index] = currentTask.copy(status = newStatus)
}
}
}
}
MainScreen.kt
@Composable
fun MainScreen(mainViewModel: MainViewModel = viewModel()) {
// Observar directamente el estado de items
val tasks = mainViewModel.items
// Llamar a un método para añadir tareas de ejemplo (solo si es necesario)
LaunchedEffect(Unit) {
if (tasks.isEmpty()) {
mainViewModel.addExampleTasks()
}
}
Row(modifier = Modifier.fillMaxSize()) {
TaskColumn(
title = "To Do",
tasks = tasks.filter { it.status == TaskStatus.TO_DO },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.TO_DO)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "In Progress",
tasks = tasks.filter { it.status == TaskStatus.IN_PROGRESS },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.IN_PROGRESS)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "Done",
tasks = tasks.filter { it.status == TaskStatus.DONE },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.DONE)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
}
}
@Composable
fun TaskColumn(
title: String,
tasks: List<Task>,
onTaskDropped: (Task) -> Unit,
viewModel: MainViewModel, // Asegúrate de recibir el ViewModel
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxHeight()
.padding(8.dp)
.border(1.dp, Color.Black, shape = RoundedCornerShape(8.dp))
) {
Text(title, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(8.dp))
tasks.forEach { task ->
DragTrarget(
modifier = Modifier.wrapContentWidth().padding(4.dp),
datatoDrop = task,
viewModel = viewModel // Pasar el ViewModel aquí
) {
TaskItem(task)
}
}
DropItem<Task>(
modifier = Modifier.fillMaxSize()
) { isInBound, droppedTask ->
if (isInBound && droppedTask != null) {
println("Task dropped: ${droppedTask.title} to $title")
onTaskDropped(droppedTask)
}
}
}
}
@Composable
fun TaskItem(task: Task) {
Box(
modifier = Modifier
.wrapContentWidth()
.padding(8.dp)
.background(Color.LightGray, shape = RoundedCornerShape(8.dp)) // Aquí se aplica el radio
.border(1.dp, Color.LightGray, shape = RoundedCornerShape(8.dp)) // Asegúrate de aplicar el mismo shape al borde
.padding(8.dp)
) {
Column {
Text("${task.title}:")
Text("${task.description}")
}
}
}
Task.kt
data class Task (val id: Int, val title: String, val description: String, var status: TaskStatus)
enum class TaskStatus{
TO_DO,IN_PROGRESS,DONE
}
Share
Improve this question
edited Nov 18, 2024 at 15:07
Jon Amengual
asked Nov 18, 2024 at 12:16
Jon AmengualJon Amengual
153 bronze badges
1
- You can find more details here – Abdo21 Commented Nov 19, 2024 at 19:20
1 Answer
Reset to default 0You don’t need to build drag and drop functionality from scratch in jetpack compose because it supports this natively.
I’ve updated your code to use compose drag and drop features and also made the code a bit simpler and more concise.
Here’s my version:
import android.content.ClipData
import android.content.ClipDescription
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activitypose.setContent
import androidx.activity.enableEdgeToEdge
import androidxpose.foundation.ExperimentalFoundationApi
import androidxpose.foundation.background
import androidxpose.foundation.border
import androidxpose.foundation.draganddrop.dragAndDropSource
import androidxpose.foundation.draganddrop.dragAndDropTarget
import androidxpose.foundation.gestures.detectTapGestures
import androidxpose.foundation.layout.Arrangement
import androidxpose.foundation.layout.Column
import androidxpose.foundation.layout.Row
import androidxpose.foundation.layout.Spacer
import androidxpose.foundation.layout.fillMaxHeight
import androidxpose.foundation.layout.fillMaxSize
import androidxpose.foundation.layout.height
import androidxpose.foundation.layout.padding
import androidxpose.foundation.shape.RoundedCornerShape
import androidxpose.material3.Scaffold
import androidxpose.material3.Text
import androidxpose.runtime.Composable
import androidxpose.runtime.key
import androidxpose.runtime.mutableStateListOf
import androidxpose.runtime.remember
import androidxpose.ui.Alignment
import androidxpose.ui.Modifier
import androidxpose.ui.draganddrop.DragAndDropEvent
import androidxpose.ui.draganddrop.DragAndDropTarget
import androidxpose.ui.draganddrop.DragAndDropTransferData
import androidxpose.ui.draganddrop.mimeTypes
import androidxpose.ui.draganddrop.toAndroidDragEvent
import androidxpose.ui.graphics.Color
import androidxpose.ui.input.key.Key
import androidxpose.ui.unit.dp
import androidx.lifecycle.ViewModel
import com.abdo21.mydragapp.ui.theme.MyDragAppTheme
import androidx.lifecycle.viewmodelpose.viewModel
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
App(modifier = Modifier
.padding(innerPadding)
)
}
}
}
}
enum class QueueType(val title: String) {
TodoQueue("To Do"),
InProgressQueue("In Progress"),
DoneQueue("Done")
}
data class Task(
val id: Int,
val title: String,
val description: String
)
class MyViewModel: ViewModel() {
val todoTasks = mutableStateListOf<Task>(
Task(id = 1, title = "Task 1", description = "Description 1"),
Task(id = 2, title = "Task 4", description = "Description 4"),
)
val inProgressTasks = mutableStateListOf<Task>(
Task(id = 3, title = "Task 2", description = "Description 2"),
Task(id = 4, title = "Task 5", description = "Description 5"),
)
val doneTasks = mutableStateListOf<Task>(
Task(id = 5, title = "Task 3", description = "Description 3"),
)
fun moveTask(
task: Task,
from: QueueType,
to: QueueType
) {
when(from) {
QueueType.TodoQueue -> todoTasks.remove(task)
QueueType.InProgressQueue -> inProgressTasks.remove(task)
QueueType.DoneQueue -> doneTasks.remove(task)
}
when(to) {
QueueType.TodoQueue -> todoTasks.add(task)
QueueType.InProgressQueue -> inProgressTasks.add(task)
QueueType.DoneQueue -> doneTasks.add(task)
}
}
}
data class DraggedData(
val task: Task,
val originQueue: QueueType,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TaskCard(
task: Task,
queueType: QueueType,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.dragAndDropSource {
detectTapGestures(
onLongPress = {
startTransfer(
DragAndDropTransferData(
ClipData.newPlainText("Task", "Task"),
localState = DraggedData(task, queueType)
)
)
}
)
}
.background(
color = Color.LightGray,
shape = RoundedCornerShape(8.dp)
)
.padding(vertical = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = task.title)
Text(text = task.description)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun QueueTask(
queueType: QueueType,
tasks: List<Task>,
modifier: Modifier = Modifier,
onTaskDrop: (Task, QueueType) -> Unit
) {
val dropTarget = remember {
object : DragAndDropTarget {
override fun onDrop(dragStartEvent: DragAndDropEvent): Boolean {
val dropEvent = dragStartEvent.toAndroidDragEvent()
val draggedTaskData = dropEvent.localState as? DraggedData
val droppedTask = draggedTaskData?.task
droppedTask?.let { onTaskDrop(it, draggedTaskData.originQueue) }
return true
}
}
}
Column(
modifier = modifier
.dragAndDropTarget (
shouldStartDragAndDrop = { dragStartEvent ->
dragStartEvent.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
}, target = dropTarget
)
.border(
width = 1.dp,
color = Color.Black,
shape = RoundedCornerShape(8.dp)
)
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = queueType.title)
Spacer(modifier = Modifier.height(4.dp))
tasks.forEach { task ->
key(task.id) {
TaskCard(
task = task,
queueType = queueType,
modifier = Modifier
.padding(vertical = 4.dp)
)
}
}
}
}
@Composable
fun App(
modifier: Modifier = Modifier,
viewModel: MyViewModel = viewModel()
) {
val todoTasks = viewModel.todoTasks
val inProgressTasks = viewModel.inProgressTasks
val doneTasks = viewModel.doneTasks
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
QueueTask(
queueType = QueueType.TodoQueue,
tasks = todoTasks,
modifier = Modifier.weight(1f),
onTaskDrop = { task, fromQueueType ->
viewModel.moveTask(task, fromQueueType, QueueType.TodoQueue)
}
)
QueueTask(
queueType = QueueType.InProgressQueue,
tasks = inProgressTasks,
modifier = Modifier.weight(1f),
onTaskDrop = { task, fromQueueType ->
viewModel.moveTask(task, fromQueueType, QueueType.InProgressQueue)
}
)
QueueTask(
queueType = QueueType.DoneQueue,
tasks = doneTasks,
modifier = Modifier.weight(1f),
onTaskDrop = { task, fromQueueType ->
viewModel.moveTask(task, fromQueueType, QueueType.DoneQueue)
}
)
}
}
Demo:
I am developing a test application with Jetpack Compose using drag and drop functionalities. I have 3 tasks that are in each assigned column: To Do, In Progress, and Done. When I try to move any task to another column, it changes but then returns to the initial column. What could be the issue?
Here is my code on GitHub: GitHub Repository
DragAndDrop.kt
internal val LocalDragTargetIfo = compositionLocalOf{ DragTargetInfo() }
@Composable
fun DragableScreen(modifier: Modifier = Modifier,content: @Composable BoxScope.() -> Unit){
val state= remember {
DragTargetInfo()
}
CompositionLocalProvider(LocalDragTargetIfo provides state){
Box(modifier=Modifier.fillMaxSize()){
content()
if(state.isDagging){
var targetSize by remember {
mutableStateOf(IntSize.Zero)
}
Box(
modifier = Modifier
.graphicsLayer {
val offset = (state.dragPosition + state.dragOffset)
scaleX = 1.3f
scaleY = 1.3f
alpha = if (targetSize == IntSize.Zero) 0f else .9f
translationX = offset.x.minus(targetSize.width / 2)
translationY = offset.y.minus(targetSize.height / 2)
}
.onGloballyPositioned {
targetSize = it.size
}
){
state.draggableComposable?.invoke()
}
}
}
}
}
@Composable
fun <T> DragTrarget(
modifier: Modifier = Modifier,
datatoDrop: T,
viewModel: MainViewModel,
content: @Composable () -> Unit
) {
var currentPosition by remember { mutableStateOf(Offset.Zero) }
val currentState = LocalDragTargetIfo.current
Box(
modifier = modifier
.onGloballyPositioned { currentPosition = it.localToWindow(Offset.Zero) }
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
viewModel.startDragging()
currentState.dataToDrop = datatoDrop
currentState.isDagging = true
currentState.dragPosition = currentPosition + it
currentState.draggableComposable = content
},
onDrag = { change, dragAmount ->
change.consume()
currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
},
onDragEnd = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
},
onDragCancel = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
}
)
}
) {
content()
}
}
@Composable
fun <T> DropItem(
modifier: Modifier,
content: @Composable BoxScope.(isInBound: Boolean, data: T?) -> Unit
) {
val dragInfo = LocalDragTargetIfo.current
val dragPosition = dragInfo.dragPosition
val dragOffset = dragInfo.dragOffset
var isCurretDropTarget by remember { mutableStateOf(false) }
Box(
modifier = modifier
.background(Color.Red)
.onGloballyPositioned {
it.boundsInWindow().let { rect ->
isCurretDropTarget = rect.contains(dragPosition + dragOffset)
}
}
) {
val data = if (isCurretDropTarget && !dragInfo.isDagging) {
dragInfo.dataToDrop as T?
} else {
null
}
// Solo ejecuta el contenido si el arrastre ha terminado y está en el área de soltar
if (!dragInfo.isDagging && isCurretDropTarget && data != null) {
content(true, data)
} else {
content(false, null)
}
}
}
internal class DragTargetInfo {
var isDagging:Boolean by mutableStateOf(false)
var dragPosition by mutableStateOf(Offset.Zero)
var dragOffset by mutableStateOf(Offset.Zero)
var draggableComposable by mutableStateOf <(@Composable () -> Unit)?>(null)
var dataToDrop by mutableStateOf<Any?>(null)
}
MainViewModel.kt
class MainViewModel : ViewModel() {
var isCurrentlyDragging by mutableStateOf(false)
private set
val items = mutableStateListOf<Task>()
var addedTasks = mutableStateListOf<Task>()
private set
fun addExampleTasks() {
items.addAll(listOf(
Task(1, "Task 1","Description 1", TaskStatus.TO_DO),
Task(2, "Task 2","Description 2", TaskStatus.IN_PROGRESS),
Task(3, "Task 3","Description 3", TaskStatus.DONE)
))
}
fun startDragging() {
isCurrentlyDragging = true
}
fun stopDragging() {
isCurrentlyDragging = false
}
fun addTask(task: Task) {
items.add(task)
}
fun updateTaskStatus(task: Task, newStatus: TaskStatus) {
val index = items.indexOfFirst { it.id == task.id }
if (index != -1) {
val currentTask = items[index]
if (currentTask.status != newStatus) {
println("Updating task ${task.id} from ${currentTask.status} to $newStatus")
// Actualizar el estado directamente
items[index] = currentTask.copy(status = newStatus)
}
}
}
}
MainScreen.kt
@Composable
fun MainScreen(mainViewModel: MainViewModel = viewModel()) {
// Observar directamente el estado de items
val tasks = mainViewModel.items
// Llamar a un método para añadir tareas de ejemplo (solo si es necesario)
LaunchedEffect(Unit) {
if (tasks.isEmpty()) {
mainViewModel.addExampleTasks()
}
}
Row(modifier = Modifier.fillMaxSize()) {
TaskColumn(
title = "To Do",
tasks = tasks.filter { it.status == TaskStatus.TO_DO },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.TO_DO)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "In Progress",
tasks = tasks.filter { it.status == TaskStatus.IN_PROGRESS },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.IN_PROGRESS)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "Done",
tasks = tasks.filter { it.status == TaskStatus.DONE },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.DONE)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
}
}
@Composable
fun TaskColumn(
title: String,
tasks: List<Task>,
onTaskDropped: (Task) -> Unit,
viewModel: MainViewModel, // Asegúrate de recibir el ViewModel
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxHeight()
.padding(8.dp)
.border(1.dp, Color.Black, shape = RoundedCornerShape(8.dp))
) {
Text(title, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(8.dp))
tasks.forEach { task ->
DragTrarget(
modifier = Modifier.wrapContentWidth().padding(4.dp),
datatoDrop = task,
viewModel = viewModel // Pasar el ViewModel aquí
) {
TaskItem(task)
}
}
DropItem<Task>(
modifier = Modifier.fillMaxSize()
) { isInBound, droppedTask ->
if (isInBound && droppedTask != null) {
println("Task dropped: ${droppedTask.title} to $title")
onTaskDropped(droppedTask)
}
}
}
}
@Composable
fun TaskItem(task: Task) {
Box(
modifier = Modifier
.wrapContentWidth()
.padding(8.dp)
.background(Color.LightGray, shape = RoundedCornerShape(8.dp)) // Aquí se aplica el radio
.border(1.dp, Color.LightGray, shape = RoundedCornerShape(8.dp)) // Asegúrate de aplicar el mismo shape al borde
.padding(8.dp)
) {
Column {
Text("${task.title}:")
Text("${task.description}")
}
}
}
Task.kt
data class Task (val id: Int, val title: String, val description: String, var status: TaskStatus)
enum class TaskStatus{
TO_DO,IN_PROGRESS,DONE
}
I am developing a test application with Jetpack Compose using drag and drop functionalities. I have 3 tasks that are in each assigned column: To Do, In Progress, and Done. When I try to move any task to another column, it changes but then returns to the initial column. What could be the issue?
Here is my code on GitHub: GitHub Repository
DragAndDrop.kt
internal val LocalDragTargetIfo = compositionLocalOf{ DragTargetInfo() }
@Composable
fun DragableScreen(modifier: Modifier = Modifier,content: @Composable BoxScope.() -> Unit){
val state= remember {
DragTargetInfo()
}
CompositionLocalProvider(LocalDragTargetIfo provides state){
Box(modifier=Modifier.fillMaxSize()){
content()
if(state.isDagging){
var targetSize by remember {
mutableStateOf(IntSize.Zero)
}
Box(
modifier = Modifier
.graphicsLayer {
val offset = (state.dragPosition + state.dragOffset)
scaleX = 1.3f
scaleY = 1.3f
alpha = if (targetSize == IntSize.Zero) 0f else .9f
translationX = offset.x.minus(targetSize.width / 2)
translationY = offset.y.minus(targetSize.height / 2)
}
.onGloballyPositioned {
targetSize = it.size
}
){
state.draggableComposable?.invoke()
}
}
}
}
}
@Composable
fun <T> DragTrarget(
modifier: Modifier = Modifier,
datatoDrop: T,
viewModel: MainViewModel,
content: @Composable () -> Unit
) {
var currentPosition by remember { mutableStateOf(Offset.Zero) }
val currentState = LocalDragTargetIfo.current
Box(
modifier = modifier
.onGloballyPositioned { currentPosition = it.localToWindow(Offset.Zero) }
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
viewModel.startDragging()
currentState.dataToDrop = datatoDrop
currentState.isDagging = true
currentState.dragPosition = currentPosition + it
currentState.draggableComposable = content
},
onDrag = { change, dragAmount ->
change.consume()
currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
},
onDragEnd = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
},
onDragCancel = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
}
)
}
) {
content()
}
}
@Composable
fun <T> DropItem(
modifier: Modifier,
content: @Composable BoxScope.(isInBound: Boolean, data: T?) -> Unit
) {
val dragInfo = LocalDragTargetIfo.current
val dragPosition = dragInfo.dragPosition
val dragOffset = dragInfo.dragOffset
var isCurretDropTarget by remember { mutableStateOf(false) }
Box(
modifier = modifier
.background(Color.Red)
.onGloballyPositioned {
it.boundsInWindow().let { rect ->
isCurretDropTarget = rect.contains(dragPosition + dragOffset)
}
}
) {
val data = if (isCurretDropTarget && !dragInfo.isDagging) {
dragInfo.dataToDrop as T?
} else {
null
}
// Solo ejecuta el contenido si el arrastre ha terminado y está en el área de soltar
if (!dragInfo.isDagging && isCurretDropTarget && data != null) {
content(true, data)
} else {
content(false, null)
}
}
}
internal class DragTargetInfo {
var isDagging:Boolean by mutableStateOf(false)
var dragPosition by mutableStateOf(Offset.Zero)
var dragOffset by mutableStateOf(Offset.Zero)
var draggableComposable by mutableStateOf <(@Composable () -> Unit)?>(null)
var dataToDrop by mutableStateOf<Any?>(null)
}
MainViewModel.kt
class MainViewModel : ViewModel() {
var isCurrentlyDragging by mutableStateOf(false)
private set
val items = mutableStateListOf<Task>()
var addedTasks = mutableStateListOf<Task>()
private set
fun addExampleTasks() {
items.addAll(listOf(
Task(1, "Task 1","Description 1", TaskStatus.TO_DO),
Task(2, "Task 2","Description 2", TaskStatus.IN_PROGRESS),
Task(3, "Task 3","Description 3", TaskStatus.DONE)
))
}
fun startDragging() {
isCurrentlyDragging = true
}
fun stopDragging() {
isCurrentlyDragging = false
}
fun addTask(task: Task) {
items.add(task)
}
fun updateTaskStatus(task: Task, newStatus: TaskStatus) {
val index = items.indexOfFirst { it.id == task.id }
if (index != -1) {
val currentTask = items[index]
if (currentTask.status != newStatus) {
println("Updating task ${task.id} from ${currentTask.status} to $newStatus")
// Actualizar el estado directamente
items[index] = currentTask.copy(status = newStatus)
}
}
}
}
MainScreen.kt
@Composable
fun MainScreen(mainViewModel: MainViewModel = viewModel()) {
// Observar directamente el estado de items
val tasks = mainViewModel.items
// Llamar a un método para añadir tareas de ejemplo (solo si es necesario)
LaunchedEffect(Unit) {
if (tasks.isEmpty()) {
mainViewModel.addExampleTasks()
}
}
Row(modifier = Modifier.fillMaxSize()) {
TaskColumn(
title = "To Do",
tasks = tasks.filter { it.status == TaskStatus.TO_DO },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.TO_DO)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "In Progress",
tasks = tasks.filter { it.status == TaskStatus.IN_PROGRESS },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.IN_PROGRESS)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "Done",
tasks = tasks.filter { it.status == TaskStatus.DONE },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.DONE)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
}
}
@Composable
fun TaskColumn(
title: String,
tasks: List<Task>,
onTaskDropped: (Task) -> Unit,
viewModel: MainViewModel, // Asegúrate de recibir el ViewModel
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxHeight()
.padding(8.dp)
.border(1.dp, Color.Black, shape = RoundedCornerShape(8.dp))
) {
Text(title, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(8.dp))
tasks.forEach { task ->
DragTrarget(
modifier = Modifier.wrapContentWidth().padding(4.dp),
datatoDrop = task,
viewModel = viewModel // Pasar el ViewModel aquí
) {
TaskItem(task)
}
}
DropItem<Task>(
modifier = Modifier.fillMaxSize()
) { isInBound, droppedTask ->
if (isInBound && droppedTask != null) {
println("Task dropped: ${droppedTask.title} to $title")
onTaskDropped(droppedTask)
}
}
}
}
@Composable
fun TaskItem(task: Task) {
Box(
modifier = Modifier
.wrapContentWidth()
.padding(8.dp)
.background(Color.LightGray, shape = RoundedCornerShape(8.dp)) // Aquí se aplica el radio
.border(1.dp, Color.LightGray, shape = RoundedCornerShape(8.dp)) // Asegúrate de aplicar el mismo shape al borde
.padding(8.dp)
) {
Column {
Text("${task.title}:")
Text("${task.description}")
}
}
}
Task.kt
data class Task (val id: Int, val title: String, val description: String, var status: TaskStatus)
enum class TaskStatus{
TO_DO,IN_PROGRESS,DONE
}
Share
Improve this question
edited Nov 18, 2024 at 15:07
Jon Amengual
asked Nov 18, 2024 at 12:16
Jon AmengualJon Amengual
153 bronze badges
1
- You can find more details here – Abdo21 Commented Nov 19, 2024 at 19:20
1 Answer
Reset to default 0You don’t need to build drag and drop functionality from scratch in jetpack compose because it supports this natively.
I’ve updated your code to use compose drag and drop features and also made the code a bit simpler and more concise.
Here’s my version:
import android.content.ClipData
import android.content.ClipDescription
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activitypose.setContent
import androidx.activity.enableEdgeToEdge
import androidxpose.foundation.ExperimentalFoundationApi
import androidxpose.foundation.background
import androidxpose.foundation.border
import androidxpose.foundation.draganddrop.dragAndDropSource
import androidxpose.foundation.draganddrop.dragAndDropTarget
import androidxpose.foundation.gestures.detectTapGestures
import androidxpose.foundation.layout.Arrangement
import androidxpose.foundation.layout.Column
import androidxpose.foundation.layout.Row
import androidxpose.foundation.layout.Spacer
import androidxpose.foundation.layout.fillMaxHeight
import androidxpose.foundation.layout.fillMaxSize
import androidxpose.foundation.layout.height
import androidxpose.foundation.layout.padding
import androidxpose.foundation.shape.RoundedCornerShape
import androidxpose.material3.Scaffold
import androidxpose.material3.Text
import androidxpose.runtime.Composable
import androidxpose.runtime.key
import androidxpose.runtime.mutableStateListOf
import androidxpose.runtime.remember
import androidxpose.ui.Alignment
import androidxpose.ui.Modifier
import androidxpose.ui.draganddrop.DragAndDropEvent
import androidxpose.ui.draganddrop.DragAndDropTarget
import androidxpose.ui.draganddrop.DragAndDropTransferData
import androidxpose.ui.draganddrop.mimeTypes
import androidxpose.ui.draganddrop.toAndroidDragEvent
import androidxpose.ui.graphics.Color
import androidxpose.ui.input.key.Key
import androidxpose.ui.unit.dp
import androidx.lifecycle.ViewModel
import com.abdo21.mydragapp.ui.theme.MyDragAppTheme
import androidx.lifecycle.viewmodelpose.viewModel
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
App(modifier = Modifier
.padding(innerPadding)
)
}
}
}
}
enum class QueueType(val title: String) {
TodoQueue("To Do"),
InProgressQueue("In Progress"),
DoneQueue("Done")
}
data class Task(
val id: Int,
val title: String,
val description: String
)
class MyViewModel: ViewModel() {
val todoTasks = mutableStateListOf<Task>(
Task(id = 1, title = "Task 1", description = "Description 1"),
Task(id = 2, title = "Task 4", description = "Description 4"),
)
val inProgressTasks = mutableStateListOf<Task>(
Task(id = 3, title = "Task 2", description = "Description 2"),
Task(id = 4, title = "Task 5", description = "Description 5"),
)
val doneTasks = mutableStateListOf<Task>(
Task(id = 5, title = "Task 3", description = "Description 3"),
)
fun moveTask(
task: Task,
from: QueueType,
to: QueueType
) {
when(from) {
QueueType.TodoQueue -> todoTasks.remove(task)
QueueType.InProgressQueue -> inProgressTasks.remove(task)
QueueType.DoneQueue -> doneTasks.remove(task)
}
when(to) {
QueueType.TodoQueue -> todoTasks.add(task)
QueueType.InProgressQueue -> inProgressTasks.add(task)
QueueType.DoneQueue -> doneTasks.add(task)
}
}
}
data class DraggedData(
val task: Task,
val originQueue: QueueType,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TaskCard(
task: Task,
queueType: QueueType,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.dragAndDropSource {
detectTapGestures(
onLongPress = {
startTransfer(
DragAndDropTransferData(
ClipData.newPlainText("Task", "Task"),
localState = DraggedData(task, queueType)
)
)
}
)
}
.background(
color = Color.LightGray,
shape = RoundedCornerShape(8.dp)
)
.padding(vertical = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = task.title)
Text(text = task.description)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun QueueTask(
queueType: QueueType,
tasks: List<Task>,
modifier: Modifier = Modifier,
onTaskDrop: (Task, QueueType) -> Unit
) {
val dropTarget = remember {
object : DragAndDropTarget {
override fun onDrop(dragStartEvent: DragAndDropEvent): Boolean {
val dropEvent = dragStartEvent.toAndroidDragEvent()
val draggedTaskData = dropEvent.localState as? DraggedData
val droppedTask = draggedTaskData?.task
droppedTask?.let { onTaskDrop(it, draggedTaskData.originQueue) }
return true
}
}
}
Column(
modifier = modifier
.dragAndDropTarget (
shouldStartDragAndDrop = { dragStartEvent ->
dragStartEvent.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
}, target = dropTarget
)
.border(
width = 1.dp,
color = Color.Black,
shape = RoundedCornerShape(8.dp)
)
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = queueType.title)
Spacer(modifier = Modifier.height(4.dp))
tasks.forEach { task ->
key(task.id) {
TaskCard(
task = task,
queueType = queueType,
modifier = Modifier
.padding(vertical = 4.dp)
)
}
}
}
}
@Composable
fun App(
modifier: Modifier = Modifier,
viewModel: MyViewModel = viewModel()
) {
val todoTasks = viewModel.todoTasks
val inProgressTasks = viewModel.inProgressTasks
val doneTasks = viewModel.doneTasks
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
QueueTask(
queueType = QueueType.TodoQueue,
tasks = todoTasks,
modifier = Modifier.weight(1f),
onTaskDrop = { task, fromQueueType ->
viewModel.moveTask(task, fromQueueType, QueueType.TodoQueue)
}
)
QueueTask(
queueType = QueueType.InProgressQueue,
tasks = inProgressTasks,
modifier = Modifier.weight(1f),
onTaskDrop = { task, fromQueueType ->
viewModel.moveTask(task, fromQueueType, QueueType.InProgressQueue)
}
)
QueueTask(
queueType = QueueType.DoneQueue,
tasks = doneTasks,
modifier = Modifier.weight(1f),
onTaskDrop = { task, fromQueueType ->
viewModel.moveTask(task, fromQueueType, QueueType.DoneQueue)
}
)
}
}
Demo:
本文标签: androidJetpack Compose Drag and Drop Tasks Return to Original ColumnHow to FixStack Overflow
版权声明:本文标题:android - Jetpack Compose: Drag and Drop Tasks Return to Original Column - How to Fix? - Stack Overflow 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://it.en369.cn/questions/1745618869a2159454.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论