refactor: 优化导航栏的显示效果 & 为导航栏添加触感反馈

This commit is contained in:
Super12138 2026-02-07 23:20:49 +08:00
parent ddb02eb9a3
commit 2fbb6fd38c
10 changed files with 136 additions and 156 deletions

View file

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

View file

@ -11,10 +11,13 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldValue
import androidx.compose.material3.adaptive.navigationsuite.rememberNavigationSuiteScaffoldState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@ -40,8 +43,12 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
val mainViewModel: MainViewModel = viewModel()
val mainBackStack = mainViewModel.mainBackStack
val showConfetti = mainViewModel.showConfetti
val mainBackStack = mainViewModel.mainBackStack
val navigationScaffoldState = rememberNavigationSuiteScaffoldState()
// 主题
val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT)
val paletteStyle by DataStoreManager.paletteStyleFlow.collectAsState(initial = Constants.PREF_PALETTE_STYLE_DEFAULT)
@ -81,14 +88,30 @@ class MainActivity : ComponentActivity() {
VibrationUtils.setEnabled(hapticFeedback)
}
// 当BackStack出现非顶层路由时隐藏底部导航栏
LaunchedEffect(mainBackStack.backStack.lastOrNull()) {
val isTopLevel =
mainBackStack.backStack.lastOrNull() in TodoDestinations.entries.map { it.route }
if (isTopLevel) {
if (navigationScaffoldState.currentValue != NavigationSuiteScaffoldValue.Visible) navigationScaffoldState.show()
} else {
if (navigationScaffoldState.currentValue != NavigationSuiteScaffoldValue.Hidden) navigationScaffoldState.hide()
}
}
ToDoTheme(
darkTheme = darkTheme,
style = PaletteStyle.fromId(paletteStyle),
contrastLevel = contrastLevel.toDouble(),
dynamicColor = dynamicColor
) {
Surface(color = TodoDefaults.BackgroundColor) {
Surface(
color = TodoDefaults.BackgroundColor,
modifier = Modifier.fillMaxSize()
) {
val view = LocalView.current
NavigationSuiteScaffold(
state = navigationScaffoldState,
navigationSuiteItems = {
TodoDestinations.entries.forEach { destination ->
val selected = destination.route == mainBackStack.topLevelKey
@ -110,11 +133,15 @@ class MainActivity : ComponentActivity() {
},
label = { Text(stringResource(destination.label)) },
selected = selected,
onClick = { mainBackStack.addTopLevel(destination.route) }
onClick = {
VibrationUtils.performHapticFeedback(view)
mainBackStack.addTopLevel(destination.route)
}
)
}
},
containerColor = TodoDefaults.BackgroundColor
containerColor = TodoDefaults.BackgroundColor,
modifier = Modifier.fillMaxSize()
) {
TopNavigation(
backStack = mainBackStack,
@ -122,7 +149,10 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize()
)
}
Konfetti(state = showConfetti)
Konfetti(
state = showConfetti,
modifier = Modifier.fillMaxSize()
)
}
}
}

View file

@ -1,7 +1,6 @@
package cn.super12138.todo.ui.components
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@ -45,9 +44,9 @@ fun Konfetti(
}
}
KonfettiView(
modifier = Modifier
.fillMaxSize()
.then(modifier),
modifier = modifier
/*.fillMaxSize()
.then(modifier)*/,
parties = remember { particles(primary.toArgb()) },
updateListener = listener
)

View file

@ -1,105 +0,0 @@
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

