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
Add a comment  | 

1 Answer 1

Reset to default 0

You 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
Add a comment  | 

1 Answer 1

Reset to default 0

You 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