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" applicationId = "cn.super12138.todo"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 941 versionCode = 950
versionName = "2.3.3" versionName = "2.3.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View file

@ -5,8 +5,7 @@ object Constants {
const val GITHUB_REPO = "https://github.com/Super12138/ToDo/" const val GITHUB_REPO = "https://github.com/Super12138/ToDo/"
const val KEY_TODO_FAB_TRANSITION = "todo_fab" const val KEY_TODO_FAB_TRANSITION = "todo_fab"
// const val KEY_TODO_CONTENT_TRANSITION = "todo_content" const val KEY_TODO_ITEM_TRANSITION = "todo_item_id"
// const val KEY_TODO_CATEGORY_TRANSITION = "todo_category"
const val DB_NAME = "todo" const val DB_NAME = "todo"
const val DB_TABLE_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 cn.super12138.todo.logic.database.TodoEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
object Repository { object Repository : IRepository {
private val db get() = TodoApp.db private val db get() = TodoApp.db
private val toDoDao = db.toDoDao() private val toDoDao = db.toDoDao()
suspend fun insertTodo(toDo: TodoEntity) { override suspend fun insertTodo(toDo: TodoEntity) {
toDoDao.insert(toDo) 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) toDoDao.update(toDo)
} }
suspend fun deleteTodo(toDo: TodoEntity) { override suspend fun deleteTodo(toDo: TodoEntity) {
toDoDao.delete(toDo) toDoDao.delete(toDo)
} }
suspend fun deleteTodoFromIds(toDoItems: List<Int>) { override suspend fun deleteTodoFromIds(toDoItems: List<Int>) {
toDoDao.deleteFromIds(toDoItems.toSet()) toDoDao.deleteFromIds(toDoItems.toSet())
} }
/*suspend fun deleteAllTodo() { /*override suspend fun deleteAllTodo() {
toDoDao.deleteAllTodo() toDoDao.deleteAllTodo()
}*/ }*/
} }

View file

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

View file

