diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7a6fdc3..3ea4e3d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,7 +22,7 @@ diff --git a/app/src/main/kotlin/dev/xitee/sleeptimer/navigation/SleepTimerNavHost.kt b/app/src/main/kotlin/dev/xitee/sleeptimer/navigation/SleepTimerNavHost.kt index 83d2158..1766f21 100644 --- a/app/src/main/kotlin/dev/xitee/sleeptimer/navigation/SleepTimerNavHost.kt +++ b/app/src/main/kotlin/dev/xitee/sleeptimer/navigation/SleepTimerNavHost.kt @@ -1,21 +1,46 @@ package dev.xitee.sleeptimer.navigation +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.NavDestination.Companion.hasRoute import dev.xitee.sleeptimer.feature.timer.about.AboutScreen import dev.xitee.sleeptimer.feature.timer.settings.SettingsScreen import dev.xitee.sleeptimer.feature.timer.settings.ThemePickerScreen +import dev.xitee.sleeptimer.feature.timer.timer.AppOrientationController +import dev.xitee.sleeptimer.feature.timer.timer.DeviceOrientation import dev.xitee.sleeptimer.feature.timer.timer.TimerScreen +import dev.xitee.sleeptimer.feature.timer.timer.rememberDeviceOrientation @Composable fun SleepTimerNavHost() { val navController = rememberNavController() + val backStackEntry by navController.currentBackStackEntryAsState() + val isTimer = backStackEntry?.destination?.hasRoute() ?: true + // Skip the cross-screen fade when the device is tilted: the orientation + // flip that accompanies Timer ↔ Settings navigation combined with a fade + // briefly shows Timer's counter-rotated content being rotated by the + // window manager, appearing upside-down. In natural portrait there is no + // orientation flip, so the fade is kept. + val deviceOrientation by rememberDeviceOrientation() + val animate = deviceOrientation == DeviceOrientation.PORTRAIT + + AppOrientationController(orientation = deviceOrientation, lockPortrait = isTimer) NavHost( navController = navController, startDestination = TimerRoute, + enterTransition = { if (animate) fadeIn() else EnterTransition.None }, + exitTransition = { if (animate) fadeOut() else ExitTransition.None }, + popEnterTransition = { if (animate) fadeIn() else EnterTransition.None }, + popExitTransition = { if (animate) fadeOut() else ExitTransition.None }, ) { composable { TimerScreen( diff --git a/feature/timer/src/main/kotlin/dev/xitee/sleeptimer/feature/timer/timer/AppOrientationController.kt b/feature/timer/src/main/kotlin/dev/xitee/sleeptimer/feature/timer/timer/AppOrientationController.kt new file mode 100644 index 0000000..77a2b9e --- /dev/null +++ b/feature/timer/src/main/kotlin/dev/xitee/sleeptimer/feature/timer/timer/AppOrientationController.kt @@ -0,0 +1,37 @@ +package dev.xitee.sleeptimer.feature.timer.timer + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.ActivityInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext + +// Owns the Activity's requestedOrientation for the whole session. Driven by the +// caller's already-tracked DeviceOrientation so there is no disposal gap when the +// lock is released — the pose is known at all times, avoiding Android's own sensor +// debounce on the handoff. +@Composable +fun AppOrientationController(orientation: DeviceOrientation, lockPortrait: Boolean) { + val context = LocalContext.current + + LaunchedEffect(lockPortrait, orientation) { + val activity = context.findActivity() ?: return@LaunchedEffect + activity.requestedOrientation = if (lockPortrait) { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } else { + orientation.toActivityInfoOrientation() + } + } +} + +private fun Context.findActivity(): Activity? { + var ctx: Context? = this + while (ctx is ContextWrapper) { + if (ctx is Activity) return ctx + ctx = ctx.baseContext + } + return null +} diff --git a/feature/timer/src/main/kotlin/dev/xitee/sleeptimer/feature/timer/timer/DeviceOrientation.kt b/feature/timer/src/main/kotlin/dev/xitee/sleeptimer/feature/timer/timer/DeviceOrientation.kt index ed995eb..ddd6b12 100644 --- a/feature/timer/src/main/kotlin/dev/xitee/sleeptimer/feature/timer/timer/DeviceOrientation.kt +++ b/feature/timer/src/main/kotlin/dev/xitee/sleeptimer/feature/timer/timer/DeviceOrientation.kt @@ -1,5 +1,6 @@ package dev.xitee.sleeptimer.feature.timer.timer +import android.content.pm.ActivityInfo import android.view.OrientationEventListener import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -22,6 +23,13 @@ fun DeviceOrientation.counterRotationDegrees(): Float = when (this) { DeviceOrientation.LANDSCAPE_RIGHT -> 90f } +fun DeviceOrientation.toActivityInfoOrientation(): Int = when (this) { + DeviceOrientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + DeviceOrientation.LANDSCAPE_LEFT -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + DeviceOrientation.PORTRAIT_REVERSED -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + DeviceOrientation.LANDSCAPE_RIGHT -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE +} + @Composable fun rememberDeviceOrientation(): State { val context = LocalContext.current