feat: Android 13+ 预测性返回手势(Predictive Back)
- BackHandler 升级为 PredictiveBackHandler expect/actual - Android 13+ 启用系统级预测返回,跟手阶段同步位移/缩放页面 - Android 低版本回退至普通 BackHandler - iOS 保持空实现(无系统返回手势) - 页面返回动画统一 250ms 时长,提升流畅感 - AndroidManifest 启用 enableOnBackInvokedCallback
This commit is contained in:
parent
58ab7eab4e
commit
fc3c8ec882
@ -3,6 +3,7 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
|
||||
@ -2,6 +2,9 @@ package plus.rua.project
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AndroidPlatform : Platform {
|
||||
override val name: String = "Android ${Build.VERSION.SDK_INT}"
|
||||
@ -14,6 +17,25 @@ actual fun getGifUri(gifFile: String): String = "file:///android_asset/gifs/$gif
|
||||
actual fun getAppIconUri(): String = "file:///android_asset/app_icon.png"
|
||||
|
||||
@Composable
|
||||
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
|
||||
androidx.activity.compose.BackHandler(enabled = enabled, onBack = onBack)
|
||||
actual fun PredictiveBackHandler(
|
||||
enabled: Boolean,
|
||||
onProgress: (Float) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
val scope = rememberCoroutineScope()
|
||||
androidx.activity.compose.PredictiveBackHandler(enabled) { progress ->
|
||||
try {
|
||||
progress.collect { backEvent ->
|
||||
onProgress(backEvent.progress)
|
||||
}
|
||||
onBack()
|
||||
} catch (e: CancellationException) {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
androidx.activity.compose.BackHandler(enabled = enabled, onBack = onBack)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package plus.rua.project
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
@ -13,10 +14,12 @@ import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import plus.rua.project.ui.AboutScreen
|
||||
import plus.rua.project.ui.CalendarMonthView
|
||||
@ -31,6 +34,20 @@ private enum class Screen { Main, About, Licenses }
|
||||
@Preview(name = "Calendar App")
|
||||
fun App() {
|
||||
var currentScreen by remember { mutableStateOf(Screen.Main) }
|
||||
var backProgress by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
val handleBack: () -> Unit = {
|
||||
backProgress = 0f
|
||||
when (currentScreen) {
|
||||
Screen.About -> currentScreen = Screen.Main
|
||||
Screen.Licenses -> currentScreen = Screen.About
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
val handleCancel: () -> Unit = {
|
||||
backProgress = 0f
|
||||
}
|
||||
|
||||
val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
|
||||
MaterialTheme(colorScheme = colorScheme) {
|
||||
@ -38,13 +55,13 @@ fun App() {
|
||||
targetState = currentScreen,
|
||||
transitionSpec = {
|
||||
if (targetState.ordinal > initialState.ordinal) {
|
||||
// 向前导航:新页面从右侧滑入覆盖,旧页面略微左移+淡出
|
||||
// 正向导航:新页面从右侧滑入覆盖,旧页面略微左移+淡出
|
||||
(slideInHorizontally { it } + fadeIn()) togetherWith
|
||||
(slideOutHorizontally { -it / 4 } + fadeOut())
|
||||
} else {
|
||||
// 向后导航:新页面从左侧滑入,旧页面略微右移+淡出
|
||||
(slideInHorizontally { -it } + fadeIn()) togetherWith
|
||||
(slideOutHorizontally { it / 4 } + fadeOut())
|
||||
// 返回导航:新页面从左侧滑入,旧页面向右侧滑出
|
||||
(slideInHorizontally(animationSpec = tween(250)) { -it } + fadeIn(animationSpec = tween(250))) togetherWith
|
||||
(slideOutHorizontally(animationSpec = tween(250)) { it } + fadeOut(animationSpec = tween(250)))
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@ -55,16 +72,36 @@ fun App() {
|
||||
onNavigateToAbout = { currentScreen = Screen.About }
|
||||
)
|
||||
Screen.About -> {
|
||||
BackHandler { currentScreen = Screen.Main }
|
||||
PredictiveBackHandler(
|
||||
enabled = backProgress == 0f,
|
||||
onProgress = { backProgress = it },
|
||||
onBack = handleBack,
|
||||
onCancel = handleCancel
|
||||
)
|
||||
AboutScreen(
|
||||
onBack = { currentScreen = Screen.Main },
|
||||
onNavigateToLicenses = { currentScreen = Screen.Licenses }
|
||||
onNavigateToLicenses = { currentScreen = Screen.Licenses },
|
||||
modifier = Modifier.graphicsLayer {
|
||||
translationX = backProgress * size.width * 0.3f
|
||||
scaleX = 1f - backProgress * 0.05f
|
||||
scaleY = 1f - backProgress * 0.05f
|
||||
}
|
||||
)
|
||||
}
|
||||
Screen.Licenses -> {
|
||||
BackHandler { currentScreen = Screen.About }
|
||||
PredictiveBackHandler(
|
||||
enabled = backProgress == 0f,
|
||||
onProgress = { backProgress = it },
|
||||
onBack = handleBack,
|
||||
onCancel = handleCancel
|
||||
)
|
||||
LicensesScreen(
|
||||
onBack = { currentScreen = Screen.About }
|
||||
onBack = { currentScreen = Screen.About },
|
||||
modifier = Modifier.graphicsLayer {
|
||||
translationX = backProgress * size.width * 0.3f
|
||||
scaleX = 1f - backProgress * 0.05f
|
||||
scaleY = 1f - backProgress * 0.05f
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,10 +19,17 @@ expect fun getGifUri(gifFile: String): String
|
||||
expect fun getAppIconUri(): String
|
||||
|
||||
/**
|
||||
* 拦截系统返回手势。
|
||||
* 预测性返回手势处理器(Android 13+)。
|
||||
*
|
||||
* @param enabled 是否启用拦截
|
||||
* @param onBack 返回回调
|
||||
* @param enabled 是否启用
|
||||
* @param onProgress 手势进度回调(0.0~1.0),跟手过程中持续调用
|
||||
* @param onBack 手势完成回调(滑动距离足够,执行返回)
|
||||
* @param onCancel 手势取消回调(滑动距离不足,回弹)
|
||||
*/
|
||||
@Composable
|
||||
expect fun BackHandler(enabled: Boolean = true, onBack: () -> Unit)
|
||||
expect fun PredictiveBackHandler(
|
||||
enabled: Boolean = true,
|
||||
onProgress: (Float) -> Unit = {},
|
||||
onBack: () -> Unit,
|
||||
onCancel: () -> Unit = {}
|
||||
)
|
||||
|
||||
@ -15,6 +15,11 @@ actual fun getGifUri(gifFile: String): String = "compose.resource://files/$gifFi
|
||||
actual fun getAppIconUri(): String = "compose.resource://files/app_icon.png"
|
||||
|
||||
@Composable
|
||||
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
|
||||
// iOS 没有系统返回键,由导航栏按钮处理
|
||||
actual fun PredictiveBackHandler(
|
||||
enabled: Boolean,
|
||||
onProgress: (Float) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
// iOS 没有预测性返回手势,由导航栏按钮处理
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user