198 lines
7.6 KiB
Kotlin
198 lines
7.6 KiB
Kotlin
package plus.rua.project
|
||
|
||
import androidx.compose.animation.core.Animatable
|
||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||
import androidx.compose.animation.core.Spring
|
||
import androidx.compose.animation.core.spring
|
||
import androidx.compose.animation.core.tween
|
||
import androidx.compose.foundation.isSystemInDarkTheme
|
||
import androidx.compose.foundation.layout.Box
|
||
import androidx.compose.foundation.layout.fillMaxSize
|
||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||
import androidx.compose.material3.MaterialTheme
|
||
import androidx.compose.material3.darkColorScheme
|
||
import androidx.compose.material3.lightColorScheme
|
||
import androidx.compose.runtime.Composable
|
||
import androidx.compose.runtime.derivedStateOf
|
||
import androidx.compose.runtime.getValue
|
||
import androidx.compose.runtime.mutableFloatStateOf
|
||
import androidx.compose.runtime.mutableStateOf
|
||
import androidx.compose.runtime.remember
|
||
import androidx.compose.runtime.rememberCoroutineScope
|
||
import androidx.compose.runtime.setValue
|
||
import androidx.compose.ui.Modifier
|
||
import androidx.compose.ui.graphics.GraphicsLayerScope
|
||
import androidx.compose.ui.graphics.graphicsLayer
|
||
import androidx.compose.ui.tooling.preview.Preview
|
||
import androidx.compose.ui.unit.dp
|
||
import kotlinx.coroutines.launch
|
||
import plus.rua.project.ui.AboutScreen
|
||
import plus.rua.project.ui.CalendarMonthView
|
||
import plus.rua.project.ui.LicensesScreen
|
||
import plus.rua.project.ui.lerp
|
||
|
||
private enum class Screen { Main, About, Licenses }
|
||
|
||
/** 返回手势动画:顶层页面滑出 + 淡出 + 缩小 + 圆角阴影 */
|
||
private fun GraphicsLayerScope.applyDismissTransform(progress: Float) {
|
||
translationX = progress * size.width * 0.5f
|
||
scaleX = 1f - progress * 0.08f
|
||
scaleY = 1f - progress * 0.08f
|
||
alpha = 1f - progress
|
||
shadowElevation = 32.dp.toPx() * progress
|
||
shape = RoundedCornerShape(28.dp * progress)
|
||
clip = progress > 0.01f
|
||
}
|
||
|
||
/** 底层页面缩放:随返回进度从 baseScale 放大到 1.0 */
|
||
private fun GraphicsLayerScope.applyRevealTransform(
|
||
progress: Float,
|
||
forwardProgress: Float,
|
||
isForwardAnimating: Boolean
|
||
) {
|
||
val baseScale = 0.92f + 0.08f * progress
|
||
val scale = if (isForwardAnimating) lerp(1f, baseScale, forwardProgress) else baseScale
|
||
scaleX = scale
|
||
scaleY = scale
|
||
}
|
||
|
||
/** 前向导航动画:新页面从右侧滑入 */
|
||
private fun GraphicsLayerScope.applyEnterTransform(progress: Float) {
|
||
translationX = (1f - progress) * size.width
|
||
alpha = progress
|
||
}
|
||
|
||
/**
|
||
* 应用入口 Composable,根据系统主题切换明暗 ColorScheme 并管理页面导航。
|
||
*
|
||
* 使用 Box 分层布局替代 AnimatedContent,支持预测性返回手势:
|
||
* - 底层页面始终组合(状态保持),缩放显现
|
||
* - 顶层页面在手势期间平滑位移、缩放、圆角、阴影
|
||
* - 前向导航从右侧滑入,返回导航跟手驱动
|
||
*/
|
||
@Composable
|
||
@Preview(name = "Calendar App")
|
||
fun App() {
|
||
var currentScreen by remember { mutableStateOf(Screen.Main) }
|
||
var backProgress by remember { mutableFloatStateOf(0f) }
|
||
val scope = rememberCoroutineScope()
|
||
|
||
val backAnimProgress = remember { Animatable(0f) }
|
||
val effectiveBackProgress by remember {
|
||
derivedStateOf { maxOf(backProgress, backAnimProgress.value) }
|
||
}
|
||
|
||
var forwardTarget by remember { mutableStateOf<Screen?>(null) }
|
||
val forwardProgress = remember { Animatable(1f) }
|
||
|
||
val handleBack: () -> Unit = {
|
||
scope.launch {
|
||
backAnimProgress.snapTo(backProgress)
|
||
backProgress = 0f
|
||
backAnimProgress.animateTo(1f, tween(250, easing = FastOutSlowInEasing))
|
||
currentScreen = when (currentScreen) {
|
||
Screen.About -> Screen.Main
|
||
Screen.Licenses -> Screen.About
|
||
else -> currentScreen
|
||
}
|
||
backAnimProgress.snapTo(0f)
|
||
}
|
||
}
|
||
|
||
val handleCancel: () -> Unit = {
|
||
scope.launch {
|
||
backAnimProgress.snapTo(backProgress)
|
||
backProgress = 0f
|
||
backAnimProgress.animateTo(0f, spring(stiffness = Spring.StiffnessMediumLow))
|
||
}
|
||
}
|
||
|
||
val navigateTo: (Screen) -> Unit = { target ->
|
||
if (forwardTarget == null) {
|
||
scope.launch {
|
||
forwardTarget = target
|
||
currentScreen = target
|
||
forwardProgress.snapTo(0f)
|
||
forwardProgress.animateTo(1f, tween(350, easing = FastOutSlowInEasing))
|
||
forwardTarget = null
|
||
}
|
||
}
|
||
}
|
||
|
||
val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
|
||
MaterialTheme(colorScheme = colorScheme) {
|
||
Box(modifier = Modifier.fillMaxSize()) {
|
||
// Layer 0: CalendarMonthView(始终组合以保持状态)
|
||
CalendarMonthView(
|
||
modifier = Modifier.graphicsLayer {
|
||
if (currentScreen != Screen.Main) {
|
||
applyRevealTransform(
|
||
effectiveBackProgress,
|
||
forwardProgress.value,
|
||
forwardTarget != null
|
||
)
|
||
}
|
||
},
|
||
onNavigateToAbout = { navigateTo(Screen.About) }
|
||
)
|
||
|
||
// Layer 1: AboutScreen(About 或 Licenses 页面时组合)
|
||
if (currentScreen == Screen.About || currentScreen == Screen.Licenses) {
|
||
AboutScreen(
|
||
onBack = {
|
||
if (currentScreen == Screen.About) handleBack()
|
||
},
|
||
onNavigateToLicenses = {
|
||
if (currentScreen == Screen.About) navigateTo(Screen.Licenses)
|
||
},
|
||
modifier = Modifier.graphicsLayer {
|
||
when (currentScreen) {
|
||
Screen.Licenses -> applyRevealTransform(
|
||
effectiveBackProgress,
|
||
forwardProgress.value,
|
||
forwardTarget == Screen.Licenses
|
||
)
|
||
|
||
Screen.About -> {
|
||
val bp = effectiveBackProgress
|
||
val fp = forwardProgress.value
|
||
when {
|
||
bp > 0.001f -> applyDismissTransform(bp)
|
||
fp < 0.999f && forwardTarget == Screen.About -> applyEnterTransform(fp)
|
||
}
|
||
}
|
||
|
||
else -> {}
|
||
}
|
||
}
|
||
)
|
||
}
|
||
|
||
// Layer 2: LicensesScreen(Licenses 页面时组合)
|
||
if (currentScreen == Screen.Licenses) {
|
||
LicensesScreen(
|
||
onBack = handleBack,
|
||
modifier = Modifier.graphicsLayer {
|
||
val bp = effectiveBackProgress
|
||
val fp = forwardProgress.value
|
||
when {
|
||
bp > 0.001f -> applyDismissTransform(bp)
|
||
fp < 0.999f && forwardTarget == Screen.Licenses -> applyEnterTransform(fp)
|
||
}
|
||
}
|
||
)
|
||
}
|
||
|
||
// 预测性返回手势
|
||
if (currentScreen != Screen.Main) {
|
||
PredictiveBackHandler(
|
||
enabled = backProgress == 0f && !backAnimProgress.isRunning && forwardTarget == null,
|
||
onProgress = { backProgress = it },
|
||
onBack = handleBack,
|
||
onCancel = handleCancel
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|