diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c291fdc..18c4764 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,7 +38,7 @@ android { applicationId = "cn.super12138.todo" minSdk = 24 targetSdk = 36 - versionCode = 950 + versionCode = 951 versionName = "2.3.3" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt b/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt index 839f2ea..7a3de92 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt @@ -21,7 +21,7 @@ object TodoDefaults { /** * 待办卡片默认高度 */ - val toDoCardHeight = 80.dp + val toDoCardHeight = 86.dp /** * 设置项水平边距 diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt index b71834e..18b01f6 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt @@ -2,11 +2,6 @@ package cn.super12138.todo.ui.navigation import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.animation.unveilIn -import androidx.compose.animation.veilOut import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -17,8 +12,13 @@ import cn.super12138.todo.ui.pages.editor.TodoAddPage import cn.super12138.todo.ui.pages.editor.TodoEditPage import cn.super12138.todo.ui.pages.overview.OverviewPage import cn.super12138.todo.ui.pages.tasks.TasksPage +import cn.super12138.todo.ui.theme.fadeThrough import cn.super12138.todo.ui.viewmodels.MainViewModel +/** + * 来自:https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/transition/MaterialFadeThrough.java#L33 + */ + @OptIn(ExperimentalAnimationApi::class) @Composable fun TopNavigation( @@ -53,12 +53,12 @@ fun TopNavigation( * } */ - val veilColor = MaterialTheme.colorScheme.surfaceDim +// val veilColor = MaterialTheme.colorScheme.surfaceDim SharedTransitionLayout { NavDisplay( backStack = backStack.backStack, onBack = ::onBack, - transitionSpec = { + /*transitionSpec = { fadeIn() togetherWith veilOut(targetColor = veilColor) }, popTransitionSpec = { @@ -66,6 +66,15 @@ fun TopNavigation( }, predictivePopTransitionSpec = { unveilIn(initialColor = veilColor) togetherWith fadeOut() + },*/ + transitionSpec = { + fadeThrough() + }, + popTransitionSpec = { + fadeThrough() + }, + predictivePopTransitionSpec = { + fadeThrough() }, entryProvider = entryProvider { entry { diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt index 51ed747..7294b3a 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt @@ -13,23 +13,20 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation3.ui.LocalNavAnimatedContentScope @@ -45,9 +42,9 @@ import cn.super12138.todo.ui.components.TopAppBarScaffold import cn.super12138.todo.ui.pages.editor.components.TodoCategoryChip import cn.super12138.todo.ui.pages.editor.components.TodoCategoryTextField import cn.super12138.todo.ui.pages.editor.components.TodoContentTextField +import cn.super12138.todo.ui.pages.editor.components.TodoMarkAsCompletedCheckbox import cn.super12138.todo.ui.pages.editor.components.TodoPrioritySlider import cn.super12138.todo.ui.pages.editor.state.rememberEditorState -import cn.super12138.todo.utils.VibrationUtils @Composable fun SharedTransitionScope.TodoAddPage( @@ -92,8 +89,6 @@ private fun TodoEditorPage( onNavigateUp: () -> Unit ) { // TODO: 本页及其相关组件重组性能检查优化 - val view = LocalView.current - val uiState = rememberEditorState(initialTodo = toDo) val originalCategories by DataStoreManager.categoriesFlow.collectAsState(initial = emptyList()) @@ -180,11 +175,11 @@ private fun TodoEditorPage( verticalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.fillMaxSize() ) { - item { + item(key = 0) { Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) } - item { + item(key=1) { TodoContentTextField( value = uiState.toDoContent, onValueChange = { uiState.toDoContent = it }, @@ -193,7 +188,7 @@ private fun TodoEditorPage( ) } - item { + item(key = 2) { Text( text = stringResource(R.string.label_category), style = MaterialTheme.typography.titleMedium @@ -222,7 +217,7 @@ private fun TodoEditorPage( } } - item { + item (key = 3){ Text( text = stringResource(R.string.label_priority), style = MaterialTheme.typography.titleMedium @@ -234,34 +229,22 @@ private fun TodoEditorPage( ) } - item { + item(key = 4) { if (toDo != null) { Text( text = stringResource(R.string.label_more), style = MaterialTheme.typography.titleMedium ) - Row( - verticalAlignment = Alignment.CenterVertically, + TodoMarkAsCompletedCheckbox( + checked = uiState.isCompleted, + onCheckedChange = { uiState.isCompleted = it }, modifier = Modifier.fillMaxWidth() - ) { - Text( - text = stringResource(R.string.tip_mark_completed), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(end = 10.dp) - ) - Switch( - checked = uiState.isCompleted, - onCheckedChange = { - VibrationUtils.performHapticFeedback(view) - uiState.isCompleted = it - } - ) - } + ) } } - item { + item (key = 5){ Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) } } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoMarkAsCompletedCheckbox.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoMarkAsCompletedCheckbox.kt new file mode 100644 index 0000000..01279de --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoMarkAsCompletedCheckbox.kt @@ -0,0 +1,59 @@ +package cn.super12138.todo.ui.pages.editor.components + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import cn.super12138.todo.R +import cn.super12138.todo.utils.VibrationUtils + +@Composable +fun TodoMarkAsCompletedCheckbox( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + val view = LocalView.current + val interactionSource = remember { MutableInteractionSource() } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier + .toggleable( + value = checked, + onValueChange = { + VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS) + onCheckedChange(it) + }, + role = Role.Checkbox, + indication = null, + interactionSource = interactionSource, + ) + ) { + Text( + text = stringResource(R.string.tip_mark_completed), + style = MaterialTheme.typography.labelLarge + ) + + Checkbox( + checked = checked, + onCheckedChange = { + VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS) + onCheckedChange(it) + }, + interactionSource = interactionSource + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt index f750e16..f4922f8 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme @@ -27,7 +27,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import androidx.navigation3.ui.LocalNavAnimatedContentScope import cn.super12138.todo.R import cn.super12138.todo.constants.Constants @@ -103,7 +102,7 @@ fun SharedTransitionScope.TasksPage( ) { LazyColumn( state = listState, - verticalArrangement = Arrangement.spacedBy(5.dp), + verticalArrangement = Arrangement.spacedBy(TodoDefaults.settingsItemPadding), modifier = Modifier.fillMaxSize() ) { item { @@ -120,32 +119,34 @@ fun SharedTransitionScope.TasksPage( ) } } else { - items( + itemsIndexed( items = filteredTodoList, - key = { it.id } - ) { + key = { _, task -> task.id } + ) { index, task -> TodoCard( // id = item.id, - content = it.content, - category = it.category, - completed = it.isCompleted, - priority = Priority.fromFloat(it.priority), - selected = selectedTodoIds.contains(it.id), + content = task.content, + category = task.category, + completed = task.isCompleted, + priority = Priority.fromFloat(task.priority), + selected = selectedTodoIds.contains(task.id), onCardClick = { if (inSelectedMode) { - viewModel.toggleTodoSelection(it) + viewModel.toggleTodoSelection(task) } else { - toTodoEditPage(it) + toTodoEditPage(task) } }, - onCardLongClick = { viewModel.toggleTodoSelection(it) }, + onCardLongClick = { viewModel.toggleTodoSelection(task) }, onChecked = { - viewModel.updateTodo(it.copy(isCompleted = true)) + viewModel.updateTodo(task.copy(isCompleted = true)) viewModel.playConfetti() }, + topRounded = index == 0, + bottomRounded = index == filteredTodoList.size - 1, modifier = Modifier .sharedBounds( - sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${it.id}"), + sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${task.id}"), animatedVisibilityScope = LocalNavAnimatedContentScope.current ) .animateItem( diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt index 9910509..12d7a15 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt @@ -1,6 +1,7 @@ package cn.super12138.todo.ui.pages.tasks.components import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.expandHorizontally import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -17,10 +18,8 @@ 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.foundation.shape.CornerBasedShape import androidx.compose.material3.Badge -import androidx.compose.material3.BadgedBox -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -28,6 +27,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -42,6 +42,7 @@ import cn.super12138.todo.logic.model.Priority import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.utils.VibrationUtils import cn.super12138.todo.utils.containerColor +import cn.super12138.todo.utils.getPartialRoundedShape @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -55,19 +56,22 @@ fun TodoCard( selected: Boolean, onCardClick: () -> Unit = {}, onCardLongClick: () -> Unit = {}, - onChecked: () -> Unit = {} + onChecked: () -> Unit = {}, + shape: CornerBasedShape = TodoDefaults.SettingsItemDefaultShape, + roundedShape: CornerBasedShape = TodoDefaults.SettingsItemRoundedShape, + topRounded: Boolean = false, + bottomRounded: Boolean = false ) { val view = LocalView.current - Card( - colors = CardDefaults.cardColors(containerColor = TodoDefaults.ContainerColor), - modifier = modifier - .fillMaxWidth() - .height(TodoDefaults.toDoCardHeight) - ) { + // TODO: 滑动删除 + val animatedContainerColor by animateColorAsState(if (selected) MaterialTheme.colorScheme.secondaryContainer else TodoDefaults.ContainerColor) + Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() + modifier = modifier + .fillMaxWidth() + .height(TodoDefaults.toDoCardHeight) + .clip(shape.getPartialRoundedShape(topRounded, bottomRounded, roundedShape)) .combinedClickable( onClick = { VibrationUtils.performHapticFeedback(view) @@ -77,7 +81,8 @@ fun TodoCard( // 因为 combinedClickable 在更新的 Compose 里已经处理好了触感反馈 onLongClick = onCardLongClick ) - .padding(horizontal = 15.dp) + .background(animatedContainerColor) + .padding(horizontal = TodoDefaults.screenHorizontalPadding) ) { AnimatedVisibility( visible = selected, @@ -104,40 +109,42 @@ fun TodoCard( } Column( - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy( + space = 5.dp, + alignment = Alignment.CenterVertically + ), modifier = Modifier .weight(1f) .fillMaxSize() ) { - BadgedBox( - badge = { - Badge( - containerColor = priority.containerColor(), - modifier = Modifier.padding(start = 5.dp) - ) { - Text( - text = stringResource(priority.nameRes), - textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, - ) - } - } + Text( + text = content, + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, + modifier = Modifier.basicMarquee() // TODO: 后续评估性能影响 + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp) ) { + Badge(containerColor = MaterialTheme.colorScheme.primary) { + Text( + text = category.ifEmpty { stringResource(R.string.tip_default_category) }, + style = MaterialTheme.typography.labelMedium, + textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, + maxLines = 1 + ) + } + Text( - text = content, - style = MaterialTheme.typography.titleLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + text = stringResource(priority.nameRes), + style = MaterialTheme.typography.labelMedium.copy(priority.containerColor()), textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, - modifier = Modifier.basicMarquee() // TODO: 后续评估性能影响 ) } - - Text( - text = category.ifEmpty { stringResource(R.string.tip_default_category) }, - style = MaterialTheme.typography.labelMedium, - textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, - maxLines = 1 - ) } AnimatedVisibility(!selected && !completed) { @@ -172,7 +179,7 @@ fun TodoCard( ) }*/ } - } + } /* diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt index cdaf9c4..a8bcc64 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt @@ -9,6 +9,7 @@ import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally @@ -17,6 +18,8 @@ import androidx.compose.animation.slideOutHorizontally */ private const val ProgressThreshold = 0.35f +private const val DEFAULT_START_SCALE = 0.95f //0.92f +private const val THRESHOLD_ALPHA = 0.5f private val Int.ForOutgoing: Int get() = (this * ProgressThreshold).toInt() @@ -84,4 +87,31 @@ fun materialSharedAxisXOut( delayMillis = 0, easing = FastOutLinearInEasing ) +) + +fun fadeThrough( + durationMillis: Int = 200, +): ContentTransform = ContentTransform( + fadeThroughIn(durationMillis = durationMillis), + fadeThroughOut(durationMillis = durationMillis) +) + +fun fadeThroughIn( + durationMillis: Int = DefaultMotionDuration, +): EnterTransition = + fadeIn( + animationSpec = tween(durationMillis), + initialAlpha = THRESHOLD_ALPHA + ) + scaleIn( + animationSpec = tween(durationMillis), + initialScale = DEFAULT_START_SCALE + ) + +fun fadeThroughOut( + durationMillis: Int = DefaultMotionDuration, +): ExitTransition = fadeOut( + animationSpec = tween( + durationMillis = durationMillis, + easing = FastOutLinearInEasing + ) ) \ No newline at end of file