@ -6,8 +6,8 @@ import cn.super12138.todo.R
enum class DarkMode( enum class DarkMode(
val id: Int, val id: Int,
@DrawableRes val iconRes: Int, @param:DrawableRes val iconRes: Int,
@StringRes val nameRes: Int @param:StringRes val nameRes: Int
) { ) {
FollowSystem(id = -1, iconRes = R.drawable.ic_lightbulb_2, nameRes = R.string.dark_mode_system), 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), 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( enum class PaletteStyle(
val id: Int, val id: Int,
@StringRes val nameRes: Int @param:StringRes val nameRes: Int
) { ) {
TonalSpot(id = 1, nameRes = R.string.palette_tonal_spot), TonalSpot(id = 1, nameRes = R.string.palette_tonal_spot),
Neutral(id = 2, nameRes = R.string.palette_neutral), Neutral(id = 2, nameRes = R.string.palette_neutral),

View file

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

View file

@ -5,7 +5,7 @@ import cn.super12138.todo.R
enum class SortingMethod( enum class SortingMethod(
val id: Int, val id: Int,
@StringRes val nameRes: Int @param:StringRes val nameRes: Int
) { ) {
// 按添加先后顺序 // 按添加先后顺序
Sequential(id = 1, nameRes = R.string.sorting_sequential), 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 android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Icon 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.TodoDefaults
import cn.super12138.todo.ui.components.Konfetti import cn.super12138.todo.ui.components.Konfetti
import cn.super12138.todo.ui.navigation.TodoDestinations 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.theme.ToDoTheme
import cn.super12138.todo.ui.viewmodels.MainViewModel import cn.super12138.todo.ui.viewmodels.MainViewModel
import cn.super12138.todo.utils.VibrationUtils import cn.super12138.todo.utils.VibrationUtils
@ -39,7 +40,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
val mainViewModel: MainViewModel = viewModel() val mainViewModel: MainViewModel = viewModel()
val navigationBackStack = mainViewModel.topLevelBackStack val mainBackStack = mainViewModel.mainBackStack
val showConfetti = mainViewModel.showConfetti val showConfetti = mainViewModel.showConfetti
// 主题 // 主题
val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT) val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT)
@ -89,27 +90,34 @@ class MainActivity : ComponentActivity() {
Surface(color = TodoDefaults.BackgroundColor) { Surface(color = TodoDefaults.BackgroundColor) {
NavigationSuiteScaffold( NavigationSuiteScaffold(
navigationSuiteItems = { navigationSuiteItems = {
TodoDestinations.entries.forEach { TodoDestinations.entries.forEach { destination ->
val selected = it.route == navigationBackStack.topLevelKey val selected = destination.route == mainBackStack.topLevelKey
item( item(
icon = { icon = {
Icon( Crossfade(selected) {
painter = if (selected) painterResource(it.selectedIcon) else painterResource( if (it) {
it.icon Icon(
), painter = painterResource(destination.selectedIcon),
contentDescription = null contentDescription = null
) )
} else {
Icon(
painter = painterResource(destination.icon),
contentDescription = null
)
}
}
}, },
label = { Text(stringResource(it.label)) }, label = { Text(stringResource(destination.label)) },
selected = selected, selected = selected,
onClick = { navigationBackStack.addTopLevel(it.route) } onClick = { mainBackStack.addTopLevel(destination.route) }
) )
} }
}, },
containerColor = TodoDefaults.BackgroundColor containerColor = TodoDefaults.BackgroundColor
) { ) {
TodoNavigation( TopNavigation(
backStack = navigationBackStack, backStack = mainBackStack,
viewModel = mainViewModel, viewModel = mainViewModel,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )

View file

@ -1,2 +1,105 @@
package cn.super12138.todo.ui.navigation 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( enum class TodoDestinations(
val route: NavKey, val route: NavKey,
@StringRes val label: Int, @param:StringRes val label: Int,
@DrawableRes val icon: Int, @param:DrawableRes val icon: Int,
@DrawableRes val selectedIcon: Int @param:DrawableRes val selectedIcon: Int
) { ) {
Overview( Overview(
route = TodoScreen.Overview, route = TodoScreen.Overview,
@ -23,7 +23,7 @@ enum class TodoDestinations(
selectedIcon = cn.super12138.todo.R.drawable.ic_ballot_filled selectedIcon = cn.super12138.todo.R.drawable.ic_ballot_filled
), ),
Settings( Settings(
route = TodoScreen.Settings, route = TodoScreen.Settings.Main,
label = cn.super12138.todo.R.string.page_settings, label = cn.super12138.todo.R.string.page_settings,
icon = cn.super12138.todo.R.drawable.ic_settings, icon = cn.super12138.todo.R.drawable.ic_settings,
selectedIcon = cn.super12138.todo.R.drawable.ic_settings_filled 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() data object Tasks : TodoScreen()
@Serializable @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 @Serializable
data object Settings : TodoScreen() sealed class Editor : TodoScreen() {
@Serializable
data object Add : Editor()
@Serializable @Serializable
data object SettingsAppearance : TodoScreen() data class Edit(val toDo: TodoEntity) : Editor()
}
@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()
} }

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.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn 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.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -41,6 +37,7 @@ 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.datastore.DataStoreManager
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.ChipItem import cn.super12138.todo.ui.components.ChipItem
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
@ -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.ui.pages.editor.state.rememberEditorState
import cn.super12138.todo.utils.VibrationUtils import cn.super12138.todo.utils.VibrationUtils
@OptIn(
ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class,
ExperimentalMaterial3ExpressiveApi::class
)
@Composable @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, modifier: Modifier = Modifier,
toDo: TodoEntity? = null, toDo: TodoEntity? = null,
onSave: (TodoEntity) -> Unit, onSave: (TodoEntity) -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit
sharedTransitionScope: SharedTransitionScope
) { ) {
// TODO: 本页及其相关组件重组性能检查优化 // TODO: 本页及其相关组件重组性能检查优化
val view = LocalView.current val view = LocalView.current
val animatedVisibilityScope = LocalNavAnimatedContentScope.current
TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val uiState = rememberEditorState(initialTodo = toDo) val uiState = rememberEditorState(initialTodo = toDo)
@ -114,7 +138,6 @@ fun TodoEditorPage(
TopAppBarScaffold( TopAppBarScaffold(
title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task), title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task),
floatingActionButton = { floatingActionButton = {
with(sharedTransitionScope) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.imePadding() modifier = Modifier.imePadding()
@ -146,15 +169,9 @@ fun TodoEditorPage(
) )
onSave(newTodo) onSave(newTodo)
} }
}, }
modifier = Modifier
.sharedElement(
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = animatedVisibilityScope
)
) )
} }
}
}, },
onBack = ::checkModifiedBeforeBack, onBack = ::checkModifiedBeforeBack,
modifier = modifier modifier = modifier
@ -163,6 +180,10 @@ fun TodoEditorPage(
verticalArrangement = Arrangement.spacedBy(5.dp), verticalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
item {
Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
}
item { item {
TodoContentTextField( TodoContentTextField(
value = uiState.toDoContent, value = uiState.toDoContent,
@ -241,7 +262,7 @@ fun TodoEditorPage(
} }
item { 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 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.Card
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -11,13 +12,14 @@ import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.ui.components.TopAppBarScaffold import cn.super12138.todo.ui.components.TopAppBarScaffold
@OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
fun OverviewPage(modifier: Modifier = Modifier) { fun OverviewPage(modifier: Modifier = Modifier) {
TopAppBarScaffold( TopAppBarScaffold(
title = stringResource(R.string.page_overview), title = stringResource(R.string.page_overview),
modifier = modifier modifier = modifier
) { ) {
Row { Column {
Card { Card {
Icon(painter = painterResource(R.drawable.ic_ballot), contentDescription = null) Icon(painter = painterResource(R.drawable.ic_ballot), contentDescription = null)
Text("总任务") Text("总任务")

View file

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

View file

@ -23,13 +23,13 @@ fun SettingsContainer(
modifier = modifier modifier = modifier
) { ) {
item { item {
Spacer(modifier = Modifier.size(TodoDefaults.settingsItemVerticalPadding)) Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
} }
content() content()
item { 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.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState 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.activity.compose.BackHandler
import androidx.compose.animation.SharedTransitionScope 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.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.main.components.TodoCard import cn.super12138.todo.ui.pages.tasks.components.TodoCard
import cn.super12138.todo.ui.pages.main.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
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun MainPage( fun SharedTransitionScope.TasksPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: MainViewModel, viewModel: MainViewModel,
toTodoEditPage: (TodoEntity?) -> Unit, toTodoAddPage: () -> Unit,
sharedTransitionScope: SharedTransitionScope, toTodoEditPage: (TodoEntity) -> Unit,
) { ) {
val animatedVisibilityScope = LocalNavAnimatedContentScope.current val animatedVisibilityScope = LocalNavAnimatedContentScope.current
@ -82,23 +82,21 @@ fun MainPage(
) )
}, },
floatingActionButton = { floatingActionButton = {
with(sharedTransitionScope) { TodoFloatingActionButton(
TodoFloatingActionButton( text = stringResource(R.string.action_add_task),
text = stringResource(R.string.action_add_task), iconRes = R.drawable.ic_add,
iconRes = R.drawable.ic_add, expanded = expandedFab,
expanded = expandedFab, onClick = { toTodoAddPage() },
onClick = { toTodoEditPage(null) }, modifier = Modifier
modifier = Modifier .sharedBounds(
.sharedElement( sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION), animatedVisibilityScope = animatedVisibilityScope
animatedVisibilityScope = animatedVisibilityScope )
) .animateFloatingActionButton(
.animateFloatingActionButton( visible = !inSelectedMode,
visible = !inSelectedMode, alignment = Alignment.BottomEnd,
alignment = Alignment.BottomEnd, )
) )
)
}
}, },
contentWindowInsets = WindowInsets(0, 0, 0, 0), contentWindowInsets = WindowInsets(0, 0, 0, 0),
modifier = modifier modifier = modifier
@ -109,7 +107,7 @@ fun MainPage(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
item { item {
Spacer(modifier = Modifier.size(TodoDefaults.settingsItemVerticalPadding)) Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding))
} }
if (filteredTodoList.isEmpty()) { if (filteredTodoList.isEmpty()) {
@ -146,6 +144,10 @@ fun MainPage(
viewModel.playConfetti() viewModel.playConfetti()
}, },
modifier = Modifier modifier = Modifier
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${it.id}"),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
.animateItem( .animateItem(
fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(),
placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
@ -154,7 +156,7 @@ fun MainPage(
) )
} }
item { 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.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@ -21,7 +19,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Badge import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox 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.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -32,7 +31,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.VibrationUtils
import cn.super12138.todo.utils.containerColor import cn.super12138.todo.utils.containerColor
@OptIn( @OptIn(ExperimentalMaterial3ExpressiveApi::class)
ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class,
ExperimentalMaterial3ExpressiveApi::class
)
@Composable @Composable
fun TodoCard( fun TodoCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -63,8 +58,8 @@ fun TodoCard(
onChecked: () -> Unit = {} onChecked: () -> Unit = {}
) { ) {
val view = LocalView.current val view = LocalView.current
val context = LocalContext.current Card(
ElevatedCard( colors = CardDefaults.cardColors(containerColor = TodoDefaults.ContainerColor),
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(TodoDefaults.toDoCardHeight) .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.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility

View file

@ -5,6 +5,7 @@ import android.net.Uri
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import cn.super12138.todo.TodoApp import cn.super12138.todo.TodoApp
import cn.super12138.todo.constants.Constants import cn.super12138.todo.constants.Constants
@ -35,7 +36,8 @@ import java.util.zip.ZipOutputStream
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel : ViewModel() { 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() private val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()