android: predictive back scale for settings pages and remediation detail

This commit is contained in:
Denozordec committed 2026-10-10 00:33:20 +07:00
1 parent 2681233449
commit dc6e1e32fb
3 files changed
+77 -3

No files matched your search

@@ -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(
}
}
}
}
}
@@ -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<VpnProfile>, 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<Float>()
val spatial = MaterialTheme.motionScheme.defaultSpatialSpec<androidx.compose.ui.unit.IntOffset>()
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<VpnProfile>, showTitle: Boolean = true)
)
}
}
}
ShxSnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter),
@@ -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()
}
}