feat: 使用导航方式2 & 添加转场动画

This commit is contained in:
Super12138 2026-01-26 22:34:39 +08:00
parent 4c95c7e26e
commit 2b28b446eb
25 changed files with 413 additions and 307 deletions

View file

@ -38,7 +38,7 @@ android {
applicationId = "cn.super12138.todo"
minSdk = 24
targetSdk = 36
versionCode = 941
versionCode = 950
versionName = "2.3.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View file

@ -5,8 +5,7 @@ object Constants {
const val GITHUB_REPO = "https://github.com/Super12138/ToDo/"
const val KEY_TODO_FAB_TRANSITION = "todo_fab"
// const val KEY_TODO_CONTENT_TRANSITION = "todo_content"
// const val KEY_TODO_CATEGORY_TRANSITION = "todo_category"
const val KEY_TODO_ITEM_TRANSITION = "todo_item_id"
const val DB_NAME = "todo"
const val DB_TABLE_NAME = "todo"

View file

@ -0,0 +1,18 @@
package cn.super12138.todo.logic
import cn.super12138.todo.logic.database.TodoEntity
import kotlinx.coroutines.flow.Flow
interface IRepository {
suspend fun insertTodo(toDo: TodoEntity)
fun getAllTodos(): Flow<List<TodoEntity>>
suspend fun updateTodo(toDo: TodoEntity)
suspend fun deleteTodo(toDo: TodoEntity)
suspend fun deleteTodoFromIds(toDoItems: List<Int>)
// suspend fun deleteAllTodo()
}

View file

@ -4,29 +4,29 @@ import cn.super12138.todo.TodoApp
import cn.super12138.todo.logic.database.TodoEntity
import kotlinx.coroutines.flow.Flow
object Repository {
object Repository : IRepository {
private val db get() = TodoApp.db
private val toDoDao = db.toDoDao()
suspend fun insertTodo(toDo: TodoEntity) {
override suspend fun insertTodo(toDo: TodoEntity) {
toDoDao.insert(toDo)
}
fun getAllTodos(): Flow<List<TodoEntity>> = toDoDao.getAll()
override fun getAllTodos(): Flow<List<TodoEntity>> = toDoDao.getAll()
suspend fun updateTodo(toDo: TodoEntity) {
override suspend fun updateTodo(toDo: TodoEntity) {
toDoDao.update(toDo)
}
suspend fun deleteTodo(toDo: TodoEntity) {
override suspend fun deleteTodo(toDo: TodoEntity) {
toDoDao.delete(toDo)
}
suspend fun deleteTodoFromIds(toDoItems: List<Int>) {
override suspend fun deleteTodoFromIds(toDoItems: List<Int>) {
toDoDao.deleteFromIds(toDoItems.toSet())
}
/*suspend fun deleteAllTodo() {
/*override suspend fun deleteAllTodo() {
toDoDao.deleteAllTodo()
}*/
}

View file

@ -5,7 +5,7 @@ import cn.super12138.todo.R
enum class ContrastLevel(
val value: Float,
@StringRes val nameRes: Int
@param:StringRes val nameRes: Int
) {
VeryLow(value = -1f, nameRes = R.string.contrast_very_low),
Low(value = -0.5f, nameRes = R.string.contrast_low),

View file

@ -6,8 +6,8 @@ import cn.super12138.todo.R
enum class DarkMode(
val id: Int,
@DrawableRes val iconRes: Int,
@StringRes val nameRes: Int
@param:DrawableRes val iconRes: Int,
@param:StringRes val nameRes: Int
) {
FollowSystem(id = -1, iconRes = R.drawable.ic_lightbulb_2, nameRes = R.string.dark_mode_system),
Light(id = 1, iconRes = R.drawable.ic_light_mode, nameRes = R.string.dark_mode_light),

View file

@ -5,7 +5,7 @@ import cn.super12138.todo.R
enum class PaletteStyle(
val id: Int,
@StringRes val nameRes: Int
@param:StringRes val nameRes: Int
) {
TonalSpot(id = 1, nameRes = R.string.palette_tonal_spot),
Neutral(id = 2, nameRes = R.string.palette_neutral),

View file

@ -5,7 +5,7 @@ import cn.super12138.todo.R
enum class Priority(
val value: Float,
@StringRes val nameRes: Int
@param:StringRes val nameRes: Int
) {
Urgent(value = 2f, nameRes = R.string.priority_urgent),
Important(value = 1f, nameRes = R.string.priority_important),

View file

@ -5,7 +5,7 @@ import cn.super12138.todo.R
enum class SortingMethod(
val id: Int,
@StringRes val nameRes: Int
@param:StringRes val nameRes: Int
) {
// 按添加先后顺序
Sequential(id = 1, nameRes = R.string.sorting_sequential),

View file

@ -16,7 +16,7 @@ object TodoDefaults {
/**
* 屏幕上下预留边距防止内容全部贴边显示过丑
*/
val screenVerticalPadding = 16.dp
val screenVerticalPadding = 8.dp
/**
* 待办卡片默认高度

View file

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Icon
@ -26,7 +27,7 @@ import cn.super12138.todo.logic.model.PaletteStyle
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.Konfetti
import cn.super12138.todo.ui.navigation.TodoDestinations
import cn.super12138.todo.ui.navigation.TodoNavigation
import cn.super12138.todo.ui.navigation.TopNavigation
import cn.super12138.todo.ui.theme.ToDoTheme
import cn.super12138.todo.ui.viewmodels.MainViewModel
import cn.super12138.todo.utils.VibrationUtils
@ -39,7 +40,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
val mainViewModel: MainViewModel = viewModel()
val navigationBackStack = mainViewModel.topLevelBackStack
val mainBackStack = mainViewModel.mainBackStack
val showConfetti = mainViewModel.showConfetti
// 主题
val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT)
@ -89,27 +90,34 @@ class MainActivity : ComponentActivity() {
Surface(color = TodoDefaults.BackgroundColor) {
NavigationSuiteScaffold(
navigationSuiteItems = {
TodoDestinations.entries.forEach {
val selected = it.route == navigationBackStack.topLevelKey
TodoDestinations.entries.forEach { destination ->
val selected = destination.route == mainBackStack.topLevelKey
item(
icon = {
Icon(
painter = if (selected) painterResource(it.selectedIcon) else painterResource(
it.icon
),
contentDescription = null
)
Crossfade(selected) {
if (it) {
Icon(
painter = painterResource(destination.selectedIcon),
contentDescription = null
)
} else {
Icon(
painter = painterResource(destination.icon),
contentDescription = null
)
}
}
},
label = { Text(stringResource(it.label)) },
label = { Text(stringResource(destination.label)) },
selected = selected,
onClick = { navigationBackStack.addTopLevel(it.route) }
onClick = { mainBackStack.addTopLevel(destination.route) }
)
}
},
containerColor = TodoDefaults.BackgroundColor
) {
TodoNavigation(
backStack = navigationBackStack,
TopNavigation(
backStack = mainBackStack,
viewModel = mainViewModel,
modifier = Modifier.fillMaxSize()
)

View file

@ -1,2 +1,105 @@
package cn.super12138.todo.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import cn.super12138.todo.ui.pages.settings.SettingsAbout
import cn.super12138.todo.ui.pages.settings.SettingsAboutLicence
import cn.super12138.todo.ui.pages.settings.SettingsAppearance
import cn.super12138.todo.ui.pages.settings.SettingsData
import cn.super12138.todo.ui.pages.settings.SettingsDataCategory
import cn.super12138.todo.ui.pages.settings.SettingsDeveloperOptions
import cn.super12138.todo.ui.pages.settings.SettingsDeveloperOptionsPadding
import cn.super12138.todo.ui.pages.settings.SettingsInterface
import cn.super12138.todo.ui.pages.settings.SettingsMain
import cn.super12138.todo.ui.theme.materialSharedAxisX
import cn.super12138.todo.ui.viewmodels.MainViewModel
private const val INITIAL_OFFSET_FACTOR = 0.10f
@Composable
fun SettingsNavigation(
backStack: NavBackStack<NavKey>,
viewModel: MainViewModel,
modifier: Modifier = Modifier
) {
fun onBack() {
backStack.removeLastOrNull()
}
NavDisplay(
backStack = backStack,
onBack = ::onBack,
transitionSpec = {
materialSharedAxisX(
initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() })
},
popTransitionSpec = {
materialSharedAxisX(
initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() })
},
entryProvider = entryProvider {
entry<TodoScreen.Settings.Main> {
SettingsMain(
toAppearancePage = { backStack.add(TodoScreen.Settings.Appearance) },
toAboutPage = { backStack.add(TodoScreen.Settings.About) },
toInterfacePage = { backStack.add(TodoScreen.Settings.Interface) },
toDataPage = { backStack.add(TodoScreen.Settings.Data) },
)
}
entry<TodoScreen.Settings.Appearance> {
SettingsAppearance(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.Interface> {
SettingsInterface(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.Data> {
SettingsData(
viewModel = viewModel,
toCategoryManager = { backStack.add(TodoScreen.Settings.DataCategory) },
onNavigateUp = ::onBack
)
}
entry<TodoScreen.Settings.DataCategory> {
SettingsDataCategory(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.About> {
SettingsAbout(
//toSpecialPage = { backStack.add(TodoScreen.Settings.AboutSpecial) },
toLicencePage = { backStack.add(TodoScreen.Settings.AboutLicence) },
toDevPage = { backStack.add(TodoScreen.Settings.DeveloperOptions) },
onNavigateUp = ::onBack,
)
}
/*entry<TodoScreen.Settings.AboutSpecial> {
SettingsAboutSpecial(viewModel = viewModel)
}*/
entry<TodoScreen.Settings.AboutLicence> {
SettingsAboutLicence(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.DeveloperOptions> {
SettingsDeveloperOptions(
toPaddingPage = { backStack.add(TodoScreen.Settings.DeveloperOptionsPadding) },
onNavigateUp = ::onBack
)
}
entry<TodoScreen.Settings.DeveloperOptionsPadding> {
SettingsDeveloperOptionsPadding(onNavigateUp = ::onBack)
}
},
modifier = modifier,
)
}

View file

@ -6,9 +6,9 @@ import androidx.navigation3.runtime.NavKey
enum class TodoDestinations(
val route: NavKey,
@StringRes val label: Int,
@DrawableRes val icon: Int,
@DrawableRes val selectedIcon: Int
@param:StringRes val label: Int,
@param:DrawableRes val icon: Int,
@param:DrawableRes val selectedIcon: Int
) {
Overview(
route = TodoScreen.Overview,
@ -23,7 +23,7 @@ enum class TodoDestinations(
selectedIcon = cn.super12138.todo.R.drawable.ic_ballot_filled
),
Settings(
route = TodoScreen.Settings,
route = TodoScreen.Settings.Main,
label = cn.super12138.todo.R.string.page_settings,
icon = cn.super12138.todo.R.drawable.ic_settings,
selectedIcon = cn.super12138.todo.R.drawable.ic_settings_filled

View file

@ -1,175 +0,0 @@
package cn.super12138.todo.ui.navigation
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import cn.super12138.todo.ui.pages.editor.TodoEditorPage
import cn.super12138.todo.ui.pages.main.MainPage
import cn.super12138.todo.ui.pages.overview.OverviewPage
import cn.super12138.todo.ui.pages.settings.SettingsAbout
import cn.super12138.todo.ui.pages.settings.SettingsAboutLicence
import cn.super12138.todo.ui.pages.settings.SettingsAppearance
import cn.super12138.todo.ui.pages.settings.SettingsData
import cn.super12138.todo.ui.pages.settings.SettingsDataCategory
import cn.super12138.todo.ui.pages.settings.SettingsDeveloperOptions
import cn.super12138.todo.ui.pages.settings.SettingsDeveloperOptionsPadding
import cn.super12138.todo.ui.pages.settings.SettingsInterface
import cn.super12138.todo.ui.pages.settings.SettingsMain
import cn.super12138.todo.ui.theme.materialSharedAxisX
import cn.super12138.todo.ui.viewmodels.MainViewModel
private const val INITIAL_OFFSET_FACTOR = 0.10f
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun TodoNavigation(
backStack: TopLevelBackStack<NavKey>,
modifier: Modifier = Modifier,
viewModel: MainViewModel
) {
fun onBack() {
backStack.removeLast()
}
val anim = NavDisplay.transitionSpec {
// Slide new content up, keeping the old content in place underneath
slideInVertically(
initialOffsetY = { it / 5 },
animationSpec = tween(200)
) + fadeIn(tween(200)) togetherWith ExitTransition.KeepUntilTransitionsFinished
} + NavDisplay.popTransitionSpec {
// Slide old content down, revealing the new content in place underneath
EnterTransition.None togetherWith
slideOutVertically(
targetOffsetY = { it / 5 },
animationSpec = tween(200)
) + fadeOut(tween(200))
} + NavDisplay.predictivePopTransitionSpec {
// Slide old content down, revealing the new content in place underneath
EnterTransition.None togetherWith
slideOutVertically(
targetOffsetY = { it / 5 },
animationSpec = tween(200)
) + fadeOut(tween(200))
}
SharedTransitionLayout {
NavDisplay(
backStack = backStack.backStack,
onBack = ::onBack,
transitionSpec = {
materialSharedAxisX(
initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() })
},
popTransitionSpec = {
materialSharedAxisX(
initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() })
},
entryProvider = entryProvider {
entry<TodoScreen.Overview>(metadata = anim) {
OverviewPage()
}
entry<TodoScreen.Tasks>(metadata = anim) {
MainPage(
viewModel = viewModel,
toTodoEditPage = { backStack.add(TodoScreen.Editor(it)) },
sharedTransitionScope = this@SharedTransitionLayout
)
}
entry<TodoScreen.Settings>(metadata = anim) {
SettingsMain(
toAppearancePage = { backStack.add(TodoScreen.SettingsAppearance) },
toAboutPage = { backStack.add(TodoScreen.SettingsAbout) },
toInterfacePage = { backStack.add(TodoScreen.SettingsInterface) },
toDataPage = { backStack.add(TodoScreen.SettingsData) },
)
}
entry<TodoScreen.Editor> { editorArgs ->
TodoEditorPage(
toDo = editorArgs.toDo,
onSave = {
viewModel.addTodo(it)
// 如果原来的待办状态为未完成并且修改后状态为完成
if (editorArgs.toDo?.isCompleted != true && it.isCompleted) {
viewModel.playConfetti()
}
onBack()
},
onDelete = {
if (editorArgs.toDo !== null) {
viewModel.deleteTodo(editorArgs.toDo)
}
onBack()
},
onNavigateUp = ::onBack,
sharedTransitionScope = this@SharedTransitionLayout
)
}
entry<TodoScreen.SettingsAppearance> {
SettingsAppearance(onNavigateUp = ::onBack)
}
entry<TodoScreen.SettingsInterface> {
SettingsInterface(onNavigateUp = ::onBack)
}
entry<TodoScreen.SettingsData> {
SettingsData(
viewModel = viewModel,
toCategoryManager = { backStack.add(TodoScreen.SettingsDataCategory) },
onNavigateUp = ::onBack
)
}
entry<TodoScreen.SettingsDataCategory> {
SettingsDataCategory(onNavigateUp = ::onBack)
}
entry<TodoScreen.SettingsAbout> {
SettingsAbout(
//toSpecialPage = { backStack.add(TodoScreen.SettingsAboutSpecial) },
toLicencePage = { backStack.add(TodoScreen.SettingsAboutLicence) },
toDevPage = { backStack.add(TodoScreen.SettingsDev) },
onNavigateUp = ::onBack,
)
}
/*entry<TodoScreen.SettingsAboutSpecial> {
SettingsAboutSpecial(viewModel = viewModel)
}*/
entry<TodoScreen.SettingsAboutLicence> {
SettingsAboutLicence(onNavigateUp = ::onBack)
}
entry<TodoScreen.SettingsDev> {
SettingsDeveloperOptions(
toPaddingPage = { backStack.add(TodoScreen.SettingsDevPadding) },
onNavigateUp = ::onBack
)
}
entry<TodoScreen.SettingsDevPadding> {
SettingsDeveloperOptionsPadding(onNavigateUp = ::onBack)
}
},
modifier = modifier,
)
}
}

View file

@ -13,35 +13,44 @@ sealed class TodoScreen : NavKey {
data object Tasks : TodoScreen()
@Serializable
data class Editor(val toDo: TodoEntity?) : TodoScreen()
sealed class Settings : TodoScreen() {
@Serializable
data object Main : Settings()
@Serializable
data object Appearance : Settings()
@Serializable
data object Interface : Settings()
@Serializable
data object Data : Settings()
@Serializable
data object DataCategory : Settings()
@Serializable
data object About : Settings()
// @Serializable
// data object AboutEasterEgg : Settings()
@Serializable
data object AboutLicence : Settings()
@Serializable
data object DeveloperOptions : Settings()
@Serializable
data object DeveloperOptionsPadding : Settings()
}
@Serializable
data object Settings : TodoScreen()
sealed class Editor : TodoScreen() {
@Serializable
data object Add : Editor()
@Serializable
data object SettingsAppearance : TodoScreen()
@Serializable
data object SettingsInterface : TodoScreen()
@Serializable
data object SettingsData : TodoScreen()
@Serializable
data object SettingsDataCategory : TodoScreen()
@Serializable
data object SettingsAbout : TodoScreen()
// @Serializable
// data object SettingsAboutSpecial : TodoScreen()
@Serializable
data object SettingsAboutLicence : TodoScreen()
@Serializable
data object SettingsDev : TodoScreen()
@Serializable
data object SettingsDevPadding : TodoScreen()
@Serializable
data class Edit(val toDo: TodoEntity) : Editor()
}
}

View file

@ -0,0 +1,122 @@
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
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
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.viewmodels.MainViewModel
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun TopNavigation(
backStack: TopLevelBackStack<NavKey>,
modifier: Modifier = Modifier,
viewModel: MainViewModel
) {
fun onBack() {
backStack.removeLast()
}
/*
* NavDisplay.transitionSpec {
* // Slide new content up, keeping the old content in place underneath
* slideInVertically(
* initialOffsetY = { it / 5 },
* animationSpec = tween(200)
* ) + fadeIn(tween(200)) togetherWith ExitTransition.KeepUntilTransitionsFinished
* } + NavDisplay.popTransitionSpec {
* // Slide old content down, revealing the new content in place underneath
* EnterTransition.None togetherWith
* slideOutVertically(
* targetOffsetY = { it / 5 },
* animationSpec = tween(200)
* ) + fadeOut(tween(200))
* } + NavDisplay.predictivePopTransitionSpec {
* // Slide old content down, revealing the new content in place underneath
* EnterTransition.None togetherWith
* slideOutVertically(
* targetOffsetY = { it / 5 },
* animationSpec = tween(200)
* ) + fadeOut(tween(200))
* }
*/
val veilColor = MaterialTheme.colorScheme.surfaceDim
SharedTransitionLayout {
NavDisplay(
backStack = backStack.backStack,
onBack = ::onBack,
transitionSpec = {
fadeIn() togetherWith veilOut(targetColor = veilColor)
},
popTransitionSpec = {
unveilIn(initialColor = veilColor) togetherWith fadeOut()
},
predictivePopTransitionSpec = {
unveilIn(initialColor = veilColor) togetherWith fadeOut()
},
entryProvider = entryProvider {
entry<TodoScreen.Overview> {
OverviewPage()
}
entry<TodoScreen.Tasks> {
TasksPage(
viewModel = viewModel,
toTodoAddPage = { backStack.add(TodoScreen.Editor.Add) },
toTodoEditPage = { backStack.add(TodoScreen.Editor.Edit(it)) }
)
}
entry<TodoScreen.Editor.Add> {
TodoAddPage(
onSave = {
viewModel.addTodo(it)
onBack()
},
onNavigateUp = ::onBack
)
}
entry<TodoScreen.Editor.Edit> { editorArgs ->
TodoEditPage(
toDo = editorArgs.toDo,
onSave = {
viewModel.addTodo(it)
// 如果原来的待办状态为未完成并且修改后状态为完成
if (!editorArgs.toDo.isCompleted && it.isCompleted) {
viewModel.playConfetti()
}
onBack()
},
onDelete = {
viewModel.deleteTodo(editorArgs.toDo)
onBack()
},
onNavigateUp = ::onBack
)
}
entry<TodoScreen.Settings.Main> {
SettingsNavigation(
backStack = viewModel.settingsBackStack,
viewModel = viewModel
)
}
},
modifier = modifier,
)
}
}

View file

@ -2,7 +2,6 @@ package cn.super12138.todo.ui.pages.editor
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
@ -17,12 +16,9 @@ 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.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -41,6 +37,7 @@ import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.ChipItem
import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.components.TodoFloatingActionButton
@ -52,23 +49,50 @@ import cn.super12138.todo.ui.pages.editor.components.TodoPrioritySlider
import cn.super12138.todo.ui.pages.editor.state.rememberEditorState
import cn.super12138.todo.utils.VibrationUtils
@OptIn(
ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class,
ExperimentalMaterial3ExpressiveApi::class
)
@Composable
fun TodoEditorPage(
fun SharedTransitionScope.TodoAddPage(
modifier: Modifier = Modifier,
onSave: (TodoEntity) -> Unit,
onNavigateUp: () -> Unit
) = TodoEditorPage(
toDo = null,
modifier = modifier.sharedBounds(
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
),
onSave = onSave,
onDelete = {},
onNavigateUp = onNavigateUp
)
@Composable
fun SharedTransitionScope.TodoEditPage(
modifier: Modifier = Modifier,
toDo: TodoEntity,
onSave: (TodoEntity) -> Unit,
onDelete: () -> Unit,
onNavigateUp: () -> Unit
) = TodoEditorPage(
toDo = toDo,
modifier = modifier.sharedBounds(
sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${toDo.id}"),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
),
onSave = onSave,
onDelete = onDelete,
onNavigateUp = onNavigateUp
)
@Composable
private fun TodoEditorPage(
modifier: Modifier = Modifier,
toDo: TodoEntity? = null,
onSave: (TodoEntity) -> Unit,
onDelete: () -> Unit,
onNavigateUp: () -> Unit,
sharedTransitionScope: SharedTransitionScope
onNavigateUp: () -> Unit
) {
// TODO: 本页及其相关组件重组性能检查优化
val view = LocalView.current
val animatedVisibilityScope = LocalNavAnimatedContentScope.current
TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val uiState = rememberEditorState(initialTodo = toDo)
@ -114,7 +138,6 @@ fun TodoEditorPage(
TopAppBarScaffold(
title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task),
floatingActionButton = {
with(sharedTransitionScope) {
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.imePadding()
@ -146,15 +169,9 @@ fun TodoEditorPage(
)
onSave(newTodo)
}
},
modifier = Modifier
.sharedElement(
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = animatedVisibilityScope
)
}
)
}
}
},
onBack = ::checkModifiedBeforeBack,
modifier = modifier
@ -163,6 +180,10 @@ fun TodoEditorPage(
verticalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.fillMaxSize()
) {
item {
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
}
item {
TodoContentTextField(
value = uiState.toDoContent,
@ -241,7 +262,7 @@ fun TodoEditorPage(
}
item {
Spacer(modifier = Modifier.size(80.dp))
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
}
}

View file

@ -1,6 +1,7 @@
package cn.super12138.todo.ui.pages.overview
import androidx.compose.foundation.layout.Row
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -11,13 +12,14 @@ import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R
import cn.super12138.todo.ui.components.TopAppBarScaffold
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun OverviewPage(modifier: Modifier = Modifier) {
TopAppBarScaffold(
title = stringResource(R.string.page_overview),
modifier = modifier
) {
Row {
Column {
Card {
Icon(painter = painterResource(R.drawable.ic_ballot), contentDescription = null)
Text("总任务")

View file

@ -21,10 +21,10 @@ fun SettingsCategory(
modifier = modifier
.fillMaxWidth()
.padding(
top = if (first) 0.dp else TodoDefaults.settingsItemVerticalPadding,
start = TodoDefaults.settingsItemHorizontalPadding / 2,
end = TodoDefaults.settingsItemHorizontalPadding / 2,
bottom = TodoDefaults.settingsItemVerticalPadding / 2
top = if (first) 0.dp else TodoDefaults.screenVerticalPadding,
start = TodoDefaults.screenHorizontalPadding / 2,
end = TodoDefaults.screenHorizontalPadding / 2,
bottom = TodoDefaults.screenVerticalPadding
)
) {
Text(

View file

@ -23,13 +23,13 @@ fun SettingsContainer(
modifier = modifier
) {
item {
Spacer(modifier = Modifier.size(TodoDefaults.settingsItemVerticalPadding))
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
}
content()
item {
Spacer(modifier = Modifier.size(TodoDefaults.settingsItemVerticalPadding))
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
}
}
}

View file

@ -1,4 +1,4 @@
package cn.super12138.todo.ui.pages.main
package cn.super12138.todo.ui.pages.tasks
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState

View file

@ -1,4 +1,4 @@
package cn.super12138.todo.ui.pages.main
package cn.super12138.todo.ui.pages.tasks
import androidx.activity.compose.BackHandler
import androidx.compose.animation.SharedTransitionScope
@ -38,17 +38,17 @@ import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.components.TodoFloatingActionButton
import cn.super12138.todo.ui.components.TopAppBarScaffold
import cn.super12138.todo.ui.pages.main.components.TodoCard
import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar
import cn.super12138.todo.ui.pages.tasks.components.TodoCard
import cn.super12138.todo.ui.pages.tasks.components.TodoTopAppBar
import cn.super12138.todo.ui.viewmodels.MainViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MainPage(
fun SharedTransitionScope.TasksPage(
modifier: Modifier = Modifier,
viewModel: MainViewModel,
toTodoEditPage: (TodoEntity?) -> Unit,
sharedTransitionScope: SharedTransitionScope,
toTodoAddPage: () -> Unit,
toTodoEditPage: (TodoEntity) -> Unit,
) {
val animatedVisibilityScope = LocalNavAnimatedContentScope.current
@ -82,23 +82,21 @@ fun MainPage(
)
},
floatingActionButton = {
with(sharedTransitionScope) {
TodoFloatingActionButton(
text = stringResource(R.string.action_add_task),
iconRes = R.drawable.ic_add,
expanded = expandedFab,
onClick = { toTodoEditPage(null) },
modifier = Modifier
.sharedElement(
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = animatedVisibilityScope
)
.animateFloatingActionButton(
visible = !inSelectedMode,
alignment = Alignment.BottomEnd,
)
)
}
TodoFloatingActionButton(
text = stringResource(R.string.action_add_task),
iconRes = R.drawable.ic_add,
expanded = expandedFab,
onClick = { toTodoAddPage() },
modifier = Modifier
.sharedBounds(
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = animatedVisibilityScope
)
.animateFloatingActionButton(
visible = !inSelectedMode,
alignment = Alignment.BottomEnd,
)
)
},
contentWindowInsets = WindowInsets(0, 0, 0, 0),
modifier = modifier
@ -109,7 +107,7 @@ fun MainPage(
modifier = Modifier.fillMaxSize()
) {
item {
Spacer(modifier = Modifier.size(TodoDefaults.settingsItemVerticalPadding))
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
}
if (filteredTodoList.isEmpty()) {
@ -146,6 +144,10 @@ fun MainPage(
viewModel.playConfetti()
},
modifier = Modifier
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${it.id}"),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
.animateItem(
fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(),
placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
@ -154,7 +156,7 @@ fun MainPage(
)
}
item {
Spacer(modifier = Modifier.size(TodoDefaults.settingsItemVerticalPadding))
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
}
}
}

View file

@ -1,12 +1,10 @@
package cn.super12138.todo.ui.pages.main.components
package cn.super12138.todo.ui.pages.tasks.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.combinedClickable
@ -21,7 +19,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.ElevatedCard
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
@ -32,7 +31,6 @@ 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.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -45,10 +43,7 @@ import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.utils.VibrationUtils
import cn.super12138.todo.utils.containerColor
@OptIn(
ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class,
ExperimentalMaterial3ExpressiveApi::class
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun TodoCard(
modifier: Modifier = Modifier,
@ -63,8 +58,8 @@ fun TodoCard(
onChecked: () -> Unit = {}
) {
val view = LocalView.current
val context = LocalContext.current
ElevatedCard(
Card(
colors = CardDefaults.cardColors(containerColor = TodoDefaults.ContainerColor),
modifier = modifier
.fillMaxWidth()
.height(TodoDefaults.toDoCardHeight)

View file

@ -1,4 +1,4 @@
package cn.super12138.todo.ui.pages.main.components
package cn.super12138.todo.ui.pages.tasks.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility

View file

@ -5,6 +5,7 @@ import android.net.Uri
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import cn.super12138.todo.TodoApp
import cn.super12138.todo.constants.Constants
@ -35,7 +36,8 @@ import java.util.zip.ZipOutputStream
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel : ViewModel() {
val topLevelBackStack = TopLevelBackStack<NavKey>(startKey = TodoScreen.Overview)
val mainBackStack = TopLevelBackStack<NavKey>(TodoScreen.Overview)
val settingsBackStack = NavBackStack<NavKey>(TodoScreen.Settings.Main)
// 待办
private val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()