fix: Remove White Flash During Screen Transitions (Closes #1926) (#1927)

`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 <noreply@anthropic.com>
This commit is contained in:
Rajarshee Chatterjee
2026-07-29 21:27:22 +05:30
committed by GitHub
parent bfb12b04aa
commit 4dc933ee5a
3 changed files with 86 additions and 6 deletions
+24 -4
View File
@@ -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 (
<GestureHandlerRootView
style={[styles.flex, { backgroundColor: theme.background }]}
>
{children}
</GestureHandlerRootView>
);
};
const ThemedPaperProvider = ({ children }: PropsWithChildren) => {
const theme = useTheme();
const paperTheme = useMemo(() => {
@@ -71,8 +91,8 @@ const App = () => {
return (
<Suspense fallback={null}>
<GestureHandlerRootView style={styles.flex}>
<ThemeProvider>
<ThemeProvider>
<ThemedRootView>
<AppErrorBoundary>
<SafeAreaProvider>
<ThemedPaperProvider>
@@ -83,8 +103,8 @@ const App = () => {
</ThemedPaperProvider>
</SafeAreaProvider>
</AppErrorBoundary>
</ThemeProvider>
</GestureHandlerRootView>
</ThemedRootView>
</ThemeProvider>
</Suspense>
);
};
+53
View File
@@ -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);
};
+9 -2
View File
@@ -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<ReaderStackParamList>();
@@ -20,6 +20,9 @@ const Stack = createNativeStackNavigator<ReaderStackParamList>();
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 }) => {
}}
>
<Stack.Screen name="Novel" component={Novel} />
<Stack.Screen name="Chapter" component={Reader} />
<Stack.Screen
name="Chapter"
component={Reader}
options={{ contentStyle: { backgroundColor: readerBackground } }}
/>
</Stack.Navigator>
</NovelContextProvider>
);