如何在 Jetpack Compose 中拖动和重新排序列表?

问题描述 投票:0回答:1

有人可以帮我纠正重新排序列表的逻辑吗?我相信 viewmodel 中的 moveTodo 功能无法正常工作。重新排序是有效的,但不像应有的那样。 项目结构 ..db ....ToDoDAO ....ToDoDB ..拖 ....拖放列表 ....DragDropListExtensions ....DragDropListState ..视图模型 ..列表页

道 `接口 ToDoDAO {

@Query("SELECT * FROM todolist")
fun getAllData() : Flow<List<toDoList>>

@Query("SELECT * FROM toDoList WHERE id = :id LIMIT 1")
suspend fun getDataById(id: Int): toDoList?

@Insert
suspend fun insertData(toDoList: toDoList)

@Query("DELETE FROM todolist WHERE id = :id")
suspend fun deleteData(id : Int)

@Update
suspend fun updateData(todo: toDoList)

@Query("SELECT * FROM todoList")
fun getAllDataSync(): List<toDoList>

@Query("SELECT MAX(position) FROM todoList")
suspend fun getMaxPosition(): Int?

}`

数据库和实体 `@Database(实体 = [toDoList::class],版本 = 2) @TypeConverters(typeConverters::class) 抽象类 ToDoDB : RoomDatabase() {

companion object{
    const val Name = "Todo_Database"
}

abstract fun getDao(): ToDoDAO

}

@实体 数据类 toDoList( @PrimaryKey(自动生成 = true) 变量 ID:整数 = 0, var 日期:日期, var 标题:字符串, 变量位置:Int ) `

拖放列表


