|
|
|
|
@@ -0,0 +1,87 @@
|
|
|
|
|
package de.jeanlucmakiola.floret.identity
|
|
|
|
|
|
|
|
|
|
import android.provider.Settings
|
|
|
|
|
import androidx.activity.BackEventCompat
|
|
|
|
|
import androidx.activity.compose.PredictiveBackHandler
|
|
|
|
|
import androidx.compose.animation.core.Animatable
|
|
|
|
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
|
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
|
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.platform.LocalContext
|
|
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
|
import kotlin.coroutines.cancellation.CancellationException
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether the user has asked the system to remove animations (Settings →
|
|
|
|
|
* Accessibility → "Remove animations", which sets the global animator duration
|
|
|
|
|
* scale to 0). Compose animations do *not* honour this platform flag on their
|
|
|
|
|
* own, so the shared motion helpers check it and fall back to no spatial
|
|
|
|
|
* movement — respecting the vestibular intent of the setting while keeping state
|
|
|
|
|
* changes legible.
|
|
|
|
|
*
|
|
|
|
|
* Read once at composition; the scale changes rarely and only takes full effect
|
|
|
|
|
* after a process restart anyway.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
fun rememberReduceMotion(): Boolean {
|
|
|
|
|
val resolver = LocalContext.current.contentResolver
|
|
|
|
|
return remember(resolver) {
|
|
|
|
|
Settings.Global.getFloat(resolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The standard Android predictive-back transform for a full-screen surface: as
|
|
|
|
|
* the back gesture is dragged, the surface scales toward ~90%, shifts toward the
|
|
|
|
|
* swiped edge and rounds its corners, previewing what's behind. Completing the
|
|
|
|
|
* gesture invokes [onBack]; cancelling springs it back.
|
|
|
|
|
*
|
|
|
|
|
* A drop-in replacement for a screen's own `BackHandler(onBack)` — register it
|
|
|
|
|
* once and apply the returned [Modifier] to that screen's root so the preview
|
|
|
|
|
* respects the same back semantics. Under reduced motion the visual preview is
|
|
|
|
|
* skipped (the back still works); on API < 34 the system delivers no progress,
|
|
|
|
|
* so it degrades to a plain back. Shared so the family's apps preview the same
|
|
|
|
|
* way.
|
|
|
|
|
*
|
|
|
|
|
* @param enabled gate the handler — e.g. set false while a screen's own inner
|
|
|
|
|
* back (a sub-section) should take the gesture instead.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
fun Modifier.predictiveBack(
|
|
|
|
|
onBack: () -> Unit,
|
|
|
|
|
enabled: Boolean = true,
|
|
|
|
|
reduceMotion: Boolean = rememberReduceMotion(),
|
|
|
|
|
): Modifier {
|
|
|
|
|
val progress = remember { Animatable(0f) }
|
|
|
|
|
var fromLeftEdge by remember { mutableStateOf(true) }
|
|
|
|
|
|
|
|
|
|
PredictiveBackHandler(enabled = enabled) { events ->
|
|
|
|
|
try {
|
|
|
|
|
events.collect { event ->
|
|
|
|
|
fromLeftEdge = event.swipeEdge == BackEventCompat.EDGE_LEFT
|
|
|
|
|
progress.snapTo(FastOutSlowInEasing.transform(event.progress))
|
|
|
|
|
}
|
|
|
|
|
onBack()
|
|
|
|
|
progress.snapTo(0f)
|
|
|
|
|
} catch (_: CancellationException) {
|
|
|
|
|
progress.animateTo(0f)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (reduceMotion) return this
|
|
|
|
|
return this.graphicsLayer {
|
|
|
|
|
val p = progress.value
|
|
|
|
|
val scale = 1f - 0.1f * p
|
|
|
|
|
scaleX = scale
|
|
|
|
|
scaleY = scale
|
|
|
|
|
translationX = (if (fromLeftEdge) 1f else -1f) * 24.dp.toPx() * p
|
|
|
|
|
shape = RoundedCornerShape(32.dp.toPx() * p)
|
|
|
|
|
clip = p > 0f
|
|
|
|
|
}
|
|
|
|
|
}
|