feat: 支持多选和删除待办(后续优化逻辑)

This commit is contained in:
Super12138 2025-01-15 15:36:14 +08:00
parent 1656063449
commit 366f19ca06
11 changed files with 256 additions and 41 deletions

View file

@ -19,6 +19,14 @@ object Repository {
}
suspend fun deleteTodo(toDo: TodoEntity) {
toDoDao.delete(toDo.id)
toDoDao.delete(toDo)
}
suspend fun deleteTodoFromIds(toDoItems: List<Int>) {
toDoDao.deleteFromIds(toDoItems.toSet())
}
/*suspend fun deleteAllTodo() {
toDoDao.deleteAllTodo()
}*/
}

View file

@ -1,6 +1,7 @@
package cn.super12138.todo.logic.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@ -18,6 +19,12 @@ interface TodoDao {
@Update
suspend fun update(toDo: TodoEntity)
@Query("DELETE FROM todo WHERE id = :toDoId")
suspend fun delete(toDoId: Int)
@Delete
suspend fun delete(toDo: TodoEntity)
@Query("DELETE FROM todo WHERE id in (:toDoIds)")
suspend fun deleteFromIds(toDoIds: Set<Int>)
/*@Query("DELETE FROM todo")
suspend fun deleteAllTodo()*/
}

View file

@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cn.super12138.todo.ui.TodoDefaults
/**
* 带动画且比 Material 3 内置组件动画好看的的可扩展 FAB
@ -36,7 +35,7 @@ fun AnimatedExtendedFloatingActionButton(
modifier = modifier
) {
Row(
modifier = Modifier.padding(horizontal = TodoDefaults.screenPadding),
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
icon()

View file

@ -1,18 +1,31 @@
package cn.super12138.todo.ui.pages.main
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.expandIn
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@ -44,36 +57,100 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
}
}
val selectedEditTodoItem = viewModel.selectedEditTodoItem
val selectedEditTodoItem = viewModel.selectedEditTodo
val selectedTodoIds = viewModel.selectedTodoIds.collectAsState()
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val isSelectedIdsEmpty by remember {
derivedStateOf {
selectedTodoIds.value.isEmpty()
}
}
val animatedTopAppBarColors by animateColorAsState(
targetValue = if (isSelectedIdsEmpty) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.surfaceContainerHighest
)
BackHandler(enabled = !isSelectedIdsEmpty) {
// 当按下返回键(或进行返回操作)时清空选择
viewModel.clearAllTodoSelection()
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
AnimatedVisibility(!isSelectedIdsEmpty) {
IconButton(onClick = { viewModel.clearAllTodoSelection() }) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.tip_clear_selected_items)
)
}
}
},
title = {
Text(stringResource(R.string.app_name))
Text(
text = if (isSelectedIdsEmpty) {
stringResource(R.string.app_name)
} else {
stringResource(
R.string.title_selected_count,
selectedTodoIds.value.size
)
}
)
},
actions = {
IconButton(
onClick = {}
) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(R.string.page_settings)
)
AnimatedContent(isSelectedIdsEmpty) { isEmpty ->
if (isEmpty) {
IconButton(
onClick = {}
) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(R.string.page_settings)
)
}
} else {
Row {
IconButton(onClick = { viewModel.toggleAllSelected() }) {
Icon(
imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.tip_select_all)
)
}
IconButton(onClick = { viewModel.deleteSelectedTodo() }) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.action_delete)
)
}
}
}
}
}
},
colors = TopAppBarDefaults.largeTopAppBarColors().copy(
containerColor = animatedTopAppBarColors
)
)
},
floatingActionButton = {
TodoFAB(
expanded = isExpanded,
onClick = { openBottomSheet = true }
)
AnimatedVisibility(
visible = isSelectedIdsEmpty,
enter = fadeIn() + expandIn(),
exit = shrinkOut() + fadeOut()
) {
TodoFAB(
expanded = isExpanded,
onClick = { openBottomSheet = true }
)
}
},
modifier = modifier
) { innerPadding ->
@ -91,8 +168,15 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
state = listState,
list = toDoList.value.filter { item -> !item.isCompleted },
onItemClick = { item ->
openBottomSheet = true
viewModel.setEditTodoItem(item)
if (isSelectedIdsEmpty) {
openBottomSheet = true
viewModel.setEditTodoItem(item)
} else {
viewModel.toggleTodoSelection(item)
}
},
onItemLongClick = { item ->
viewModel.toggleTodoSelection(item)
},
onItemChecked = { item ->
item.apply {
@ -107,6 +191,7 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
viewModel.playConfetti()
}
},
selectedTodoIds = selectedTodoIds.value,
modifier = Modifier
.weight(3f)
.fillMaxSize()
@ -126,8 +211,15 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
state = listState,
list = toDoList.value.filter { item -> !item.isCompleted },
onItemClick = { item ->
openBottomSheet = true
viewModel.setEditTodoItem(item)
if (isSelectedIdsEmpty) {
openBottomSheet = true
viewModel.setEditTodoItem(item)
} else {
viewModel.toggleTodoSelection(item)
}
},
onItemLongClick = { item ->
viewModel.toggleTodoSelection(item)
},
onItemChecked = { item ->
item.apply {
@ -142,6 +234,7 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
viewModel.playConfetti()
}
},
selectedTodoIds = selectedTodoIds.value,
modifier = Modifier
.weight(3f)
.fillMaxSize()

View file

@ -29,7 +29,9 @@ fun ManagerFragment(
state: LazyListState,
list: List<TodoEntity>,
onItemClick: (TodoEntity) -> Unit = {},
onItemLongClick: (TodoEntity) -> Unit = {},
onItemChecked: (TodoEntity) -> Unit = {},
selectedTodoIds: List<Int>,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
@ -67,10 +69,12 @@ fun ManagerFragment(
content = item.content,
subject = Subjects.fromId(item.subject).getDisplayName(context),
onCardClick = { onItemClick(item) },
onCardLongClick = { onItemLongClick(item) },
onChecked = { onItemChecked(item) },
selected = selectedTodoIds.contains(item.id),
modifier = Modifier
.padding(vertical = 5.dp)
.animateItem()
.animateItem() // TODO: 设置动画时间
)
}
}

View file

@ -1,13 +1,19 @@
package cn.super12138.todo.ui.pages.main.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.ElevatedCard
@ -18,31 +24,53 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TodoCard(
content: String,
subject: String,
onCardClick: () -> Unit = {},
onCardLongClick: () -> Unit = {},
onChecked: () -> Unit = {},
selected: Boolean,
modifier: Modifier = Modifier
) {
ElevatedCard(
modifier = modifier
.fillMaxWidth()
.height(80.dp),
onClick = onCardClick
.height(80.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.combinedClickable(
onClick = onCardClick,
onLongClick = onCardLongClick
)
.padding(horizontal = 15.dp)
) {
AnimatedVisibility(selected) {
Box(
Modifier
.padding(end = 15.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.onSecondaryContainer)
.padding(5.dp)
) {
Icon(
imageVector = Icons.Outlined.Check,
tint = MaterialTheme.colorScheme.onSecondary,
contentDescription = stringResource(R.string.tip_select_this)
)
}
}
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
@ -63,12 +91,14 @@ fun TodoCard(
)
}
IconButton(onClick = onChecked) {
Icon(
imageVector = Icons.Outlined.Check,
tint = MaterialTheme.colorScheme.primary,
contentDescription = stringResource(R.string.tip_mark_completed)
)
AnimatedVisibility(!selected) {
IconButton(onClick = onChecked) {
Icon(
imageVector = Icons.Outlined.Check,
tint = MaterialTheme.colorScheme.primary,
contentDescription = stringResource(R.string.tip_mark_completed)
)
}
}
/*Box(

View file

@ -8,14 +8,22 @@ import androidx.lifecycle.viewModelScope
import cn.super12138.todo.logic.Repository
import cn.super12138.todo.logic.database.TodoEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class MainViewModel : ViewModel() {
val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()
var selectedEditTodoItem by mutableStateOf<TodoEntity?>(null)
var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
private set
val showConfetti = mutableStateOf(false)
// 多选逻辑参考https://github.com/X1nto/Mauth
private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
val selectedTodoIds = _selectedTodoIds.asStateFlow()
fun addTodo(toDo: TodoEntity) {
viewModelScope.launch {
Repository.insertTodo(toDo)
@ -28,17 +36,75 @@ class MainViewModel : ViewModel() {
}
}
fun playConfetti() {
showConfetti.value = true
}
fun setEditTodoItem(toDo: TodoEntity?) {
selectedEditTodoItem = toDo
}
fun deleteTodo(toDo: TodoEntity) {
viewModelScope.launch {
Repository.deleteTodo(toDo)
}
}
/*fun deleteAllTodo() {
viewModelScope.launch {
Repository.deleteAllTodo()
}
}*/
fun setEditTodoItem(toDo: TodoEntity?) {
selectedEditTodo = toDo
}
/**
* 切换待办的选择状态
*/
fun toggleTodoSelection(toDo: TodoEntity) {
_selectedTodoIds.update { idList ->
if (idList.contains(toDo.id)) {
// 若已经选择取消选择
idList - toDo.id
} else {
// 若未选择添加到列表中,立即选中
idList + toDo.id
}
}
}
/**
* 切换是否全选
*/
fun toggleAllSelected() {
viewModelScope.launch {
toDos.firstOrNull()?.let { todos ->
val allIds = todos.map { it.id }
_selectedTodoIds.update { currentSelectedIds ->
if (currentSelectedIds.containsAll(allIds)) {
// 如果当前是全选状态,取消所有选择(切换为全不选)
emptyList()
} else {
// 如果当前不是全选状态,选中所有项
allIds
}
}
}
}
}
/**
* 清除全部已选择的待办
*/
fun clearAllTodoSelection() {
_selectedTodoIds.update { emptyList() }
}
/**
* 删除选择的待办
*/
fun deleteSelectedTodo() {
viewModelScope.launch {
Repository.deleteTodoFromIds(selectedTodoIds.value)
clearAllTodoSelection()
}
}
fun playConfetti() {
showConfetti.value = true
}
}

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.ToDo" parent="android:Theme.Material.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>

View file

@ -26,4 +26,7 @@
<string name="tip_no_crash_logs">没有日志传入</string>
<string name="action_exit_app">退出应用</string>
<string name="action_delete">删除</string>
<string name="tip_clear_selected_items">清除已选择的项目</string>
<string name="tip_select_all">全选</string>
<string name="title_selected_count">已选择 %s 项</string>
</resources>

View file

@ -25,4 +25,7 @@
<string name="tip_no_crash_logs">No crash logs</string>
<string name="action_exit_app">Exit app</string>
<string name="action_delete">Delete</string>
<string name="tip_clear_selected_items">Clear selected items</string>
<string name="tip_select_all">Select all</string>
<string name="title_selected_count">%s selected</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.ToDo" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>