MindMachine: add immersive fullscreen and tap-to-reveal bars
This commit is contained in:
@@ -48,6 +48,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -61,6 +62,9 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
|||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -80,7 +84,10 @@ import com.mindmachine.mvp.session.SplitFlashFrame
|
|||||||
import com.mindmachine.mvp.session.computeSplitFlashFrame
|
import com.mindmachine.mvp.session.computeSplitFlashFrame
|
||||||
import com.mindmachine.mvp.session.flashIntervalMsToSeconds
|
import com.mindmachine.mvp.session.flashIntervalMsToSeconds
|
||||||
import com.mindmachine.mvp.session.flashIntervalSecondsToMs
|
import com.mindmachine.mvp.session.flashIntervalSecondsToMs
|
||||||
|
import com.mindmachine.mvp.session.shouldRevealSystemBarsOnTap
|
||||||
import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault
|
import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault
|
||||||
|
import com.mindmachine.mvp.session.shouldUseImmersiveFullscreen
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val MindMachineColors = darkColorScheme(
|
private val MindMachineColors = darkColorScheme(
|
||||||
@@ -320,6 +327,13 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val immersiveFullscreen = shouldUseImmersiveFullscreen(isVisualMode, ui.runtimeState)
|
||||||
|
var revealSystemBarsSignal by remember { mutableIntStateOf(0) }
|
||||||
|
ApplyImmersiveMode(
|
||||||
|
enabled = immersiveFullscreen,
|
||||||
|
revealSignal = revealSystemBarsSignal,
|
||||||
|
)
|
||||||
|
|
||||||
var showOverlay by remember(ui.runtimeState) { mutableStateOf(shouldShowActiveControlsByDefault(ui.runtimeState)) }
|
var showOverlay by remember(ui.runtimeState) { mutableStateOf(shouldShowActiveControlsByDefault(ui.runtimeState)) }
|
||||||
LaunchedEffect(ui.runtimeState) {
|
LaunchedEffect(ui.runtimeState) {
|
||||||
showOverlay = shouldShowActiveControlsByDefault(ui.runtimeState)
|
showOverlay = shouldShowActiveControlsByDefault(ui.runtimeState)
|
||||||
@@ -334,7 +348,12 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup:
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.Black)
|
.background(Color.Black)
|
||||||
.clickable { showOverlay = !showOverlay }
|
.clickable {
|
||||||
|
showOverlay = !showOverlay
|
||||||
|
if (shouldRevealSystemBarsOnTap(isVisualMode, ui.runtimeState)) {
|
||||||
|
revealSystemBarsSignal += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
if (isVisualMode) {
|
if (isVisualMode) {
|
||||||
Row(Modifier.fillMaxSize()) {
|
Row(Modifier.fillMaxSize()) {
|
||||||
@@ -389,6 +408,41 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ApplyImmersiveMode(enabled: Boolean, revealSignal: Int) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val activity = context as? Activity ?: return
|
||||||
|
val window = activity.window
|
||||||
|
val controller = remember(window) { WindowInsetsControllerCompat(window, window.decorView) }
|
||||||
|
|
||||||
|
DisposableEffect(window) {
|
||||||
|
onDispose {
|
||||||
|
controller.show(WindowInsetsCompat.Type.systemBars())
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
|
} else {
|
||||||
|
controller.show(WindowInsetsCompat.Type.systemBars())
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(revealSignal, enabled) {
|
||||||
|
if (!enabled || revealSignal == 0) return@LaunchedEffect
|
||||||
|
controller.show(WindowInsetsCompat.Type.systemBars())
|
||||||
|
delay(2500)
|
||||||
|
if (enabled) {
|
||||||
|
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun FlashColor.toComposeColor(): Color = when (this) {
|
private fun FlashColor.toComposeColor(): Color = when (this) {
|
||||||
FlashColor.RED -> Color.Red
|
FlashColor.RED -> Color.Red
|
||||||
FlashColor.GREEN -> Color.Green
|
FlashColor.GREEN -> Color.Green
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.mindmachine.mvp.session
|
||||||
|
|
||||||
|
import com.mindmachine.mvp.domain.RuntimeState
|
||||||
|
|
||||||
|
fun shouldUseImmersiveFullscreen(isVisualMode: Boolean, runtimeState: RuntimeState): Boolean {
|
||||||
|
return isVisualMode && (runtimeState == RuntimeState.COUNTDOWN || runtimeState == RuntimeState.RUNNING)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldRevealSystemBarsOnTap(isVisualMode: Boolean, runtimeState: RuntimeState): Boolean {
|
||||||
|
return shouldUseImmersiveFullscreen(isVisualMode, runtimeState)
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.mindmachine.mvp
|
package com.mindmachine.mvp
|
||||||
|
|
||||||
import com.mindmachine.mvp.domain.RuntimeState
|
import com.mindmachine.mvp.domain.RuntimeState
|
||||||
|
import com.mindmachine.mvp.session.shouldRevealSystemBarsOnTap
|
||||||
import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault
|
import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault
|
||||||
|
import com.mindmachine.mvp.session.shouldUseImmersiveFullscreen
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -21,4 +23,25 @@ class ActiveSessionUiTest {
|
|||||||
assertTrue(shouldShowActiveControlsByDefault(RuntimeState.COMPLETED))
|
assertTrue(shouldShowActiveControlsByDefault(RuntimeState.COMPLETED))
|
||||||
assertTrue(shouldShowActiveControlsByDefault(RuntimeState.STOPPED))
|
assertTrue(shouldShowActiveControlsByDefault(RuntimeState.STOPPED))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun immersive_fullscreen_applies_only_to_visual_countdown_and_running_states() {
|
||||||
|
assertTrue(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.COUNTDOWN))
|
||||||
|
assertTrue(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.RUNNING))
|
||||||
|
|
||||||
|
assertFalse(shouldUseImmersiveFullscreen(isVisualMode = false, runtimeState = RuntimeState.COUNTDOWN))
|
||||||
|
assertFalse(shouldUseImmersiveFullscreen(isVisualMode = false, runtimeState = RuntimeState.RUNNING))
|
||||||
|
assertFalse(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.PAUSED))
|
||||||
|
assertFalse(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.INTERRUPTED))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun tap_to_reveal_system_bars_is_only_enabled_during_active_visual_stimulation() {
|
||||||
|
assertTrue(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.COUNTDOWN))
|
||||||
|
assertTrue(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.RUNNING))
|
||||||
|
|
||||||
|
assertFalse(shouldRevealSystemBarsOnTap(isVisualMode = false, runtimeState = RuntimeState.RUNNING))
|
||||||
|
assertFalse(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.PAUSED))
|
||||||
|
assertFalse(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.COMPLETED))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user