feat(main, editor): 更加丝滑的 FAB 过渡动画

This commit is contained in:
Super12138 2025-02-05 21:41:38 +08:00
parent 3ce8e5befa
commit 4d432935b3
5 changed files with 168 additions and 127 deletions

View file

@ -4,6 +4,8 @@ object Constants {
const val DEVELOPER_GITHUB = "https://github.com/Super12138/" const val DEVELOPER_GITHUB = "https://github.com/Super12138/"
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 SP_NAME = "cn.super12138.todo_preferences" const val SP_NAME = "cn.super12138.todo_preferences"
const val PREF_DYNAMIC_COLOR = "dynamic_color" const val PREF_DYNAMIC_COLOR = "dynamic_color"

View file

@ -1,5 +1,7 @@
package cn.super12138.todo.ui.navigation package cn.super12138.todo.ui.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
@ -19,6 +21,7 @@ import cn.super12138.todo.ui.viewmodels.MainViewModel
private const val INITIAL_OFFSET_FACTOR = 0.10f private const val INITIAL_OFFSET_FACTOR = 0.10f
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun TodoNavigation( fun TodoNavigation(
navController: NavHostController = rememberNavController(), navController: NavHostController = rememberNavController(),
@ -26,95 +29,101 @@ fun TodoNavigation(
viewModel: MainViewModel, viewModel: MainViewModel,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
NavHost( SharedTransitionLayout {
navController = navController, NavHost(
startDestination = startDestination, navController = navController,
enterTransition = { startDestination = startDestination,
materialSharedAxisXIn( enterTransition = {
initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() } materialSharedAxisXIn(
) initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }
}, )
exitTransition = { },
materialSharedAxisXOut( exitTransition = {
targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() } materialSharedAxisXOut(
) targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }
}, )
popEnterTransition = { },
materialSharedAxisXIn( popEnterTransition = {
initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() } materialSharedAxisXIn(
) initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }
}, )
popExitTransition = { },
materialSharedAxisXOut( popExitTransition = {
targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() } materialSharedAxisXOut(
) targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }
}, )
modifier = modifier },
) { modifier = modifier
composable(TodoScreen.Main.name) { ) {
MainPage( composable(TodoScreen.Main.name) {
viewModel = viewModel, MainPage(
toTodoEditPage = { navController.navigate(TodoScreen.TodoEditor.name) }, viewModel = viewModel,
toSettingsPage = { navController.navigate(TodoScreen.SettingsMain.name) } toTodoEditPage = { navController.navigate(TodoScreen.TodoEditor.name) },
) toSettingsPage = { navController.navigate(TodoScreen.SettingsMain.name) },
} sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@composable
)
}
composable(TodoScreen.TodoEditor.name) { composable(TodoScreen.TodoEditor.name) {
TodoEditorPage( TodoEditorPage(
toDo = viewModel.selectedEditTodo, toDo = viewModel.selectedEditTodo,
onSave = { onSave = {
viewModel.addTodo(it) viewModel.addTodo(it)
viewModel.setEditTodoItem(null)
navController.navigateUp()
},
onDelete = {
if (viewModel.selectedEditTodo !== null) {
viewModel.deleteTodo(viewModel.selectedEditTodo!!)
viewModel.setEditTodoItem(null) viewModel.setEditTodoItem(null)
} navController.navigateUp()
navController.navigateUp() },
}, onDelete = {
onNavigateUp = { if (viewModel.selectedEditTodo !== null) {
navController.navigateUp() viewModel.deleteTodo(viewModel.selectedEditTodo!!)
viewModel.setEditTodoItem(null) viewModel.setEditTodoItem(null)
} }
) navController.navigateUp()
} },
onNavigateUp = {
navController.navigateUp()
viewModel.setEditTodoItem(null)
},
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@composable
)
}
composable(TodoScreen.SettingsMain.name) { composable(TodoScreen.SettingsMain.name) {
SettingsMain( SettingsMain(
toAppearancePage = { navController.navigate(TodoScreen.SettingsAppearance.name) }, toAppearancePage = { navController.navigate(TodoScreen.SettingsAppearance.name) },
toAboutPage = { navController.navigate(TodoScreen.SettingsAbout.name) }, toAboutPage = { navController.navigate(TodoScreen.SettingsAbout.name) },
toInterfacePage = { navController.navigate(TodoScreen.SettingsInterface.name) }, toInterfacePage = { navController.navigate(TodoScreen.SettingsInterface.name) },
onNavigateUp = { navController.navigateUp() }, onNavigateUp = { navController.navigateUp() },
) )
} }
composable(TodoScreen.SettingsAppearance.name) { composable(TodoScreen.SettingsAppearance.name) {
SettingsAppearance( SettingsAppearance(
viewModel = viewModel, viewModel = viewModel,
onNavigateUp = { navController.navigateUp() } onNavigateUp = { navController.navigateUp() }
) )
} }
composable(TodoScreen.SettingsInterface.name) { composable(TodoScreen.SettingsInterface.name) {
SettingsInterface( SettingsInterface(
viewModel = viewModel, viewModel = viewModel,
onNavigateUp = { navController.navigateUp() } onNavigateUp = { navController.navigateUp() }
) )
} }
composable(TodoScreen.SettingsAbout.name) { composable(TodoScreen.SettingsAbout.name) {
SettingsAbout( SettingsAbout(
onNavigateUp = { navController.navigateUp() }, onNavigateUp = { navController.navigateUp() },
toLicencePage = { navController.navigate(TodoScreen.SettingsAboutLicence.name) } toLicencePage = { navController.navigate(TodoScreen.SettingsAboutLicence.name) }
) )
} }
composable(TodoScreen.SettingsAboutLicence.name) { composable(TodoScreen.SettingsAboutLicence.name) {
SettingsAboutLicence( SettingsAboutLicence(
onNavigateUp = { navController.navigateUp() } onNavigateUp = { navController.navigateUp() }
) )
}
} }
} }
} }

View file

@ -2,6 +2,9 @@ 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.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -43,6 +46,7 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.R import cn.super12138.todo.R
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.model.Priority import cn.super12138.todo.logic.model.Priority
import cn.super12138.todo.logic.model.Subjects import cn.super12138.todo.logic.model.Subjects
@ -51,13 +55,15 @@ import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.FilterChipGroup import cn.super12138.todo.ui.components.FilterChipGroup
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun TodoEditorPage( fun TodoEditorPage(
toDo: TodoEntity? = null, toDo: TodoEntity? = null,
onSave: (TodoEntity) -> Unit, onSave: (TodoEntity) -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
BackHandler { BackHandler {
@ -75,48 +81,54 @@ fun TodoEditorPage(
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),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
floatingActionButton = { floatingActionButton = {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { with(sharedTransitionScope) {
if (toDo !== null) { Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
if (toDo !== null) {
AnimatedExtendedFloatingActionButton(
icon = {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.action_delete)
)
},
text = { Text(stringResource(R.string.action_delete)) },
expanded = true,
containerColor = MaterialTheme.colorScheme.errorContainer,
onClick = onDelete
)
}
AnimatedExtendedFloatingActionButton( AnimatedExtendedFloatingActionButton(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Outlined.Delete, imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.action_delete) contentDescription = stringResource(R.string.action_save)
) )
}, },
text = { Text(stringResource(R.string.action_delete)) }, text = { Text(stringResource(R.string.action_save)) },
expanded = true, expanded = true,
containerColor = MaterialTheme.colorScheme.errorContainer, onClick = {
onClick = onDelete if (text.trim().isEmpty()) {
isError = true
return@AnimatedExtendedFloatingActionButton
}
isError = false
onSave(
TodoEntity(
content = text,
subject = selectedItemIndex,
isCompleted = toDo?.isCompleted ?: false,
priority = sliderPosition,
id = toDo?.id ?: 0
)
)
},
modifier = Modifier.sharedElement(
state = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = animatedVisibilityScope
)
) )
} }
AnimatedExtendedFloatingActionButton(
icon = {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.action_save)
)
},
text = { Text(stringResource(R.string.action_save)) },
expanded = true,
onClick = {
if (text.trim().isEmpty()) {
isError = true
return@AnimatedExtendedFloatingActionButton
}
isError = false
onSave(
TodoEntity(
content = text,
subject = selectedItemIndex,
isCompleted = toDo?.isCompleted ?: false,
priority = sliderPosition,
id = toDo?.id ?: 0
)
)
}
)
} }
}, },
onBack = onNavigateUp, onBack = onNavigateUp,

View file

@ -2,6 +2,9 @@ package cn.super12138.todo.ui.pages.main
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.expandIn import androidx.compose.animation.expandIn
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -27,17 +30,21 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.window.core.layout.WindowWidthSizeClass import androidx.window.core.layout.WindowWidthSizeClass
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.database.TodoEntity import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.ui.pages.main.components.TodoDeleteConfirmDialog import cn.super12138.todo.ui.pages.main.components.TodoDeleteConfirmDialog
import cn.super12138.todo.ui.pages.main.components.TodoFAB import cn.super12138.todo.ui.pages.main.components.TodoFAB
import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar
import cn.super12138.todo.ui.viewmodels.MainViewModel import cn.super12138.todo.ui.viewmodels.MainViewModel
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun MainPage( fun MainPage(
viewModel: MainViewModel, viewModel: MainViewModel,
toTodoEditPage: () -> Unit, toTodoEditPage: () -> Unit,
toSettingsPage: () -> Unit, toSettingsPage: () -> Unit,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val toDoList = viewModel.toDos.collectAsState(initial = emptyList()) val toDoList = viewModel.toDos.collectAsState(initial = emptyList())
@ -80,15 +87,21 @@ fun MainPage(
) )
}, },
floatingActionButton = { floatingActionButton = {
AnimatedVisibility( with(sharedTransitionScope) {
visible = isSelectedIdsEmpty, AnimatedVisibility(
enter = fadeIn() + expandIn(), visible = isSelectedIdsEmpty,
exit = shrinkOut() + fadeOut() enter = fadeIn() + expandIn(),
) { exit = shrinkOut() + fadeOut()
TodoFAB( ) {
expanded = isExpanded, TodoFAB(
onClick = toTodoEditPage expanded = isExpanded,
) onClick = toTodoEditPage,
modifier = Modifier.sharedElement(
state = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = animatedVisibilityScope
)
)
}
} }
}, },
contentWindowInsets = WindowInsets.safeContent.exclude(WindowInsets.ime), contentWindowInsets = WindowInsets.safeContent.exclude(WindowInsets.ime),

View file

@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
@ -33,7 +34,11 @@ fun TodoFAB(
) )
}, },
text = { text = {
Text(stringResource(R.string.action_add_task)) Text(
text = stringResource(R.string.action_add_task),
maxLines = 1,
overflow = TextOverflow.Clip
)
}, },
expanded = expanded, expanded = expanded,
elevation = elevation, elevation = elevation,