Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:theme="@style/Theme.SleepTimer">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TimerRoute>() ?: 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<TimerRoute> {
TimerScreen(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<DeviceOrientation> {
val context = LocalContext.current
Expand Down
Loading