feat(main, editor): 更加丝滑的 FAB 过渡动画
This commit is contained in:
parent
3ce8e5befa
commit
4d432935b3
5 changed files with 168 additions and 127 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue