feat: 搜索待办
This commit is contained in:
parent
e5601df0b5
commit
87cccfefc1
7 changed files with 292 additions and 90 deletions
|
|
@ -38,7 +38,7 @@ android {
|
||||||
applicationId = "cn.super12138.todo"
|
applicationId = "cn.super12138.todo"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 989
|
versionCode = 990
|
||||||
versionName = "2.3.3"
|
versionName = "2.3.3"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
package cn.super12138.todo.ui.pages.tasks
|
package cn.super12138.todo.ui.pages.tasks
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -11,11 +17,13 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.animateFloatingActionButton
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -31,15 +39,16 @@ import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
import cn.super12138.todo.R
|
import cn.super12138.todo.R
|
||||||
import cn.super12138.todo.constants.Constants
|
import cn.super12138.todo.constants.Constants
|
||||||
import cn.super12138.todo.logic.database.TodoEntity
|
import cn.super12138.todo.logic.database.TodoEntity
|
||||||
import cn.super12138.todo.logic.datastore.DataStoreManager
|
|
||||||
import cn.super12138.todo.logic.model.Priority
|
import cn.super12138.todo.logic.model.Priority
|
||||||
import cn.super12138.todo.ui.TodoDefaults
|
import cn.super12138.todo.ui.TodoDefaults
|
||||||
import cn.super12138.todo.ui.components.ConfirmDialog
|
import cn.super12138.todo.ui.components.ConfirmDialog
|
||||||
import cn.super12138.todo.ui.components.TodoFloatingActionButton
|
import cn.super12138.todo.ui.components.TodoFloatingActionButton
|
||||||
import cn.super12138.todo.ui.components.TopAppBarScaffold
|
import cn.super12138.todo.ui.components.TopAppBarScaffold
|
||||||
import cn.super12138.todo.ui.pages.tasks.components.TodoCard
|
import cn.super12138.todo.ui.pages.tasks.components.TodoCard
|
||||||
|
import cn.super12138.todo.ui.pages.tasks.components.TodoSearchTextField
|
||||||
import cn.super12138.todo.ui.pages.tasks.components.TodoTopAppBar
|
import cn.super12138.todo.ui.pages.tasks.components.TodoTopAppBar
|
||||||
import cn.super12138.todo.ui.viewmodels.MainViewModel
|
import cn.super12138.todo.ui.viewmodels.MainViewModel
|
||||||
|
import cn.super12138.todo.utils.toLocalDateString
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -51,22 +60,35 @@ fun SharedTransitionScope.TasksPage(
|
||||||
) {
|
) {
|
||||||
val animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
val animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
|
||||||
|
val toDoList by viewModel.sortedTodos.collectAsState(initial = emptyList())
|
||||||
val selectedTodos = viewModel.selectedTodoIds.collectAsState()
|
val selectedTodos = viewModel.selectedTodoIds.collectAsState()
|
||||||
val showCompleted by DataStoreManager.showCompletedFlow.collectAsState(initial = Constants.PREF_SHOW_COMPLETED_DEFAULT)
|
|
||||||
|
// 状态持久化
|
||||||
|
val searchFieldState = rememberTextFieldState()
|
||||||
|
val searchMode = rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
var showDeleteConfirmDialog by rememberSaveable { mutableStateOf(false) }
|
var showDeleteConfirmDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val selectedTodoIds by remember { derivedStateOf { selectedTodos.value } }
|
val selectedTodoIds by remember { derivedStateOf { selectedTodos.value } }
|
||||||
val inSelectedMode by remember { derivedStateOf { !selectedTodoIds.isEmpty() } }
|
val inSelectedMode by remember { derivedStateOf { !selectedTodoIds.isEmpty() } }
|
||||||
val toDoList by viewModel.sortedTodos.collectAsState(initial = emptyList())
|
|
||||||
val filteredTodoList =
|
|
||||||
if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted }
|
|
||||||
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
|
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
|
||||||
|
|
||||||
|
val filteredTodoList = if (searchFieldState.text.isBlank()) toDoList else toDoList.filter {
|
||||||
|
it.content.contains(searchFieldState.text, ignoreCase = true) ||
|
||||||
|
it.category.contains(searchFieldState.text, ignoreCase = true) ||
|
||||||
|
it.dueDate?.toLocalDateString()
|
||||||
|
?.contains(searchFieldState.text, ignoreCase = true) == true
|
||||||
|
}
|
||||||
|
// TODO: 移除已完成任务的过滤及其设置项
|
||||||
|
// if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted }
|
||||||
|
|
||||||
// 当按下返回键(或进行返回操作)时清空选择,仅在非选择模式下生效
|
// 当按下返回键(或进行返回操作)时清空选择,仅在非选择模式下生效
|
||||||
BackHandler(inSelectedMode) { viewModel.clearAllTodoSelection() }
|
BackHandler(inSelectedMode) { viewModel.clearAllTodoSelection() }
|
||||||
|
|
||||||
|
// 选择时自动退出搜索模式
|
||||||
|
LaunchedEffect(inSelectedMode) { searchMode.value = false }
|
||||||
|
|
||||||
TopAppBarScaffold(
|
TopAppBarScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TodoTopAppBar(
|
TodoTopAppBar(
|
||||||
|
|
@ -74,7 +96,9 @@ fun SharedTransitionScope.TasksPage(
|
||||||
selectedMode = inSelectedMode,
|
selectedMode = inSelectedMode,
|
||||||
onCancelSelect = { viewModel.clearAllTodoSelection() },
|
onCancelSelect = { viewModel.clearAllTodoSelection() },
|
||||||
onSelectAll = { viewModel.selectAllTodos() },
|
onSelectAll = { viewModel.selectAllTodos() },
|
||||||
onDeleteSelectedTodo = { showDeleteConfirmDialog = true }
|
onDeleteSelectedTodo = { showDeleteConfirmDialog = true },
|
||||||
|
onSearchModeChange = { searchMode.value = it },
|
||||||
|
searchMode = searchMode.value,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
|
@ -98,65 +122,84 @@ fun SharedTransitionScope.TasksPage(
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
Column {
|
||||||
state = listState,
|
AnimatedVisibility(
|
||||||
verticalArrangement = Arrangement.spacedBy(TodoDefaults.settingsItemPadding),
|
visible = searchMode.value,
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
enter = fadeIn(MaterialTheme.motionScheme.fastEffectsSpec()) + expandVertically(
|
||||||
item {
|
MaterialTheme.motionScheme.fastSpatialSpec()
|
||||||
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
|
),
|
||||||
|
exit = fadeOut(MaterialTheme.motionScheme.fastEffectsSpec()) + shrinkVertically(
|
||||||
|
MaterialTheme.motionScheme.fastSpatialSpec()
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
TodoSearchTextField(
|
||||||
|
searchMode = searchMode.value,
|
||||||
|
onSearchModeChange = { searchMode.value = it },
|
||||||
|
textFieldState = searchFieldState
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filteredTodoList.isEmpty()) {
|
LazyColumn(
|
||||||
item {
|
state = listState,
|
||||||
Text(
|
verticalArrangement = Arrangement.spacedBy(TodoDefaults.settingsItemPadding),
|
||||||
text = stringResource(R.string.tip_no_task),
|
modifier = Modifier.fillMaxSize()
|
||||||
style = MaterialTheme.typography.titleMedium,
|
) {
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items(
|
|
||||||
items = filteredTodoList,
|
|
||||||
key = { it.id }
|
|
||||||
) { task ->
|
|
||||||
TodoCard(
|
|
||||||
// id = item.id,
|
|
||||||
content = task.content,
|
|
||||||
category = task.category,
|
|
||||||
completed = task.isCompleted,
|
|
||||||
dueDate = task.dueDate,
|
|
||||||
priority = Priority.fromFloat(task.priority),
|
|
||||||
selected = selectedTodoIds.contains(task.id),
|
|
||||||
onCardClick = {
|
|
||||||
if (inSelectedMode) {
|
|
||||||
viewModel.toggleTodoSelection(task)
|
|
||||||
} else {
|
|
||||||
toTodoEditPage(task)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCardLongClick = { viewModel.toggleTodoSelection(task) },
|
|
||||||
onChecked = {
|
|
||||||
viewModel.updateTodo(task.copy(isCompleted = true))
|
|
||||||
viewModel.playConfetti()
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.sharedBounds(
|
|
||||||
sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${task.id}"),
|
|
||||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
|
||||||
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds
|
|
||||||
)
|
|
||||||
.animateItem(
|
|
||||||
fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(),
|
|
||||||
placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
|
|
||||||
fadeOutSpec = MaterialTheme.motionScheme.fastEffectsSpec()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
|
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filteredTodoList.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.tip_no_task),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(
|
||||||
|
items = filteredTodoList,
|
||||||
|
key = { it.id }
|
||||||
|
) { task ->
|
||||||
|
TodoCard(
|
||||||
|
// id = item.id,
|
||||||
|
content = task.content,
|
||||||
|
category = task.category,
|
||||||
|
completed = task.isCompleted,
|
||||||
|
dueDate = task.dueDate,
|
||||||
|
priority = Priority.fromFloat(task.priority),
|
||||||
|
selected = selectedTodoIds.contains(task.id),
|
||||||
|
onCardClick = {
|
||||||
|
if (inSelectedMode) {
|
||||||
|
viewModel.toggleTodoSelection(task)
|
||||||
|
} else {
|
||||||
|
toTodoEditPage(task)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCardLongClick = { viewModel.toggleTodoSelection(task) },
|
||||||
|
onChecked = {
|
||||||
|
viewModel.updateTodo(task.copy(isCompleted = true))
|
||||||
|
viewModel.playConfetti()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${task.id}"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||||
|
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds
|
||||||
|
)
|
||||||
|
.animateItem(
|
||||||
|
fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(),
|
||||||
|
placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
|
||||||
|
fadeOutSpec = MaterialTheme.motionScheme.fastEffectsSpec()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package cn.super12138.todo.ui.pages.tasks.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cn.super12138.todo.R
|
||||||
|
import cn.super12138.todo.utils.VibrationUtils
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun TodoSearchTextField(
|
||||||
|
searchMode: Boolean,
|
||||||
|
onSearchModeChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
textFieldState: TextFieldState,
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
TextField(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
state = textFieldState,
|
||||||
|
shape = CircleShape,
|
||||||
|
placeholder = { Text(stringResource(R.string.action_search)) },
|
||||||
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
leadingIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
VibrationUtils.performHapticFeedback(view)
|
||||||
|
onSearchModeChange(!searchMode)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_arrow_back),
|
||||||
|
contentDescription = stringResource(R.string.action_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = textFieldState.text.isNotBlank(),
|
||||||
|
enter = fadeIn() + scaleIn(),
|
||||||
|
exit = fadeOut() + scaleOut(),
|
||||||
|
) {
|
||||||
|
IconButton(onClick = { textFieldState.setTextAndPlaceCursorAtEnd("") }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_close),
|
||||||
|
contentDescription = stringResource(R.string.action_clear)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package cn.super12138.todo.ui.pages.tasks.components
|
package cn.super12138.todo.ui.pages.tasks.components
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedContentScope
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
|
@ -8,12 +10,14 @@ import androidx.compose.animation.expandIn
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.animation.shrinkOut
|
import androidx.compose.animation.shrinkOut
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
|
@ -38,8 +42,10 @@ import cn.super12138.todo.utils.VibrationUtils
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TodoTopAppBar(
|
fun TodoTopAppBar(
|
||||||
selectedTodoIds: List<Int>,
|
searchMode: Boolean,
|
||||||
selectedMode: Boolean,
|
selectedMode: Boolean,
|
||||||
|
selectedTodoIds: List<Int>,
|
||||||
|
onSearchModeChange: (Boolean) -> Unit,
|
||||||
onCancelSelect: () -> Unit,
|
onCancelSelect: () -> Unit,
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
onDeleteSelectedTodo: () -> Unit,
|
onDeleteSelectedTodo: () -> Unit,
|
||||||
|
|
@ -86,6 +92,7 @@ fun TodoTopAppBar(
|
||||||
exit = navIconExitTransition
|
exit = navIconExitTransition
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
|
shapes = IconButtonDefaults.shapes(),
|
||||||
onClick = {
|
onClick = {
|
||||||
VibrationUtils.performHapticFeedback(view)
|
VibrationUtils.performHapticFeedback(view)
|
||||||
onCancelSelect()
|
onCancelSelect()
|
||||||
|
|
@ -122,37 +129,22 @@ fun TodoTopAppBar(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
AnimatedVisibility(
|
AnimatedContent(
|
||||||
visible = selectedMode,
|
targetState = selectedMode,
|
||||||
enter = enterTransition,
|
transitionSpec = { titleTransition }
|
||||||
exit = exitTransition
|
|
||||||
) {
|
) {
|
||||||
Row {
|
if (it) {
|
||||||
IconButton(
|
ActionMultipleSelection(
|
||||||
onClick = {
|
onSelectAll = onSelectAll,
|
||||||
VibrationUtils.performHapticFeedback(view)
|
onDeleteSelectedTodo = onDeleteSelectedTodo
|
||||||
onSelectAll()
|
)
|
||||||
}
|
} else {
|
||||||
) {
|
ActionSearch(
|
||||||
Icon(
|
searchMode = searchMode,
|
||||||
painter = painterResource(R.drawable.ic_select_all),
|
onSearchModeChange = onSearchModeChange,
|
||||||
contentDescription = stringResource(R.string.tip_select_all)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
VibrationUtils.performHapticFeedback(view)
|
|
||||||
onDeleteSelectedTodo()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_delete),
|
|
||||||
contentDescription = stringResource(R.string.action_delete)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Transparent),
|
colors = TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Transparent),
|
||||||
modifier = modifier.drawBehind {
|
modifier = modifier.drawBehind {
|
||||||
|
|
@ -161,11 +153,84 @@ fun TodoTopAppBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun AnimatedContentScope.ActionSearch(
|
||||||
|
searchMode: Boolean,
|
||||||
|
onSearchModeChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
|
||||||
|
BackHandler(enabled = searchMode) { onSearchModeChange(false) }
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !searchMode,
|
||||||
|
enter = fadeIn() + scaleIn(),
|
||||||
|
exit = fadeOut() + scaleOut(),
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
shapes = IconButtonDefaults.shapes(),
|
||||||
|
onClick = {
|
||||||
|
VibrationUtils.performHapticFeedback(view)
|
||||||
|
onSearchModeChange(!searchMode)
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_search),
|
||||||
|
contentDescription = stringResource(R.string.action_search)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AnimatedContentScope.ActionMultipleSelection(
|
||||||
|
onSelectAll: () -> Unit,
|
||||||
|
onDeleteSelectedTodo: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
shapes = IconButtonDefaults.shapes(),
|
||||||
|
onClick = {
|
||||||
|
VibrationUtils.performHapticFeedback(view)
|
||||||
|
onSelectAll()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_select_all),
|
||||||
|
contentDescription = stringResource(R.string.tip_select_all)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
shapes = IconButtonDefaults.shapes(),
|
||||||
|
onClick = {
|
||||||
|
VibrationUtils.performHapticFeedback(view)
|
||||||
|
onDeleteSelectedTodo()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_delete),
|
||||||
|
contentDescription = stringResource(R.string.action_delete)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(locale = "zh-rCN", showBackground = true)
|
@Preview(locale = "zh-rCN", showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TodoTopAppBarPreview() {
|
private fun TodoTopAppBarPreview() {
|
||||||
val selectedMode = remember { mutableStateOf(false) }
|
val selectedMode = remember { mutableStateOf(false) }
|
||||||
TodoTopAppBar(
|
TodoTopAppBar(
|
||||||
|
searchMode = true,
|
||||||
|
onSearchModeChange = {},
|
||||||
selectedTodoIds = (1..10).toList(),
|
selectedTodoIds = (1..10).toList(),
|
||||||
selectedMode = selectedMode.value,
|
selectedMode = selectedMode.value,
|
||||||
onCancelSelect = { selectedMode.value = !selectedMode.value },
|
onCancelSelect = { selectedMode.value = !selectedMode.value },
|
||||||
|
|
|
||||||
9
app/src/main/res/drawable/ic_search.xml
Normal file
9
app/src/main/res/drawable/ic_search.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M380,640Q271,640 195.5,564.5Q120,489 120,380Q120,271 195.5,195.5Q271,120 380,120Q489,120 564.5,195.5Q640,271 640,380Q640,424 626,463Q612,502 588,532L812,756Q823,767 823,784Q823,801 812,812Q801,823 784,823Q767,823 756,812L532,588Q502,612 463,626Q424,640 380,640ZM380,560Q455,560 507.5,507.5Q560,455 560,380Q560,305 507.5,252.5Q455,200 380,200Q305,200 252.5,252.5Q200,305 200,380Q200,455 252.5,507.5Q305,560 380,560Z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -129,4 +129,7 @@
|
||||||
<string name="title_all_task">总任务</string>
|
<string name="title_all_task">总任务</string>
|
||||||
<string name="title_completed_task">已完成</string>
|
<string name="title_completed_task">已完成</string>
|
||||||
<string name="title_pending_task">未完成</string>
|
<string name="title_pending_task">未完成</string>
|
||||||
|
<string name="action_search">搜索</string>
|
||||||
|
<string name="action_close">关闭</string>
|
||||||
|
<string name="sorting_due_date">截止日期</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -130,4 +130,7 @@
|
||||||
<string name="title_all_task">All</string>
|
<string name="title_all_task">All</string>
|
||||||
<string name="title_completed_task">Completed</string>
|
<string name="title_completed_task">Completed</string>
|
||||||
<string name="title_pending_task">Pending</string>
|
<string name="title_pending_task">Pending</string>
|
||||||
|
<string name="action_search">Search</string>
|
||||||
|
<string name="action_close">Close</string>
|
||||||
|
<string name="sorting_due_date">Due date</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Reference in a new issue