有人可以帮我纠正重新排序列表的逻辑吗?我相信 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 中。
每次更新时都必须根据位置对列表进行排序。更新后的 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")
}
}
}