Stop loading screens flashing white against the chosen theme

Two causes. The activity's window was a Light platform theme with no
values-night, so the bare window was white whatever theme was picked. And on
the category and poem screens Load sits outside the Scaffold, so while a page
was loading nothing painted a background and that white window showed through.

The window is now painted from the chosen theme before the first frame, and
kept in step when the theme changes, which a static XML theme could not do for
sepia or OLED. Load paints its own loading and error states too.

Android 12+ draws its splash before any app code runs, so that one can only
follow the system light/dark setting; picking sepia or OLED on a light-mode
phone still shows a light splash for a frame.

Verified on an API 36 emulator under the OLED theme: the loading screen is now
true black with the themed spinner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anas Rashid 2026-10-04 01:14:02 +02:00
parent a5fc608bca
commit b545220bbc
9 changed files with 96 additions and 13 deletions

View File

@ -10,6 +10,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.core.graphics.drawable.toDrawable
import com.ganjoor.android.data.Bookmarks import com.ganjoor.android.data.Bookmarks
import com.ganjoor.android.data.Ganjoor import com.ganjoor.android.data.Ganjoor
import com.ganjoor.android.data.LocalBookmarks import com.ganjoor.android.data.LocalBookmarks
@ -17,6 +18,7 @@ import com.ganjoor.android.ui.GanjoorApp
import com.ganjoor.android.ui.LocalSettings import com.ganjoor.android.ui.LocalSettings
import com.ganjoor.android.ui.Settings import com.ganjoor.android.ui.Settings
import com.ganjoor.android.ui.theme.GanjoorTheme import com.ganjoor.android.ui.theme.GanjoorTheme
import com.ganjoor.android.ui.theme.windowBackground
import java.util.Locale import java.util.Locale
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -35,9 +37,19 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Ganjoor.init(applicationContext) Ganjoor.init(applicationContext)
// Before the first frame: otherwise the window keeps the platform's white through
// startup and every screen transition, whatever theme is chosen.
val settings = Settings(applicationContext)
val systemInDark = resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
window.setBackgroundDrawable(
windowBackground(settings.value.theme, systemInDark).toDrawable()
)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
val settings = remember { Settings(applicationContext) } val settings = remember { settings }
val bookmarks = remember { Bookmarks(applicationContext) } val bookmarks = remember { Bookmarks(applicationContext) }
// The client reads this flag on every request, so keep it in step with the setting. // The client reads this flag on every request, so keep it in step with the setting.
Ganjoor.offline = settings.value.offline Ganjoor.offline = settings.value.offline

View File

@ -1,5 +1,6 @@
package com.ganjoor.android.ui package com.ganjoor.android.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -36,12 +37,23 @@ fun <T> Load(key: Any?, block: suspend () -> T, content: @Composable (T) -> Unit
result.let { outcome -> result.let { outcome ->
when { when {
outcome == null -> Box(Modifier.fillMaxSize(), Alignment.Center) { // Painted explicitly: on the category and poem screens Load sits outside the
// Scaffold, so while loading nothing else fills the window and the bare window
// background would show through.
outcome == null -> Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator() CircularProgressIndicator()
} }
outcome.isFailure -> Column( outcome.isFailure -> Column(
modifier = Modifier.fillMaxSize().padding(24.dp), modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(24.dp),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {

View File

@ -8,7 +8,9 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.ganjoor.android.ui.Language import com.ganjoor.android.ui.Language
import com.ganjoor.android.ui.ThemeMode import com.ganjoor.android.ui.ThemeMode
@ -146,6 +148,20 @@ private val SepiaDarkScheme = darkColorScheme(
outline = Color(0xFF968A72), outline = Color(0xFF968A72),
) )
/**
* The window's own background colour for a theme. The activity paints this before Compose runs,
* so the gap between the window appearing and the first frame matches the theme instead of
* flashing the platform's white. A static XML theme can't express sepia or OLED, hence this.
*/
fun windowBackground(mode: ThemeMode, systemInDark: Boolean): Int = when (mode) {
ThemeMode.Light -> LightScheme
ThemeMode.Dark -> DarkScheme
ThemeMode.Sepia -> SepiaScheme
ThemeMode.SepiaDark -> SepiaDarkScheme
ThemeMode.Black -> BlackScheme
ThemeMode.System -> if (systemInDark) DarkScheme else LightScheme
}.background.toArgb()
@Composable @Composable
fun GanjoorTheme(mode: ThemeMode, language: Language, content: @Composable () -> Unit) { fun GanjoorTheme(mode: ThemeMode, language: Language, content: @Composable () -> Unit) {
val dark = when (mode) { val dark = when (mode) {
@ -164,6 +180,9 @@ fun GanjoorTheme(mode: ThemeMode, language: Language, content: @Composable () ->
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
val window = (view.context as Activity).window val window = (view.context as Activity).window
// Keep the window in step when the theme changes, so switching themes doesn't leave
// a stale colour behind transitions.
window.setBackgroundDrawable(scheme.background.toArgb().toDrawable())
WindowCompat.getInsetsController(window, view).apply { WindowCompat.getInsetsController(window, view).apply {
isAppearanceLightStatusBars = !dark isAppearanceLightStatusBars = !dark
isAppearanceLightNavigationBars = !dark isAppearanceLightNavigationBars = !dark

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Android 12+ draws its own splash from these attributes, before any of our code runs, so it
can only follow the system's light/dark setting — not the theme chosen inside the app.
Picking sepia or OLED on a light-mode phone still shows a light splash for one frame.
-->
<style name="Theme.Ganjoorappandroid" parent="android:Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/window_background</item>
<item name="android:windowSplashScreenBackground">@color/window_background</item>
</style>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Matches DarkScheme.background in Theme.kt. -->
<color name="window_background">#191C1C</color>
</resources>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Ganjoorappandroid" parent="android:Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/window_background</item>
</style>
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Android 12+ draws its own splash from these attributes, before any of our code runs, so it
can only follow the system's light/dark setting — not the theme chosen inside the app.
Picking sepia or OLED on a light-mode phone still shows a light splash for one frame.
-->
<style name="Theme.Ganjoorappandroid" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:windowBackground">@color/window_background</item>
<item name="android:windowSplashScreenBackground">@color/window_background</item>
</style>
</resources>

View File

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="purple_200">#FFBB86FC</color> <!-- Matches LightScheme.background in Theme.kt. -->
<color name="purple_500">#FF6200EE</color> <color name="window_background">#FAFDFC</color>
<color name="purple_700">#FF3700B3</color> </resources>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Ganjoorappandroid" parent="android:Theme.Material.Light.NoActionBar" /> <!--
</resources> The starting window Android shows before onCreate runs. MainActivity immediately repaints
it from the chosen theme, so this only has to cover light vs dark; values-night supplies
the other half.
-->
<style name="Theme.Ganjoorappandroid" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:windowBackground">@color/window_background</item>
</style>
</resources>