feat: 使用 Navigation3 导航 & 对 FAB 动画进行 MD3E 适配

This commit is contained in:
Super12138 2025-11-08 14:28:49 +08:00
parent 66e9ab82d2
commit 0fb4192493
11 changed files with 172 additions and 164 deletions

View file

@ -100,7 +100,6 @@ dependencies {
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.animation)
implementation(libs.androidx.navigation)
implementation(libs.androidx.material3.adaptive)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.android)
@ -109,6 +108,8 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icon.core)
implementation(libs.androidx.material.icon.extended)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
// About Libraries
implementation(libs.aboutlibraries.core)
implementation(libs.aboutlibraries.compose)

View file

@ -4,7 +4,9 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import cn.super12138.todo.constants.Constants
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = Constants.DB_TABLE_NAME)
data class TodoEntity(
@ColumnInfo(name = "content") val content: String,

View file

@ -17,11 +17,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import cn.super12138.todo.R
import cn.super12138.todo.utils.VibrationUtils
import com.kyant.capsule.ContinuousRoundedRectangle
@Composable
fun ConfirmDialog(

View file

@ -4,10 +4,11 @@ import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
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.settings.SettingsAbout
@ -19,8 +20,7 @@ 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.materialSharedAxisXIn
import cn.super12138.todo.ui.theme.materialSharedAxisXOut
import cn.super12138.todo.ui.theme.materialSharedAxisX
import cn.super12138.todo.ui.viewmodels.MainViewModel
private const val INITIAL_OFFSET_FACTOR = 0.10f
@ -29,126 +29,117 @@ private const val INITIAL_OFFSET_FACTOR = 0.10f
@Composable
fun TodoNavigation(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = TodoScreen.Main.name,
backStack: NavBackStack<NavKey> = rememberNavBackStack(TodoScreen.Main),
viewModel: MainViewModel
) {
fun onBack() {
backStack.removeAt(backStack.lastIndex)
}
SharedTransitionLayout {
NavHost(
navController = navController,
startDestination = startDestination,
enterTransition = {
materialSharedAxisXIn(
initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }
)
NavDisplay(
backStack = backStack,
onBack = ::onBack,
transitionSpec = {
materialSharedAxisX(
initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() })
},
exitTransition = {
materialSharedAxisXOut(
targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }
)
popTransitionSpec = {
materialSharedAxisX(
initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() })
},
popEnterTransition = {
materialSharedAxisXIn(
initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }
)
entryProvider = entryProvider {
entry<TodoScreen.Main> {
MainPage(
viewModel = viewModel,
toTodoEditPage = { backStack.add(TodoScreen.Editor(it)) },
toSettingsPage = { backStack.add(TodoScreen.SettingsMain) },
sharedTransitionScope = this@SharedTransitionLayout
)
}
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.SettingsMain> {
SettingsMain(
toAppearancePage = { backStack.add(TodoScreen.SettingsAppearance) },
toAboutPage = { backStack.add(TodoScreen.SettingsAbout) },
toInterfacePage = { backStack.add(TodoScreen.SettingsInterface) },
toDataPage = { backStack.add(TodoScreen.SettingsData) },
onNavigateUp = ::onBack,
)
}
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)
}
},
popExitTransition = {
materialSharedAxisXOut(
targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }
)
},
modifier = modifier
) {
composable(TodoScreen.Main.name) {
MainPage(
viewModel = viewModel,
toTodoEditPage = { navController.navigate(TodoScreen.TodoEditor.name) },
toSettingsPage = { navController.navigate(TodoScreen.SettingsMain.name) },
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@composable
)
}
composable(TodoScreen.TodoEditor.name) {
TodoEditorPage(
toDo = viewModel.selectedEditTodo,
onSave = {
viewModel.addTodo(it)
// 如果原来的待办状态为未完成并且修改后状态为完成
if (viewModel.selectedEditTodo?.isCompleted != true && it.isCompleted) {
viewModel.playConfetti()
}
navController.navigateUp()
},
onDelete = {
if (viewModel.selectedEditTodo !== null) {
viewModel.deleteTodo(viewModel.selectedEditTodo!!)
viewModel.setEditTodoItem(null)
}
navController.navigateUp()
},
onNavigateUp = { navController.navigateUp() },
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@composable
)
}
composable(TodoScreen.SettingsMain.name) {
SettingsMain(
toAppearancePage = { navController.navigate(TodoScreen.SettingsAppearance.name) },
toAboutPage = { navController.navigate(TodoScreen.SettingsAbout.name) },
toInterfacePage = { navController.navigate(TodoScreen.SettingsInterface.name) },
toDataPage = { navController.navigate(TodoScreen.SettingsData.name) },
onNavigateUp = { navController.navigateUp() },
)
}
composable(TodoScreen.SettingsAppearance.name) {
SettingsAppearance(onNavigateUp = { navController.navigateUp() })
}
composable(TodoScreen.SettingsInterface.name) {
SettingsInterface(onNavigateUp = { navController.navigateUp() })
}
composable(TodoScreen.SettingsData.name) {
SettingsData(
viewModel = viewModel,
toCategoryManager = { navController.navigate(TodoScreen.SettingsDataCategory.name) },
onNavigateUp = { navController.navigateUp() }
)
}
composable(TodoScreen.SettingsDataCategory.name) {
SettingsDataCategory(onNavigateUp = { navController.navigateUp() })
}
composable(TodoScreen.SettingsAbout.name) {
SettingsAbout(
//toSpecialPage = { navController.navigate(TodoScreen.SettingsAboutSpecial.name) },
toLicencePage = { navController.navigate(TodoScreen.SettingsAboutLicence.name) },
toDevPage = { navController.navigate(TodoScreen.SettingsDev.name) },
onNavigateUp = { navController.navigateUp() },
)
}
/*composable(TodoScreen.SettingsAboutSpecial.name) {
SettingsAboutSpecial(viewModel = viewModel)
}*/
composable(TodoScreen.SettingsAboutLicence.name) {
SettingsAboutLicence(onNavigateUp = { navController.navigateUp() })
}
composable(TodoScreen.SettingsDev.name) {
SettingsDeveloperOptions(
toPaddingPage = { navController.navigate(TodoScreen.SettingsDevPadding.name) },
onNavigateUp = { navController.navigateUp() }
)
}
composable(TodoScreen.SettingsDevPadding.name) {
SettingsDeveloperOptionsPadding(onNavigateUp = { navController.navigateUp() })
}
}
modifier = modifier,
)
}
}

