From 4dc933ee5a361eb0535120fa192c1eb51ba8cc70 Mon Sep 17 00:00:00 2001 From: Rajarshee Chatterjee Date: Wed, 29 Jul 2026 21:27:22 +0530 Subject: [PATCH] fix: Remove White Flash During Screen Transitions (Closes #1926) (#1927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AppTheme` never set `android:windowBackground`, so it was inherited from `Theme.AppCompat.DayNight.NoActionBar` and resolved to a near-white colour whenever the system was in light mode — independently of the theme picked inside the app. Nothing between that window and the navigators painted a background of its own: the root view, `GestureHandlerRootView` and `SafeAreaProvider` are all transparent, and only the individual screens carry `contentStyle`. Any frame in which no screen was drawn — mounting a nested navigator such as `MoreStack` or `ReaderStack`, or freezing the screen being left — therefore showed the white window through. Paint the root view with the active theme background so the window stays covered, and give `AppTheme` a day/night `android:windowBackground` so the frames before React draws are themed as well. The chapter screen additionally uses the reader background instead of the app background, so pushing it no longer flashes the app theme before the reader appears. Claude-Session: https://claude.ai/code/session_014uxuC7JVkShhWnN1WhkTaH Co-authored-by: Claude --- App.tsx | 28 ++++++++++++--- plugins/withAndroidCustomizations.js | 53 ++++++++++++++++++++++++++++ src/navigators/ReaderStack.tsx | 11 ++++-- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/App.tsx b/App.tsx index d7a1990b..df4f9a1b 100644 --- a/App.tsx +++ b/App.tsx @@ -26,6 +26,26 @@ import { ThemeProvider, useTheme } from '@hooks/persisted/useTheme'; enableFreeze(true); const sqliteAdapters = __DEV__ && opSqliteAdapter ? [opSqliteAdapter] : []; +/** + * The Android window background is resolved from the system light/dark setting, + * so it is bright white whenever the system is light – regardless of the theme + * picked inside the app. Nothing between it and the navigators paints a + * background of its own, so every frame in which no screen is drawn (mounting a + * nested navigator, freezing the screen being left) shows through as a flash. + * Painting the root view keeps the window covered at all times. + */ +const ThemedRootView = ({ children }: PropsWithChildren) => { + const theme = useTheme(); + + return ( + + {children} + + ); +}; + const ThemedPaperProvider = ({ children }: PropsWithChildren) => { const theme = useTheme(); const paperTheme = useMemo(() => { @@ -71,8 +91,8 @@ const App = () => { return ( - - + + @@ -83,8 +103,8 @@ const App = () => { - - + + ); }; diff --git a/plugins/withAndroidCustomizations.js b/plugins/withAndroidCustomizations.js index 38c63c1b..4b509917 100644 --- a/plugins/withAndroidCustomizations.js +++ b/plugins/withAndroidCustomizations.js @@ -1,4 +1,8 @@ const { + AndroidConfig, + withAndroidColors, + withAndroidColorsNight, + withAndroidStyles, withAppBuildGradle, withDangerousMod, } = require('@expo/config-plugins'); @@ -35,6 +39,53 @@ const DRAWABLE_RESOURCE_NAMES = [ ]; const ANDROID_DENSITIES = ['mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi']; +/** + * `AppTheme` inherits `android:windowBackground` from + * `Theme.AppCompat.DayNight.NoActionBar`, which resolves to a near-white colour + * whenever the system is in light mode. The window is visible for the frames + * between the splash screen going away and React drawing its first frame, so + * the app opens with a white flash for anyone using a dark theme on a light + * system. These are the backgrounds of the default light and dark themes. + */ +const WINDOW_BACKGROUND_RESOURCE = 'lnreaderWindowBackground'; +const WINDOW_BACKGROUND_LIGHT = '#FEFBFF'; +const WINDOW_BACKGROUND_DARK = '#1B1B1F'; + +const withWindowBackgroundColors = config => { + config = withAndroidColors(config, config => { + config.modResults = AndroidConfig.Colors.assignColorValue( + config.modResults, + { name: WINDOW_BACKGROUND_RESOURCE, value: WINDOW_BACKGROUND_LIGHT }, + ); + + return config; + }); + + return withAndroidColorsNight(config, config => { + config.modResults = AndroidConfig.Colors.assignColorValue( + config.modResults, + { name: WINDOW_BACKGROUND_RESOURCE, value: WINDOW_BACKGROUND_DARK }, + ); + + return config; + }); +}; + +const withWindowBackgroundStyle = config => + withAndroidStyles(config, config => { + config.modResults = AndroidConfig.Styles.assignStylesValue( + config.modResults, + { + add: true, + parent: AndroidConfig.Styles.getAppThemeGroup(), + name: 'android:windowBackground', + value: `@color/${WINDOW_BACKGROUND_RESOURCE}`, + }, + ); + + return config; + }); + const withBuildGradleCustomizations = config => withAppBuildGradle(config, config => { const { contents } = config.modResults; @@ -133,5 +184,7 @@ const withAndroidVariantAssets = config => module.exports = config => { config = withBuildGradleCustomizations(config); + config = withWindowBackgroundColors(config); + config = withWindowBackgroundStyle(config); return withAndroidVariantAssets(config); }; diff --git a/src/navigators/ReaderStack.tsx b/src/navigators/ReaderStack.tsx index 5bb8e04a..59e2e62f 100644 --- a/src/navigators/ReaderStack.tsx +++ b/src/navigators/ReaderStack.tsx @@ -12,7 +12,7 @@ import { ReaderStackParamList, } from './types'; import { NovelContextProvider } from '@screens/novel/NovelContext'; -import { useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; const Stack = createNativeStackNavigator(); @@ -20,6 +20,9 @@ const Stack = createNativeStackNavigator(); const ReaderStack = ({ route }) => { const params = useRef(route?.params); const theme = useTheme(); + // The reader has a background of its own, so the screen is given the same one + // to keep the app background from showing while the chapter is pushed. + const { theme: readerBackground } = useChapterReaderSettings(); // eslint-disable-next-line react-hooks/refs const routeParams = route?.params ?? params.current; @@ -36,7 +39,11 @@ const ReaderStack = ({ route }) => { }} > - + );