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.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.core.graphics.drawable.toDrawable
import com.ganjoor.android.data.Bookmarks
import com.ganjoor.android.data.Ganjoor
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.Settings
import com.ganjoor.android.ui.theme.GanjoorTheme
import com.ganjoor.android.ui.theme.windowBackground
import java.util.Locale
class MainActivity : ComponentActivity() {
@ -35,9 +37,19 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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()
setContent {
val settings = remember { Settings(applicationContext) }
val settings = remember { settings }
val bookmarks = remember { Bookmarks(applicationContext) }
// The client reads this flag on every request, so keep it in step with the setting.
Ganjoor.offline = settings.value.offline

View File

@ -1,5 +1,6 @@
package com.ganjoor.android.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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 ->
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()
}
outcome.isFailure -> Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {

View File

@ -8,7 +8,9 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.WindowCompat
import com.ganjoor.android.ui.Language
import com.ganjoor.android.ui.ThemeMode
@ -146,6 +148,20 @@ private val SepiaDarkScheme = darkColorScheme(
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
fun GanjoorTheme(mode: ThemeMode, language: Language, content: @Composable () -> Unit) {
val dark = when (mode) {
@ -164,6 +180,9 @@ fun GanjoorTheme(mode: ThemeMode, language: Language, content: @Composable () ->
if (!view.isInEditMode) {
SideEffect {
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 {
isAppearanceLightStatusBars = !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"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Matches LightScheme.background in Theme.kt. -->
<color name="window_background">#FAFDFC</color>
</resources>

View File

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Ganjoorappandroid" parent="android:Theme.Material.Light.NoActionBar" />
<!--
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>