View file

@ -1,17 +1,44 @@
package cn.super12138.todo.ui.navigation
enum class TodoScreen {
Main,
TodoEditor,
SettingsMain,
SettingsAppearance,
SettingsInterface,
SettingsData,
SettingsDataCategory,
SettingsAbout,
import androidx.navigation3.runtime.NavKey
import cn.super12138.todo.logic.database.TodoEntity
import kotlinx.serialization.Serializable
//SettingsAboutSpecial,
SettingsAboutLicence,
SettingsDev,
SettingsDevPadding
@Serializable
sealed class TodoScreen : NavKey {
@Serializable
data object Main : TodoScreen()
@Serializable
data class Editor(val toDo: TodoEntity?) : TodoScreen()
@Serializable
data object SettingsMain : TodoScreen()
@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()
}

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.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.expandVertically
@ -43,6 +42,7 @@ 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
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.database.TodoEntity
@ -69,11 +69,11 @@ fun TodoEditorPage(
onSave: (TodoEntity) -> Unit,
onDelete: () -> Unit,
onNavigateUp: () -> Unit,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope
sharedTransitionScope: SharedTransitionScope
) {
// TODO: 本页及其相关组件重组性能检查优化
val view = LocalView.current
val animatedVisibilityScope = LocalNavAnimatedContentScope.current
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val uiState = rememberEditorState(initialTodo = toDo)

View file

@ -74,7 +74,7 @@ class EditorState(val initialTodo: TodoEntity? = null) {
* 保存状态的 Saver 对象用于适配 rememberSaveable
*/
object Saver : androidx.compose.runtime.saveable.Saver<EditorState, Any> {
override fun SaverScope.save(value: EditorState): Any? {
override fun SaverScope.save(value: EditorState): Any {
return listOf(
value.initialTodo?.id ?: 0,
value.toDoContent,
@ -89,7 +89,7 @@ class EditorState(val initialTodo: TodoEntity? = null) {
)
}
override fun restore(value: Any): EditorState? {
override fun restore(value: Any): EditorState {
val list = value as List<*>
val initialTodo = list[0] as? TodoEntity?
return EditorState(initialTodo).apply {

View file

@ -1,7 +1,6 @@
package cn.super12138.todo.ui.pages.main
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.layout.Column
@ -31,9 +30,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import androidx.window.core.layout.WindowSizeClass
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.components.ConfirmDialog
import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar
@ -44,12 +45,13 @@ import cn.super12138.todo.ui.viewmodels.MainViewModel
fun MainPage(
modifier: Modifier = Modifier,
viewModel: MainViewModel,
toTodoEditPage: () -> Unit,
toTodoEditPage: (TodoEntity?) -> Unit,
toSettingsPage: () -> Unit,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope,
windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass,
) {
val animatedVisibilityScope = LocalNavAnimatedContentScope.current
val toDos = viewModel.sortedTodos.collectAsState(initial = emptyList())
val selectedTodos = viewModel.selectedTodoIds.collectAsState()
val showCompleted by DataStoreManager.showCompletedFlow.collectAsState(initial = Constants.PREF_SHOW_COMPLETED_DEFAULT)
@ -91,10 +93,7 @@ fun MainPage(
)
},
expanded = expandedFab,
onClick = {
viewModel.setEditTodoItem(null) // 每次添加待办前清除上一次已选待办
toTodoEditPage()
},
onClick = { toTodoEditPage(null) },
modifier = Modifier
.sharedElement(
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
@ -133,8 +132,7 @@ fun MainPage(
if (inSelectedMode) {
viewModel.toggleTodoSelection(item)
} else {
viewModel.setEditTodoItem(item)
toTodoEditPage()
toTodoEditPage(item)
}
},
onItemLongClick = { viewModel.toggleTodoSelection(it) },
@ -171,8 +169,7 @@ fun MainPage(
if (inSelectedMode) {
viewModel.toggleTodoSelection(item)
} else {
viewModel.setEditTodoItem(item)
toTodoEditPage()
toTodoEditPage(item)
}
},
onItemLongClick = { viewModel.toggleTodoSelection(it) },

View file

@ -2,7 +2,6 @@ package cn.super12138.todo.ui.pages.settings
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn

View file

@ -2,9 +2,7 @@ package cn.super12138.todo.ui.viewmodels
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cn.super12138.todo.TodoApp
@ -51,8 +49,6 @@ class MainViewModel : ViewModel() {
}
val showConfetti = mutableStateOf(false)
var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
private set
// 多选逻辑参考https://github.com/X1nto/Mauth
private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
@ -82,10 +78,6 @@ class MainViewModel : ViewModel() {
}
}*/
fun setEditTodoItem(toDo: TodoEntity?) {
selectedEditTodo = toDo
}
/**
* 切换待办的选择状态
*/

View file

@ -10,9 +10,9 @@ composeBom = "2025.11.00"
liveData = "1.9.4"
icons = "1.7.8"
room = "2.8.3"
navigation = "2.9.6"
material3 = "1.5.0-alpha08"
adaptive = "1.2.0"
nav3Core = "1.0.0-rc01"
# AboutLibraries
aboutLibsRelease = "13.1.0"
aboutLibsReleasePlugin = "13.1.0"
@ -49,7 +49,6 @@ androidx-datastore-preferences = { group = "androidx.datastore", name = "datasto
# Compose
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-animation = { group = "androidx.compose.animation", name = "animation" }
androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
androidx-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android" }
@ -61,6 +60,8 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
androidx-material-icon-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "icons" }
androidx-material-icon-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "icons" }
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
# Room
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }