refactor: 优化 FAB 共享元素转场动画

This commit is contained in:
Super12138 2026-02-08 11:27:41 +08:00
parent fd2dbe7735
commit da0728d390
4 changed files with 80 additions and 31 deletions

View file

@ -2,6 +2,7 @@ package cn.super12138.todo.ui.navigation
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
@ -22,28 +23,9 @@ import cn.super12138.todo.ui.pages.settings.SettingsMain
import cn.super12138.todo.ui.pages.tasks.TasksPage import cn.super12138.todo.ui.pages.tasks.TasksPage
import cn.super12138.todo.ui.theme.fadeThrough import cn.super12138.todo.ui.theme.fadeThrough
import cn.super12138.todo.ui.theme.materialSharedAxisX import cn.super12138.todo.ui.theme.materialSharedAxisX
import cn.super12138.todo.ui.theme.veilFade
import cn.super12138.todo.ui.viewmodels.MainViewModel import cn.super12138.todo.ui.viewmodels.MainViewModel
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) @OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
fun TopNavigation( fun TopNavigation(
@ -55,7 +37,33 @@ fun TopNavigation(
backStack.removeLast() backStack.removeLast()
} }
// val veilColor = MaterialTheme.colorScheme.surfaceDim val veilColor = MaterialTheme.colorScheme.surfaceDim
fun editorTransition() = NavDisplay.transitionSpec {
veilFade(veilColor)
} + NavDisplay.popTransitionSpec {
veilFade(veilColor)
} + NavDisplay.predictivePopTransitionSpec {
veilFade(veilColor)
}
val initialOffestFactor = 0.10f
fun settingsTransition() = NavDisplay.transitionSpec {
materialSharedAxisX(
initialOffsetX = { (it * initialOffestFactor).toInt() },
targetOffsetX = { -(it * initialOffestFactor).toInt() }
)
} + NavDisplay.popTransitionSpec {
materialSharedAxisX(
initialOffsetX = { -(it * initialOffestFactor).toInt() },
targetOffsetX = { (it * initialOffestFactor).toInt() }
)
} + NavDisplay.predictivePopTransitionSpec {
materialSharedAxisX(
initialOffsetX = { -(it * initialOffestFactor).toInt() },
targetOffsetX = { (it * initialOffestFactor).toInt() }
)
}
SharedTransitionLayout { SharedTransitionLayout {
NavDisplay( NavDisplay(
backStack = backStack.backStack, backStack = backStack.backStack,
@ -88,7 +96,7 @@ fun TopNavigation(
) )
} }
entry<TodoScreen.Editor.Add> { entry<TodoScreen.Editor.Add>(metadata = editorTransition()) {
TodoAddPage( TodoAddPage(
onSave = { onSave = {
viewModel.addTodo(it) viewModel.addTodo(it)
@ -98,7 +106,7 @@ fun TopNavigation(
) )
} }
entry<TodoScreen.Editor.Edit> { editorArgs -> entry<TodoScreen.Editor.Edit>(metadata = editorTransition()) { editorArgs ->
TodoEditPage( TodoEditPage(
toDo = editorArgs.toDo, toDo = editorArgs.toDo,
onSave = { onSave = {

View file

@ -53,10 +53,13 @@ fun SharedTransitionScope.TodoAddPage(
onNavigateUp: () -> Unit onNavigateUp: () -> Unit
) = TodoEditorPage( ) = TodoEditorPage(
toDo = null, toDo = null,
modifier = modifier.sharedBounds( modifier = modifier
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION), .sharedBounds(
animatedVisibilityScope = LocalNavAnimatedContentScope.current sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
), animatedVisibilityScope = LocalNavAnimatedContentScope.current,
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds
)
.skipToLookaheadSize(), // 这个修饰符必须放后面
onSave = onSave, onSave = onSave,
onDelete = {}, onDelete = {},
onNavigateUp = onNavigateUp onNavigateUp = onNavigateUp
@ -76,13 +79,14 @@ fun SharedTransitionScope.TodoEditPage(
animatedVisibilityScope = LocalNavAnimatedContentScope.current, animatedVisibilityScope = LocalNavAnimatedContentScope.current,
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds
), ),
//TODO: 没想好加不加 .skipToLookaheadSize(),
onSave = onSave, onSave = onSave,
onDelete = onDelete, onDelete = onDelete,
onNavigateUp = onNavigateUp onNavigateUp = onNavigateUp
) )
@Composable @Composable
fun TodoEditorPage( fun SharedTransitionScope.TodoEditorPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
toDo: TodoEntity? = null, toDo: TodoEntity? = null,
onSave: (TodoEntity) -> Unit, onSave: (TodoEntity) -> Unit,

View file

@ -27,7 +27,6 @@ import androidx.compose.ui.Alignment
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.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.zIndex
import androidx.navigation3.ui.LocalNavAnimatedContentScope import androidx.navigation3.ui.LocalNavAnimatedContentScope
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants import cn.super12138.todo.constants.Constants
@ -87,7 +86,8 @@ fun SharedTransitionScope.TasksPage(
modifier = Modifier modifier = Modifier
.sharedBounds( .sharedBounds(
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION), sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = animatedVisibilityScope animatedVisibilityScope = animatedVisibilityScope,
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds
) )
.animateFloatingActionButton( .animateFloatingActionButton(
visible = !inSelectedMode, visible = !inSelectedMode,

View file

@ -3,6 +3,7 @@ package cn.super12138.todo.ui.theme
import androidx.compose.animation.ContentTransform import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.LinearOutSlowInEasing
@ -12,6 +13,9 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.unveilIn
import androidx.compose.animation.veilOut
import androidx.compose.ui.graphics.Color
/** /**
* 来自https://github.com/Ashinch/ReadYou/blob/main/app/src/main/java/me/ash/reader/ui/motion/MaterialSharedAxis.kt * 来自https://github.com/Ashinch/ReadYou/blob/main/app/src/main/java/me/ash/reader/ui/motion/MaterialSharedAxis.kt
@ -115,3 +119,36 @@ fun fadeThroughOut(
easing = FastOutLinearInEasing easing = FastOutLinearInEasing
) )
) )
fun veilFade(
initialColor: Color,
durationMillis: Int = 200
): ContentTransform = ContentTransform(
veilFadeIn(
initialColor = initialColor,
durationMillis = durationMillis
),
veilFadeOut(
initialColor = initialColor,
durationMillis = durationMillis
)
)
@OptIn(ExperimentalAnimationApi::class)
fun veilFadeIn(
initialColor: Color,
durationMillis: Int = DefaultMotionDuration,
): EnterTransition =
fadeIn(
) + unveilIn(
initialColor = initialColor
)
@OptIn(ExperimentalAnimationApi::class)
fun veilFadeOut(
initialColor: Color,
durationMillis: Int = DefaultMotionDuration,
): ExitTransition =
fadeOut() + veilOut(
targetColor = initialColor
)