@ -10,13 +10,39 @@ 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.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.pages.tasks.TasksPage
import cn.super12138.todo.ui.theme.fadeThrough
import cn.super12138.todo.ui.theme.materialSharedAxisX
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
*/
private const val INITIAL_OFFSET_FACTOR = 0.10f
private fun settingsTransition() = NavDisplay.transitionSpec {
materialSharedAxisX(
initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }
)
} + NavDisplay.popTransitionSpec {
materialSharedAxisX(
initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }
)
} + NavDisplay.predictivePopTransitionSpec {
materialSharedAxisX(
initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }
)
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
@ -28,29 +54,6 @@ fun TopNavigation(
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 {
@ -66,15 +69,12 @@ fun TopNavigation(
predictivePopTransitionSpec = {
unveilIn(initialColor = veilColor) togetherWith fadeOut()
},*/
transitionSpec = {
fadeThrough()
},
popTransitionSpec = {
fadeThrough()
},
predictivePopTransitionSpec = {
fadeThrough()
},
/**
* 来自https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/transition/MaterialFadeThrough.java#L33
*/
transitionSpec = { fadeThrough() },
popTransitionSpec = { fadeThrough() },
predictivePopTransitionSpec = { fadeThrough() },
entryProvider = entryProvider {
entry<TodoScreen.Overview> {
OverviewPage(viewModel = viewModel)
@ -118,11 +118,60 @@ fun TopNavigation(
}
entry<TodoScreen.Settings.Main> {
SettingsNavigation(
backStack = viewModel.settingsBackStack,
viewModel = viewModel
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>(metadata = settingsTransition()) {
SettingsAppearance(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.Interface>(metadata = settingsTransition()) {
SettingsInterface(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.Data>(metadata = settingsTransition()) {
SettingsData(
viewModel = viewModel,
toCategoryManager = { backStack.add(TodoScreen.Settings.DataCategory) },
onNavigateUp = ::onBack
)
}
entry<TodoScreen.Settings.DataCategory>(metadata = settingsTransition()) {
SettingsDataCategory(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.About>(metadata = settingsTransition()) {
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>(metadata = settingsTransition()) {
SettingsAboutSpecial(viewModel = viewModel)
}*/
entry<TodoScreen.Settings.AboutLicence>(metadata = settingsTransition()) {
SettingsAboutLicence(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.DeveloperOptions>(metadata = settingsTransition()) {
SettingsDeveloperOptions(
toPaddingPage = { backStack.add(TodoScreen.Settings.DeveloperOptionsPadding) },
onNavigateUp = ::onBack
)
}
entry<TodoScreen.Settings.DeveloperOptionsPadding>(metadata = settingsTransition()) {
SettingsDeveloperOptionsPadding(onNavigateUp = ::onBack)
}
},
modifier = modifier,
)

View file

@ -121,7 +121,7 @@ fun TodoDueDateChooser(
},
shapes = ButtonDefaults.shapes()
) {
Text("清除")
Text(stringResource(R.string.action_clear))
}
TextButton(
onClick = {

View file

@ -12,6 +12,13 @@ import cn.super12138.todo.R
import cn.super12138.todo.logic.database.TodoEntity
class EditorState(val initialTodo: TodoEntity? = null) {
/* companion object{
fun Saver() =
androidx.compose.runtime.saveable.Saver<EditorState, TodoEntity>(
save = { it.initialTodo },
restore = { EditorState(it) },
)
}*/
var toDoContent by mutableStateOf(initialTodo?.content ?: "")
var isErrorContent by mutableStateOf(false)
var selectedCategoryIndex by mutableIntStateOf(-1)

View file

@ -5,7 +5,6 @@ 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
@ -37,7 +36,6 @@ import java.util.zip.ZipOutputStream
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel : ViewModel() {
val mainBackStack = TopLevelBackStack<NavKey>(TodoScreen.Overview)
val settingsBackStack = NavBackStack<NavKey>(TodoScreen.Settings.Main)
// 待办
private val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()

View file

@ -117,4 +117,5 @@
<string name="page_tasks">待办</string>
<string name="page_overview">概览</string>
<string name="tip_short_category">尽量使用简短的分类名称</string>
<string name="action_clear">清除</string>
</resources>

View file

@ -118,4 +118,5 @@
<string name="page_tasks">Tasks</string>
<string name="page_overview">Overview</string>
<string name="tip_short_category">Keep category names concise</string>
<string name="action_clear">Clear</string>
</resources>