diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.kt index f90d65cbe..dd0c1aacc 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.kt @@ -61,6 +61,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.strongswan.android.R +import org.strongswan.android.ui.compose.components.PredictiveBackScale import org.strongswan.android.logic.imc.RemediationInstruction import org.strongswan.android.ui.compose.components.ShxCard import org.strongswan.android.ui.compose.components.ShxEmptyState @@ -127,7 +128,8 @@ private fun RemediationUi( onFinish: () -> Unit, ) { - BackHandler(enabled = selected != null) { onSelect(null) } + PredictiveBackScale(enabled = selected != null, onBack = { onSelect(null) }, modifier = Modifier.fillMaxSize()) + { Column(Modifier.fillMaxSize()) { TopAppBar( @@ -246,4 +248,5 @@ private fun RemediationUi( } } } + } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt index 06eb88435..e5179f0d2 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt @@ -96,6 +96,7 @@ import androidx.compose.material3.Icon import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.widthIn +import org.strongswan.android.ui.compose.components.PredictiveBackScale import org.strongswan.android.R import org.strongswan.android.data.VpnProfile import org.strongswan.android.logic.StrongSwanApplication @@ -178,14 +179,18 @@ fun SettingsScreen(profiles: List, showTitle: Boolean = true) var pageState by rememberSaveable { mutableStateOf(SettingsPage.ROOT) } /* from a sub page "back" returns to the settings list, not out of the tab */ - BackHandler(enabled = pageState != SettingsPage.ROOT) { pageState = SettingsPage.ROOT } val effects = MaterialTheme.motionScheme.defaultEffectsSpec() val spatial = MaterialTheme.motionScheme.defaultSpatialSpec() Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { + PredictiveBackScale( + enabled = pageState != SettingsPage.ROOT, + onBack = { pageState = SettingsPage.ROOT }, + modifier = Modifier.widthIn(max = 640.dp).fillMaxSize(), + ) + { AnimatedContent( - modifier = Modifier.widthIn(max = 640.dp), targetState = pageState, transitionSpec = { val forward = targetState != SettingsPage.ROOT @@ -533,6 +538,7 @@ fun SettingsScreen(profiles: List, showTitle: Boolean = true) ) } } + } ShxSnackbarHost( hostState = snackbarHostState, modifier = Modifier.align(Alignment.BottomCenter), diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/PredictiveBack.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/PredictiveBack.kt new file mode 100644 index 000000000..f02bb2de7 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/PredictiveBack.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2026 SHX VPN + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + */ + +package org.strongswan.android.ui.compose.components + +import androidx.activity.compose.PredictiveBackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp +import kotlin.coroutines.cancellation.CancellationException + +/** + * Handles the system back gesture for in-screen navigation (a sub page that returns + * to its parent): while the user drags, the content shrinks slightly and rounds its + * corners, then [onBack] runs when the gesture is committed. + */ +@Composable +fun PredictiveBackScale( + enabled: Boolean, + onBack: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) +{ + var progress by remember { mutableFloatStateOf(0f) } + PredictiveBackHandler(enabled = enabled) { events -> + try + { + events.collect { progress = it.progress } + onBack() + progress = 0f + } + catch (e: CancellationException) + { + progress = 0f + throw e + } + } + Box( + modifier + .graphicsLayer { + val scale = 1f - 0.08f * progress + scaleX = scale + scaleY = scale + } + .clip(RoundedCornerShape((progress * 28f).dp)), + ) + { + content() + } +}