@OptIn(ExperimentalFoundationApi::class)
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun DragDropList(
    items: List<toDoList>, // Changed to List<toDoList> since LazyColumn expects a list
    onMove: (Int, Int) -> Unit,
    viewModel: TodoViewModel,
    // onDelete: (Int) -> Unit, // Added item id to onDelete for better handling
    modifier: Modifier = Modifier
) {
    val scope = rememberCoroutineScope()
    var overScrollJob by remember { mutableStateOf<Job?>(null) }
    val dragDropListState = rememberDragDropListState(onMove = onMove)

    var showDialog by remember { mutableStateOf(false) }
    var currentItem by remember { mutableStateOf<toDoList?>(null) } // Track current item for editing
    var textInput by remember { mutableStateOf(TextFieldValue("")) }
    val context = LocalContext.current

    LazyColumn(
        modifier = modifier
            .pointerInput(Unit) {
                detectDragGesturesAfterLongPress(
                    onDrag = { change, offset ->
                        change.consume()
                        dragDropListState.onDrag(offset = offset)


                        if (overScrollJob?.isActive == true)
                            return@detectDragGesturesAfterLongPress

                        dragDropListState
                            .checkForScroll()
                            .takeIf { it != 0f }
                            ?.let {
                                overScrollJob = scope.launch {
                                    dragDropListState.lazyListState.scrollBy(it)
                                }
                            } ?: kotlin.run { overScrollJob?.cancel() }

                    },
                    onDragStart = {
                        dragDropListState.onDragStart(it)
                    },
                    onDragEnd = {
                        Log.d("DragDropList", "ended")
                        dragDropListState.onDraginterrupted() },
                    onDragCancel = { dragDropListState.onDraginterrupted() }
                )
            }
            .fillMaxSize()
            .padding(all = 10.dp),
        state = dragDropListState.lazyListState
    ) {
        itemsIndexed(items) { index: Int, item: toDoList ->
            Row(
                modifier = Modifier
                    .composed {
                        val offsetOrNull = dragDropListState.elementDisplacement.takeIf {
                            index == dragDropListState.currentIndexOfDraggedItem
                        }
                        Modifier.graphicsLayer {
                            translationY = offsetOrNull ?: 0f
                        }
                    }
                    .fillMaxWidth()
                    .padding(8.dp)
                    .clip(RoundedCornerShape(16.dp))
                    .background(MaterialTheme.colorScheme.primary)
                    .animateItemPlacement()
                    .padding(16.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column(
                    modifier = Modifier.weight(1f)
                ) {
                    Text(
                        text = SimpleDateFormat("EEE, MMM d", Locale.ENGLISH).format(item.date),
                        fontSize = 12.sp,
                        color = Color.LightGray
                    )
                    Text(
                        text = SimpleDateFormat("HH:mm:aa", Locale.ENGLISH).format(item.date),
                        fontSize = 12.sp,
                        color = Color.LightGray
                    )
                    Text(
                        text = item.title,
                        fontSize = 20.sp,
                        color = Color.White
                    )
                }

                IconButton(onClick = {
                    currentItem = item
                    textInput = TextFieldValue(item.title)
                    showDialog = true
                }) {
                    Icon(
                        painter = painterResource(id = R.drawable.baseline_edit_24),
                        contentDescription = "Edit",
                        tint = Color.White
                    )
                }

                IconButton(onClick = { viewModel.deleteTodo(item.id) }) {
                    Icon(
                        painter = painterResource(id = R.drawable.baseline_delete_24),
                        contentDescription = "Delete",
                        tint = Color.White
                    )
                }
            }
        }
    }

    // Dialog for editing task
    if (showDialog && currentItem != null) {
        AlertDialog(
            onDismissRequest = { showDialog = false },
            title = { Text(text = "Edit Task") },
            text = {
                // Text input field inside the dialog
                OutlinedTextField(
                    value = textInput,
                    onValueChange = { newValue -> textInput = newValue },
                    placeholder = { Text("Enter Text") },
                    label = { Text("Enter Text") },
                    modifier = Modifier.fillMaxWidth() // Make the text field fill the dialog width
                )
            },
            confirmButton = {
                TextButton(
                    onClick = {
                        // Action on Confirm button
                        // Handle the input data (e.g., save or process the text)
                        if (textInput.text.isNotEmpty()) {
                            currentItem?.let {
                                viewModel.updateTodo(it.id, textInput.text)
                            }
                            showDialog = false
                        } else {
                            Toast.makeText(context, "Please enter some text", Toast.LENGTH_SHORT).show()
                        }
                    }
                ) {
                    Text("Confirm")
                }
            },
            dismissButton = {
                TextButton(
                    onClick = { showDialog = false }
                ) {
                    Text("Dismiss")
                }
            }
        )
    }
}

拖放列表扩展

fun LazyListState.getVisibleItemInfoFor(absoluteIndex: Int): LazyListItemInfo? {
    return this.layoutInfo.visibleItemsInfo.getOrNull(absoluteIndex - this.layoutInfo.visibleItemsInfo.first().index)
}

val LazyListItemInfo.offsetEnd: Int
    get() = this.offset + this.size

fun <T> MutableList<T>.move(from: Int, to: Int) {
    if (from == to) return
    val element = this.removeAt(from) ?: return
    this.add(to, element)

}

拖放列表状态

@Composable
fun rememberDragDropListState(
    lazyListState: LazyListState = rememberLazyListState(),
    onMove: (Int, Int) -> Unit
): DragDropListState {
    return remember(lazyListState) {
        DragDropListState(lazyListState, onMove)
    }
}

class DragDropListState(
    val lazyListState: LazyListState,
    private var onMove: (Int, Int) -> Unit
){
    var draggedDistance by mutableStateOf(0f)
    var initiallyDraggedElement by mutableStateOf<LazyListItemInfo?>(null)
    var currentIndexOfDraggedItem by mutableStateOf<Int?>(null)
    val initialOffsets: Pair<Int, Int>?
        get() = initiallyDraggedElement?.let {
            Pair(it.offset, it.offsetEnd)
        }

    val elementDisplacement: Float?
        get() = currentIndexOfDraggedItem
            ?.let { draggedIndex ->
                // Find the visible item corresponding to the dragged index
                lazyListState.getVisibleItemInfoFor(absoluteIndex = draggedIndex)
//                lazyListState.layoutInfo.visibleItemsInfo.find { it.index == draggedIndex }
            }
            ?.let { visibleItem -> (initiallyDraggedElement?.offset ?: 0f).toFloat() + draggedDistance - visibleItem.offset
//                // Calculate the displacement based on the initial offset and dragged distance
//                val initialOffset = (initiallyDraggedElement?.offset ?: 0f).toFloat()
//                val currentOffset = visibleItem.offset.toFloat()
//
//                // Return the displacement as the difference between initial and current offsets
//                initialOffset + draggedDistance - currentOffset
            }

    val currentElement: LazyListItemInfo?
        get() = currentIndexOfDraggedItem?.let { draggedIndex ->
            // Find the visible item corresponding to the dragged index
            lazyListState.getVisibleItemInfoFor(absoluteIndex = draggedIndex)
            //lazyListState.layoutInfo.visibleItemsInfo.find { it.index == draggedIndex }
        }

    var overScrollJob by mutableStateOf<Job?>(null)

    fun onDragStart(offset: Offset) {

        lazyListState.layoutInfo.visibleItemsInfo
            .firstOrNull { item ->
                // Convert y offset to an Int and check if it is within the bounds of this item's vertical position
                offset.y.toInt() in item.offset..(item.offset + item.size)
            }?.also {
                currentIndexOfDraggedItem = it.index
                initiallyDraggedElement = it
            }
    }

    fun onDraginterrupted(){
        draggedDistance = 0f
        currentIndexOfDraggedItem = null
        initiallyDraggedElement = null
        overScrollJob?.cancel()
    }

    fun onDrag(offset: Offset) {
        draggedDistance += offset.y

        initialOffsets?.let { (topOffset, bottomOffset) ->
            val startOffset = topOffset + draggedDistance
            val endOffset = bottomOffset + draggedDistance

            currentElement?.let { hoveredElement ->
                lazyListState.layoutInfo.visibleItemsInfo
                    .filterNot { item ->
                        item.offsetEnd < startOffset || item.offset > endOffset || hoveredElement.index == item.index
                    }
                    .firstOrNull { item ->
                        val delta = startOffset - hoveredElement.offset
                        when {
                            delta > 0 -> (endOffset > item.offsetEnd)
                            else -> (startOffset < item.offset)
                        }
                    }?.also { item ->
                        currentIndexOfDraggedItem?.let { current ->
                            onMove.invoke(current, item.index)
                        }
                        currentIndexOfDraggedItem = item.index
                    }
            }
        }
    }

    fun checkForScroll(): Float{
        return initiallyDraggedElement?.let {
            val startOffset = it.offset + draggedDistance
            val endOffset = it.offsetEnd + draggedDistance

            return@let when {
                draggedDistance > 0 -> (endOffset - lazyListState.layoutInfo.viewportEndOffset).takeIf {  diff ->
                    diff > 0
                }
                draggedDistance > 0 -> (endOffset - lazyListState.layoutInfo.viewportEndOffset).takeIf { diff ->
                    diff < 0
                }
                else -> null
            }
        } ?: 0f
    }



}

视图模型

class TodoViewModel : ViewModel() {

    val todoDao = MainApp.todoDatabase.getDao()


    // Use MutableStateFlow for handling the list state
    private val _todoList = MutableStateFlow<List<toDoList>>(emptyList())
    val todoList: StateFlow<List<toDoList>> = _todoList.asStateFlow()

    // Use StateFlow to expose Flow as state-driven data to the UI
    init {
        // Collect initial data from the database
        viewModelScope.launch(Dispatchers.IO) {
            _todoList.value = todoDao.getAllDataSync()
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    fun addTodo(title : String){
        // Creating separate thread for DB operation
        viewModelScope.launch(Dispatchers.IO) {

            val maxPosition = todoDao.getMaxPosition() ?: -1

            todoDao.insertData(toDoList( title = title, date = Date.from(Instant.now()), position = maxPosition + 1 ) )
            _todoList.value = todoDao.getAllDataSync()
        }
    }

    fun deleteTodo(id : Int){
        viewModelScope.launch(Dispatchers.IO) {
            todoDao.deleteData(id)
            _todoList.value = todoDao.getAllDataSync()
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    fun updateTodo(id: Int, newTitle: String) {
        viewModelScope.launch(Dispatchers.IO) {
            val todo = todoDao.getDataById(id)
            if (todo != null) {
                val updatedTodo = todo.copy(title = newTitle, date = Date.from(Instant.now()))
                todoDao.updateData(updatedTodo)

                // Instead of fetching all todos again, update the specific item in the current state
                _todoList.value = _todoList.value.map {
                    if (it.id == id) updatedTodo else it
                }
            }
        }


    }

    fun moveTodo(fromIndex: Int, toIndex: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            // Fetch the current list of todos
            val todos = todoDao.getAllDataSync()

            // Ensure that both indices are within the valid range
            if (fromIndex in todos.indices && toIndex in todos.indices) {
                // Create a mutable copy of the current list
                val reorderedList = todos.toMutableList()

                // Hold the item to be moved
                val movedTodo = reorderedList[fromIndex]

                // Remove the item from its original position
                reorderedList.removeAt(fromIndex)

                // Insert it at the new position
                reorderedList.add(toIndex, movedTodo)

                // Update the positions for all items in the reordered list
                // Ensure to update all items' positions based on their new indices
                for (index in reorderedList.indices) {
                    // Create a new todo with the updated position
                    val updatedTodo = reorderedList[index].copy(position = index)

                    // Update the database with the new position
                    if (updatedTodo.position != todos[index].position) {
                        todoDao.updateData(updatedTodo)
                    }
                }

                // Update the state with the new order
                _todoList.value = reorderedList
            } else {
                // Log an error if indices are out of range
                Log.e("TodoViewModel", "Invalid indices: fromIndex = $fromIndex, toIndex = $toIndex")
            }
        }
    }

}

ListsPage(实际 UI)

@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun TodoListPage(viewModel: TodoViewModel){

    val todoList by viewModel.todoList.collectAsState()
    var inputText by remember {
        mutableStateOf("")
    }
    val context = LocalContext.current


    Scaffold(

        topBar = {
            TopAppBar(
                title = { Text("To-Do List", color = Color.White) },
                colors = TopAppBarDefaults.smallTopAppBarColors(
                    containerColor = Purple40
                )
            )
        },


        content ={ it ->

                Column(
                    modifier = Modifier
                        .fillMaxHeight()
                        .padding(it)
                ) {

                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(8.dp),
                        horizontalArrangement = Arrangement.SpaceBetween
                    ) {
                        OutlinedTextField(
                            modifier= Modifier.weight(2f),
                            value = inputText,
                            onValueChange = {
                                inputText = it
                            },
                            shape = RoundedCornerShape(20.dp),
                            )
                        Button(modifier = Modifier.padding(8.dp),
                            onClick = {
                            if(inputText.isNotEmpty()){
                                viewModel.addTodo(inputText)
                                inputText = ""
                            }else{
                                Toast.makeText(context, "Please enter some text", Toast.LENGTH_SHORT).show()
                            }
                        }

                        ) {
                            Text(text = "Add", color = Color.White)
                        }
                    }

                    DragDropList(items = todoList, onMove = { fromIndex, toIndex -> viewModel.moveTodo(fromIndex, toIndex)} , viewModel = viewModel)


                }

        }
    )
}

我希望重新排序能够正常进行。所有更改都正确反映在后端,但不会以这种方式显示在 UI 中。

android-listview android-jetpack-compose android-jetpack android-mvvm android-debug
1个回答
0
投票

每次更新时都必须根据位置对列表进行排序。更新后的 moveTodo 功能是

fun moveTodo(fromIndex: Int, toIndex: Int) {
viewModelScope.launch(Dispatchers.IO) {
    val todos = todoDao.getAllDataSync()

    if (fromIndex in todos.indices && toIndex in todos.indices) {
        val re = todos.toMutableList()
        val reorderedList = re.sortedBy { it.position }.toMutableList()

        val fromTodo = reorderedList[fromIndex]
        val toTodo = reorderedList[toIndex]

        val tempPosition = fromTodo.position
        fromTodo.position = toTodo.position
        toTodo.position = tempPosition

        todoDao.updateData(fromTodo)
        todoDao.updateData(toTodo)

        reorderedList[fromIndex] = fromTodo
        reorderedList[toIndex] = toTodo

        _todoList.value = reorderedList

        withContext(Dispatchers.Main) {
            Log.d("TodoViewModel", "Updated todoList: ${todos} ")
        }
    } else {
        Log.e("TodoViewModel", "Invalid indices: fromIndex = $fromIndex, toIndex = $toIndex")
    }
}

}

© www.soinside.com 2019 - 2024. All rights reserved.