This is the gist which explains how to use Android predictive back gesture and iOS swipe to go back when using Voyager in Compose multiplatform.
If you are just using Android, you can merge the commonMain stuff and the androidMain stuff.
This is the gist which explains how to use Android predictive back gesture and iOS swipe to go back when using Voyager in Compose multiplatform.
If you are just using Android, you can merge the commonMain stuff and the androidMain stuff.
// Put this in the commmonMain folder | |
package nl.kevinvanmierlo.testapp | |
@Composable | |
fun App() { | |
MyApplicationTheme { | |
KMNavigator(TabBarScreen()) | |
} | |
} | |
@OptIn(ExperimentalVoyagerApi::class) | |
@Composable | |
public fun KMNavigator( | |
screen: Screen, | |
disposeBehavior: NavigatorDisposeBehavior = NavigatorDisposeBehavior(), | |
onBackPressed: OnBackPressed = { true }, | |
content: NavigatorContent = { KMNavigatorContent(navigator = it) } | |
) { | |
Navigator( | |
screen = screen, | |
disposeBehavior = disposeBehavior, | |
onBackPressed = onBackPressed, | |
content = content | |
) | |
} | |
} | |
@Composable | |
public fun KMNavigatorContent(navigator: Navigator) { | |
PlatformNavigatorContent(navigator) | |
} |
// Put this in the androidMain folder | |
package nl.kevinvanmierlo.testapp | |
import androidx.activity.BackEventCompat | |
import androidx.activity.OnBackPressedCallback | |
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.DisposableEffect | |
import androidx.compose.runtime.SideEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberUpdatedState | |
import androidx.compose.ui.platform.LocalLifecycleOwner | |
@Composable | |
public fun PredictiveBackHandler( | |
enabled: Boolean = true, | |
onBackStarted: () -> Unit, | |
onBackProgressed: (backEvent: BackEventCompat) -> Unit, | |
onBackCancelled: () -> Unit, | |
onBack: () -> Unit | |
) { | |
// Safely update the current `onBack` lambda when a new one is provided | |
val currentOnBack by rememberUpdatedState(onBack) | |
// Remember in Composition a back callback that calls the `onBack` lambda | |
val backCallback = remember { | |
object : OnBackPressedCallback(enabled) { | |
override fun handleOnBackCancelled() { | |
onBackCancelled() | |
} | |
override fun handleOnBackProgressed(backEvent: BackEventCompat) { | |
onBackProgressed(backEvent) | |
} | |
override fun handleOnBackStarted(backEvent: BackEventCompat) { | |
onBackStarted() | |
} | |
override fun handleOnBackPressed() { | |
currentOnBack() | |
} | |
} | |
} | |
// On every successful composition, update the callback with the `enabled` value | |
SideEffect { | |
backCallback.isEnabled = enabled | |
} | |
val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) { | |
"No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner" | |
}.onBackPressedDispatcher | |
val lifecycleOwner = LocalLifecycleOwner.current | |
DisposableEffect(lifecycleOwner, backDispatcher) { | |
// Add callback to the backDispatcher | |
backDispatcher.addCallback(lifecycleOwner, backCallback) | |
// When the effect leaves the Composition, remove the callback | |
onDispose { | |
backCallback.remove() | |
} | |
} | |
} |
// Put this in the commmonMain folder | |
package nl.kevinvanmierlo.testapp | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.NonRestartableComposable | |
import androidx.compose.runtime.RememberObserver | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
/** | |
* This is different from the normal StateChangeEffect in that this only gives a callback on change | |
*/ | |
@Composable | |
@NonRestartableComposable | |
fun StateChangeEffectIgnoringInitial( | |
key1: Any?, | |
canRunEffect: () -> Boolean = { true }, | |
effect: () -> Unit | |
) { | |
var firstRemember by remember { mutableStateOf(true) } | |
remember(key1) { | |
StateChangeEffectImpl( | |
remembered = { | |
firstRemember = false | |
}, | |
canRunEffect = canRunEffect, | |
effect = effect, | |
canRun = !firstRemember | |
) | |
} | |
} | |
@Composable | |
@NonRestartableComposable | |
fun StateChangeEffect( | |
key1: Any?, | |
canRunEffect: () -> Boolean = { true }, | |
effect: () -> Unit | |
) { | |
remember(key1) { StateChangeEffectImpl( | |
canRunEffect = canRunEffect, | |
effect = effect, | |
) } | |
} | |
@Composable | |
@NonRestartableComposable | |
fun StateChangeEffect( | |
key1: Any?, | |
key2: Any?, | |
canRunEffect: () -> Boolean = { true }, | |
effect: () -> Unit | |
) { | |
remember(key1, key2) { | |
StateChangeEffectImpl( | |
canRunEffect = canRunEffect, | |
effect = effect, | |
) | |
} | |
} | |
private class StateChangeEffectImpl( | |
private val remembered: () -> Unit = {}, | |
private val canRunEffect: () -> Boolean, | |
private val effect: () -> Unit, | |
private val canRun: Boolean = true, | |
) : RememberObserver { | |
override fun onRemembered() { | |
remembered() | |
// Remember will always be called once, but state change often only wants to get called when something changed | |
if(canRun && canRunEffect()) { | |
effect() | |
} | |
} | |
override fun onForgotten() { | |
// Nothing to do, we only need onRemembered to run | |
} | |
override fun onAbandoned() { | |
// Nothing to do as [onRemembered] was not called. | |
} | |
} |
// Put this in the commonMain folder | |
package nl.kevinvanmierlo.testapp | |
import kotlin.math.PI | |
import kotlin.math.abs | |
import kotlin.math.cos | |
import kotlin.math.pow | |
import kotlin.math.sin | |
import kotlin.math.sqrt | |
/** | |
* The Easing class provides a collection of ease functions. It does not use the standard 4 param | |
* ease signature. Instead it uses a single param which indicates the current linear ratio (0 to 1) of the tween. | |
*/ | |
class EasingInterpolator(val ease: Ease) { | |
val easingProvider = EasingProvider.get(ease) | |
fun getInterpolation(input: Float): Float { | |
return easingProvider(input) | |
} | |
} | |
/** | |
* The Easing class provides a collection of ease functions. It does not use the standard 4 param | |
* ease signature. Instead it uses a single param which indicates the current linear ratio (0 to 1) of the tween. | |
*/ | |
enum class Ease { | |
LINEAR, | |
QUAD_IN, | |
QUAD_OUT, | |
QUAD_IN_OUT, | |
CUBIC_IN, | |
CUBIC_OUT, | |
CUBIC_IN_OUT, | |
QUART_IN, | |
QUART_OUT, | |
QUART_IN_OUT, | |
QUINT_IN, | |
QUINT_OUT, | |
QUINT_IN_OUT, | |
SINE_IN, | |
SINE_OUT, | |
SINE_IN_OUT, | |
BACK_IN, | |
BACK_OUT, | |
BACK_IN_OUT, | |
CIRC_IN, | |
CIRC_OUT, | |
CIRC_IN_OUT | |
} | |
typealias EaseFunc = (Float) -> Float | |
/** | |
* The Easing class provides a collection of ease functions. It does not use the standard 4 param | |
* ease signature. Instead it uses a single param which indicates the current linear ratio (0 to 1) of the tween. | |
*/ | |
internal object EasingProvider { | |
operator fun get(ease: Ease?): EaseFunc { | |
return when(ease) { | |
Ease.LINEAR -> { { it } } | |
Ease.QUAD_IN -> { { getPowIn(it, 2.0) } } | |
Ease.QUAD_OUT -> { { getPowOut(it, 2.0) } } | |
Ease.QUAD_IN_OUT -> { { getPowInOut(it, 2.0) } } | |
Ease.CUBIC_IN -> { { getPowIn(it, 3.0) } } | |
Ease.CUBIC_OUT -> { { getPowOut(it, 3.0) } } | |
Ease.CUBIC_IN_OUT -> { { getPowInOut(it, 3.0) } } | |
Ease.QUART_IN -> { { getPowIn(it, 4.0) } } | |
Ease.QUART_OUT -> { { getPowOut(it, 4.0) } } | |
Ease.QUART_IN_OUT -> { { getPowInOut(it, 4.0) } } | |
Ease.QUINT_IN -> { { getPowIn(it, 5.0) } } | |
Ease.QUINT_OUT -> { { getPowOut(it, 5.0) } } | |
Ease.QUINT_IN_OUT -> { { getPowInOut(it, 5.0) } } | |
Ease.SINE_IN -> { { (1f - cos(it * PI / 2f)).toFloat() } } | |
Ease.SINE_OUT -> { { sin(it * PI / 2f).toFloat() } } | |
Ease.SINE_IN_OUT -> { { (-0.5f * (cos(PI * it) - 1f)).toFloat() } } | |
Ease.BACK_IN -> { { (it * it * ((1.7 + 1f) * it - 1.7)).toFloat() } } | |
Ease.BACK_OUT -> { { | |
val elapsedTimeRate = it - 1 | |
(elapsedTimeRate * elapsedTimeRate * ((1.7 + 1f) * elapsedTimeRate + 1.7) + 1f).toFloat() | |
} } | |
Ease.BACK_IN_OUT -> { { getBackInOut(it, 1.7f) } } | |
Ease.CIRC_IN -> { { -(sqrt((1f - it * it).toDouble()) - 1).toFloat() } } | |
Ease.CIRC_OUT -> { { | |
val elapsedTimeRate = it - 1 | |
sqrt((1f - elapsedTimeRate * elapsedTimeRate).toDouble()).toFloat() | |
} } | |
Ease.CIRC_IN_OUT -> { | |
{ | |
var elapsedTimeRate = it * 2f | |
if (elapsedTimeRate < 1f) { | |
(-0.5f * (sqrt(1f - elapsedTimeRate * elapsedTimeRate) - 1f)) | |
} else { | |
elapsedTimeRate -= 2f | |
(0.5f * (sqrt(1f - elapsedTimeRate * elapsedTimeRate) + 1f)); | |
} | |
} | |
} | |
else -> { { it } } | |
} | |
} | |
/** | |
* @param elapsedTimeRate Elapsed displayDateTime / Total displayDateTime | |
* @param pow pow The exponent to use (ex. 3 would return a cubic ease). | |
* @return easedValue | |
*/ | |
private fun getPowIn(elapsedTimeRate: Float, pow: Double): Float { | |
return elapsedTimeRate.toDouble().pow(pow).toFloat() | |
} | |
/** | |
* @param elapsedTimeRate Elapsed displayDateTime / Total displayDateTime | |
* @param pow pow The exponent to use (ex. 3 would return a cubic ease). | |
* @return easedValue | |
*/ | |
private fun getPowOut(elapsedTimeRate: Float, pow: Double): Float { | |
return (1f - (1 - elapsedTimeRate).toDouble().pow(pow)).toFloat() | |
} | |
/** | |
* @param elapsedTimeRate Elapsed displayDateTime / Total displayDateTime | |
* @param pow pow The exponent to use (ex. 3 would return a cubic ease). | |
* @return easedValue | |
*/ | |
private fun getPowInOut(elapsedTimeRate: Float, pow: Double): Float { | |
var elapsedTimeRate = elapsedTimeRate | |
return if(2.let { elapsedTimeRate *= it; elapsedTimeRate } < 1) { | |
(0.5 * elapsedTimeRate.toDouble().pow(pow)).toFloat() | |
} else (1 - 0.5 * abs((2 - elapsedTimeRate).toDouble().pow(pow))).toFloat() | |
} | |
/** | |
* @param elapsedTimeRate Elapsed displayDateTime / Total displayDateTime | |
* @param amount amount The strength of the ease. | |
* @return easedValue | |
*/ | |
private fun getBackInOut(elapsedTimeRate: Float, amount: Float): Float { | |
var elapsedTimeRate = elapsedTimeRate | |
var amount = amount | |
amount *= 1.525.toFloat() | |
return if(2.let { elapsedTimeRate *= it; elapsedTimeRate } < 1) { | |
(0.5 * (elapsedTimeRate * elapsedTimeRate * ((amount + 1) * elapsedTimeRate - amount))).toFloat() | |
} else (0.5 * (2.let { elapsedTimeRate -= it; elapsedTimeRate } * elapsedTimeRate * ((amount + 1) * elapsedTimeRate + amount) + 2)).toFloat() | |
} | |
} |