diff --git a/androidApp/src/main/assets/app_icon.png b/androidApp/src/main/assets/app_icon.png
index 53fc536..d4491c3 100644
Binary files a/androidApp/src/main/assets/app_icon.png and b/androidApp/src/main/assets/app_icon.png differ
diff --git a/androidApp/src/main/res/drawable/ic_launcher_background.xml b/androidApp/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index e93e11a..0000000
--- a/androidApp/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/androidApp/src/main/res/drawable/ic_launcher_foreground.xml b/androidApp/src/main/res/drawable/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d1..0000000
--- a/androidApp/src/main/res/drawable/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index eca70cf..0000000
--- a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index eca70cf..0000000
--- a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png
index a571e60..9c67d11 100644
Binary files a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 61da551..9c67d11 100644
Binary files a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png
index c41dd28..8c16c78 100644
Binary files a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png
index db5080a..8c16c78 100644
Binary files a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png
index 6dba46d..333be02 100644
Binary files a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index da31a87..333be02 100644
Binary files a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 15ac681..78b044c 100644
Binary files a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index b216f2d..78b044c 100644
Binary files a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index f25a419..9a9fedb 100644
Binary files a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index e96783c..9a9fedb 100644
Binary files a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/shared/src/androidMain/kotlin/plus/rua/project/Platform.android.kt b/shared/src/androidMain/kotlin/plus/rua/project/Platform.android.kt
index 6b4d872..aef4f5c 100644
--- a/shared/src/androidMain/kotlin/plus/rua/project/Platform.android.kt
+++ b/shared/src/androidMain/kotlin/plus/rua/project/Platform.android.kt
@@ -1,9 +1,12 @@
package plus.rua.project
import android.os.Build
+import androidx.activity.BackEventCompat
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.flow.Flow
class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
@@ -22,19 +25,20 @@ actual fun PredictiveBackHandler(
onBack: () -> Unit,
onCancel: () -> Unit
) {
- if (Build.VERSION.SDK_INT >= 34) {
- rememberCoroutineScope()
- androidx.activity.compose.PredictiveBackHandler(enabled) { progress ->
- try {
- progress.collect { backEvent ->
- onProgress(backEvent.progress)
- }
- onBack()
- } catch (e: CancellationException) {
- onCancel()
+ // 官方 PredictiveBackHandler — Flow 模式:collect 完成=返回,CancellationException=取消
+ PredictiveBackHandler(enabled = enabled) { progress: Flow ->
+ try {
+ progress.collect { event ->
+ onProgress(event.progress)
}
+ onBack()
+ } catch (e: CancellationException) {
+ onCancel()
}
- } else {
- androidx.activity.compose.BackHandler(enabled = enabled, onBack = onBack)
+ }
+
+ // 降级:部分设备(如 OPPO/ColorOS)不通过 OnBackInvokedCallback 分发返回事件
+ BackHandler(enabled = enabled) {
+ onBack()
}
}
diff --git a/shared/src/commonMain/composeResources/files/app_icon.png b/shared/src/commonMain/composeResources/files/app_icon.png
index 53fc536..d4491c3 100644
Binary files a/shared/src/commonMain/composeResources/files/app_icon.png and b/shared/src/commonMain/composeResources/files/app_icon.png differ
diff --git a/shared/src/commonMain/kotlin/plus/rua/project/App.kt b/shared/src/commonMain/kotlin/plus/rua/project/App.kt
index 317e723..0a0be15 100644
--- a/shared/src/commonMain/kotlin/plus/rua/project/App.kt
+++ b/shared/src/commonMain/kotlin/plus/rua/project/App.kt
@@ -1,117 +1,203 @@
package plus.rua.project
-import androidx.compose.animation.AnimatedContent
+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.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.animation.togetherWith
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) {
+ // 二次缓动:小幅手势产生更柔和的视觉变化,大幅手势仍达到完整效果
+ val p = progress * progress
+ translationX = p * size.width * 0.5f
+ scaleX = 1f - p * 0.08f
+ scaleY = 1f - p * 0.08f
+ alpha = 1f - p
+ shadowElevation = 32.dp.toPx() * p
+ shape = RoundedCornerShape(28.dp * p)
+ clip = p > 0.01f
+}
+
+/** 底层页面缩放:随返回进度从 baseScale 放大到 1.0 */
+private fun GraphicsLayerScope.applyRevealTransform(
+ progress: Float,
+ forwardProgress: Float,
+ isForwardAnimating: Boolean
+) {
+ val p = progress * progress
+ val baseScale = 0.92f + 0.08f * p
+ 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 handleBack: () -> Unit = {
- backProgress = 0f
- when (currentScreen) {
- Screen.About -> currentScreen = Screen.Main
- Screen.Licenses -> currentScreen = Screen.About
- else -> {}
+ val backAnimProgress = remember { Animatable(0f) }
+ val effectiveBackProgress by remember {
+ derivedStateOf { maxOf(backProgress, backAnimProgress.value) }
+ }
+
+ var forwardTarget by remember { mutableStateOf(null) }
+ val forwardProgress = remember { Animatable(1f) }
+
+ var isHandlingBack by remember { mutableStateOf(false) }
+ val handleBack: () -> Unit = lambda@{
+ if (isHandlingBack) return@lambda
+ isHandlingBack = true
+ scope.launch {
+ backAnimProgress.snapTo(backProgress)
+ backProgress = 0f
+ backAnimProgress.animateTo(1f, spring(stiffness = Spring.StiffnessMedium, dampingRatio = Spring.DampingRatioNoBouncy))
+ currentScreen = when (currentScreen) {
+ Screen.About -> Screen.Main
+ Screen.Licenses -> Screen.About
+ else -> currentScreen
+ }
+ backAnimProgress.animateTo(0f, spring(stiffness = Spring.StiffnessMedium, dampingRatio = Spring.DampingRatioNoBouncy))
+ isHandlingBack = false
}
}
val handleCancel: () -> Unit = {
- backProgress = 0f
+ 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) {
- AnimatedContent(
- targetState = currentScreen,
- transitionSpec = {
- if (targetState.ordinal > initialState.ordinal) {
- // 正向导航:新页面从右侧滑入覆盖,旧页面略微左移+淡出
- (slideInHorizontally { it } + fadeIn()) togetherWith
- (slideOutHorizontally { -it / 4 } + fadeOut())
- } else {
- // 返回导航:新页面从左侧滑入,旧页面向右侧滑出
- (slideInHorizontally(animationSpec = tween(250)) { -it } + fadeIn(
- animationSpec = tween(
- 250
+ Box(modifier = Modifier.fillMaxSize()) {
+ // Layer 0: CalendarMonthView(始终组合以保持状态)
+ CalendarMonthView(
+ modifier = Modifier.graphicsLayer {
+ if (currentScreen != Screen.Main) {
+ applyRevealTransform(
+ effectiveBackProgress,
+ forwardProgress.value,
+ forwardTarget != null
)
- )) togetherWith
- (slideOutHorizontally(animationSpec = tween(250)) { it } + fadeOut(
- animationSpec = tween(250)
- ))
- }
- },
- modifier = Modifier.fillMaxSize()
- ) { screen ->
- when (screen) {
- Screen.Main -> CalendarMonthView(
- modifier = Modifier,
- onNavigateToAbout = { currentScreen = Screen.About }
+ }
+ },
+ 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 -> {}
+ }
+ }
)
+ }
- Screen.About -> {
- PredictiveBackHandler(
- enabled = backProgress == 0f,
- onProgress = { backProgress = it },
- onBack = handleBack,
- onCancel = handleCancel
- )
- AboutScreen(
- onBack = { currentScreen = Screen.Main },
- onNavigateToLicenses = { currentScreen = Screen.Licenses },
- modifier = Modifier.graphicsLayer {
- translationX = backProgress * size.width * 0.3f
- scaleX = 1f - backProgress * 0.05f
- scaleY = 1f - backProgress * 0.05f
+ // 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)
}
- )
- }
+ }
+ )
+ }
- Screen.Licenses -> {
- PredictiveBackHandler(
- enabled = backProgress == 0f,
- onProgress = { backProgress = it },
- onBack = handleBack,
- onCancel = handleCancel
- )
- LicensesScreen(
- onBack = { currentScreen = Screen.About },
- modifier = Modifier.graphicsLayer {
- translationX = backProgress * size.width * 0.3f
- scaleX = 1f - backProgress * 0.05f
- scaleY = 1f - backProgress * 0.05f
- }
- )
- }
+ // 预测性返回手势
+ if (currentScreen != Screen.Main) {
+ PredictiveBackHandler(
+ enabled = !backAnimProgress.isRunning && !isHandlingBack && forwardTarget == null,
+ onProgress = { backProgress = it },
+ onBack = handleBack,
+ onCancel = handleCancel
+ )
}
}
}