fix(screensaver): match night brightness across hosts, end on leave
The dream now turns its window down to the same near-zero brightness as the in-app screen saver, not just to the system's dim level, and builds its ComposeView from the window's context rather than the service's. The in-app screen saver finishes once it is no longer shown, so waking the phone shows the lock screen, and rotating it into a stand no longer recreates it. The outgoing minute of the crossfade is hidden from screen readers.
This commit is contained in:
1 parent
4765b8e088
commit
be2c0ada04
6 files changed
+48
-24
No files matched your search
@@ -307,13 +307,14 @@
|
||||
android:theme="@style/Theme.Clockula" />
|
||||
|
||||
<!-- The same screen saver on demand, from the tabs' overflow menu: an
|
||||
app cannot start the system's dream itself. Not exported, and
|
||||
no history: leaving it by Home or Recents finishes it, so the app
|
||||
never comes back to a dim screen saver instead of its tabs. -->
|
||||
app cannot start the system's dream itself. Not exported; it
|
||||
finishes itself once no longer shown. Rotating into a dock or a
|
||||
stand re-lays it out rather than recreating it, which would
|
||||
flash black and reset its brightness. -->
|
||||
<activity
|
||||
android:name=".ui.screensaver.ScreenSaverActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/Theme.Clockula.ScreenSaver" />
|
||||
|
||||
<!-- Persists the per-app language on API < 33, where the platform
|
||||
|
||||
+11
-3
@@ -10,7 +10,9 @@ import javax.inject.Inject
|
||||
* Clockula's screen saver, as the system starts it — on a charger or a dock,
|
||||
* from Settings → Display → Screen saver. Non-interactive: any touch wakes
|
||||
* the device, as a screen saver should. Night mode turns the display down to
|
||||
* the system's dim level ([setScreenBright]) on top of the dimmed colours.
|
||||
* the system's dim level ([setScreenBright]) and the window's own
|
||||
* brightness to the same near-zero the in-app host uses, on top of the
|
||||
* dimmed colours.
|
||||
*
|
||||
* Lives in `ui/` because it hosts Compose. Its `ComposeView` finds its
|
||||
* lifecycle, view-model store and saved-state owners on the window's decor
|
||||
@@ -32,14 +34,20 @@ class ClockulaDreamService : DreamService() {
|
||||
isFullscreen = true
|
||||
// Dim until the settings say otherwise (ScreenSaverRoot).
|
||||
isScreenBright = false
|
||||
window.applyScreenSaverBrightness(nightMode = true)
|
||||
|
||||
val owner = DreamViewOwner().also { owner = it }
|
||||
owner.create(window.decorView)
|
||||
setContentView(
|
||||
ComposeView(this).apply {
|
||||
// The window's context, not the service's: a Service is not a UI
|
||||
// context, and the window's knows its display's density and size.
|
||||
ComposeView(window.context).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow)
|
||||
setContent {
|
||||
ScreenSaverRoot(source, onNightModeChanged = { night -> isScreenBright = !night })
|
||||
ScreenSaverRoot(source, onNightModeChanged = { night ->
|
||||
isScreenBright = !night
|
||||
window.applyScreenSaverBrightness(night)
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -48,13 +48,13 @@ class ScreenSaverActivity : ComponentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setShowWhenLocked(true)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
|
||||
applyNightBrightness(true)
|
||||
window.applyScreenSaverBrightness(nightMode = true)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
hide(WindowInsetsCompat.Type.systemBars())
|
||||
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
setContent { ScreenSaverRoot(source, onNightModeChanged = ::applyNightBrightness) }
|
||||
setContent { ScreenSaverRoot(source, onNightModeChanged = window::applyScreenSaverBrightness) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -73,6 +73,11 @@ class ScreenSaverActivity : ComponentActivity() {
|
||||
override fun onStop() {
|
||||
unregisterReceiver(powerReceiver)
|
||||
super.onStop()
|
||||
// Gone once it is no longer shown — Home, Recents, or the screen
|
||||
// timing out on battery — as a dream ends, so the next wake shows the
|
||||
// lock screen and the next launch the app, never a screen saver left
|
||||
// behind. A rotation recreates nothing (configChanges in the manifest).
|
||||
if (!isChangingConfigurations) finish()
|
||||
}
|
||||
|
||||
override fun onUserInteraction() {
|
||||
@@ -90,15 +95,4 @@ class ScreenSaverActivity : ComponentActivity() {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyNightBrightness(nightMode: Boolean) {
|
||||
window.attributes = window.attributes.apply {
|
||||
screenBrightness = if (nightMode) NIGHT_BRIGHTNESS else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** Near the bottom of the panel's range; not 0, which some panels take as "off". */
|
||||
const val NIGHT_BRIGHTNESS = 0.01f
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@@ -80,15 +81,18 @@ internal fun ScreenSaverContent(state: ScreenSaverUiState, modifier: Modifier =
|
||||
animationSpec = if (reduceMotion) snap() else tween(ScreenSaverDefaults.MOVE_MILLIS),
|
||||
label = "drift",
|
||||
) { shown ->
|
||||
Drifting(shown.position) { Clock(shown, side) }
|
||||
// The outgoing minute is only a picture while it fades: a screen
|
||||
// reader finds one time on screen, never two.
|
||||
val semantics = if (shown == state) Modifier else Modifier.clearAndSetSemantics {}
|
||||
Drifting(shown.position, semantics) { Clock(shown, side) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Places its one child at [position]'s fractions of the space the child leaves free. */
|
||||
@Composable
|
||||
private fun Drifting(position: ScreenSaverDrift.Position, content: @Composable () -> Unit) {
|
||||
Layout(content = content, modifier = Modifier.fillMaxSize()) { measurables, constraints ->
|
||||
private fun Drifting(position: ScreenSaverDrift.Position, modifier: Modifier, content: @Composable () -> Unit) {
|
||||
Layout(content = content, modifier = modifier.fillMaxSize()) { measurables, constraints ->
|
||||
val placeable = measurables.single().measure(constraints.copy(minWidth = 0, minHeight = 0))
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
placeable.place(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.jeanlucmakiola.clockula.ui.screensaver
|
||||
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -13,6 +15,21 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.jeanlucmakiola.clockula.ui.theme.ScreenSaverTheme
|
||||
import de.jeanlucmakiola.floret.prefs.Appearance
|
||||
|
||||
/** Near the bottom of the panel's range; not 0, which some panels take as "off". */
|
||||
private const val NIGHT_BRIGHTNESS = 0.01f
|
||||
|
||||
/**
|
||||
* Both hosts' display brightness for [nightMode]: almost nothing at night,
|
||||
* the user's own otherwise. The same window setting for the dream and the
|
||||
* activity, so the system's screen saver is no brighter at night than the
|
||||
* one opened from the menu.
|
||||
*/
|
||||
internal fun Window.applyScreenSaverBrightness(nightMode: Boolean) {
|
||||
attributes = attributes.apply {
|
||||
screenBrightness = if (nightMode) NIGHT_BRIGHTNESS else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What both hosts set as their content: [source]'s state in the screen
|
||||
* saver's theme. Collected with the host's lifecycle, so a stopped dream or
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.screensaver.ScreenSaverStyle
|
||||
|
||||
Reference in new issue
Block a user