Files
lnreader/plugins/withAndroidCustomizations.js
Rajarshee Chatterjee 4dc933ee5a 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>
2026-07-29 21:27:22 +05:30

191 lines
5.3 KiB
JavaScript

const {
AndroidConfig,
withAndroidColors,
withAndroidColorsNight,
withAndroidStyles,
withAppBuildGradle,
withDangerousMod,
} = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');
const BUILD_TYPES_MARKER = '// @generated by with-android-customizations';
const BUILD_TYPES_APPLY = `${BUILD_TYPES_MARKER}
apply from: new File(
rootDir,
"../plugins/android/android-build-types.gradle",
)
`;
const ANDROID_ASSET_VARIANTS = [
{ source: 'release', target: 'main' },
{ source: 'preview', target: 'preRelease' },
{ source: 'debug', target: 'debug' },
];
const LAUNCHER_RESOURCE_NAMES = [
'ic_launcher',
'ic_launcher_background',
'ic_launcher_foreground',
'ic_launcher_monochrome',
'ic_launcher_round',
];
const DRAWABLE_RESOURCE_NAMES = [
'notification_icon',
'splash_icon',
'splashscreen_logo',
];
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;
if (!contents.includes(BUILD_TYPES_MARKER)) {
config.modResults.contents = `${contents.trimEnd()}
${BUILD_TYPES_APPLY}`;
}
return config;
});
const removeGeneratedIconResources = resDir => {
if (!fs.existsSync(resDir)) {
return;
}
for (const resourceDirName of fs.readdirSync(resDir)) {
const resourceDir = path.join(resDir, resourceDirName);
if (!fs.statSync(resourceDir).isDirectory()) {
continue;
}
if (resourceDirName.startsWith('mipmap-')) {
for (const resourceName of LAUNCHER_RESOURCE_NAMES) {
for (const extension of ['png', 'webp', 'xml']) {
fs.rmSync(path.join(resourceDir, `${resourceName}.${extension}`), {
force: true,
});
}
}
}
if (resourceDirName.startsWith('drawable-')) {
for (const resourceName of DRAWABLE_RESOURCE_NAMES) {
for (const extension of ['png', 'webp', 'xml']) {
fs.rmSync(path.join(resourceDir, `${resourceName}.${extension}`), {
force: true,
});
}
}
}
}
};
const installVariantSplashIcons = resDir => {
for (const density of ANDROID_DENSITIES) {
const drawableDir = path.join(resDir, `drawable-${density}`);
const sourcePath = path.join(drawableDir, 'splash_icon.png');
const splashPath = path.join(drawableDir, 'splashscreen_logo.png');
const nightDrawableDir = path.join(resDir, `drawable-night-${density}`);
const nightSplashPath = path.join(
nightDrawableDir,
'splashscreen_logo.png',
);
fs.renameSync(sourcePath, splashPath);
fs.mkdirSync(nightDrawableDir, { recursive: true });
fs.copyFileSync(splashPath, nightSplashPath);
}
};
const withAndroidVariantAssets = config =>
withDangerousMod(config, [
'android',
config => {
const { projectRoot, platformProjectRoot } = config.modRequest;
for (const variant of ANDROID_ASSET_VARIANTS) {
const sourceResDir = path.join(
projectRoot,
'assets',
'android-icons',
variant.source,
'res',
);
const targetResDir = path.join(
platformProjectRoot,
'app',
'src',
variant.target,
'res',
);
removeGeneratedIconResources(targetResDir);
fs.mkdirSync(targetResDir, { recursive: true });
fs.cpSync(sourceResDir, targetResDir, { recursive: true });
installVariantSplashIcons(targetResDir);
}
return config;
},
]);
module.exports = config => {
config = withBuildGradleCustomizations(config);
config = withWindowBackgroundColors(config);
config = withWindowBackgroundStyle(config);
return withAndroidVariantAssets(config);
};