1import { ConfigPlugin, withDangerousMod } from '@expo/config-plugins'; 2import { ExpoConfig } from '@expo/config-types'; 3import { generateImageAsync } from '@expo/image-utils'; 4import fs from 'fs-extra'; 5import path from 'path'; 6 7import { 8 getAndroidDarkSplashConfig, 9 getAndroidSplashConfig, 10 SplashScreenConfig, 11} from './getAndroidSplashConfig'; 12 13type DRAWABLE_SIZE = 'default' | 'mdpi' | 'hdpi' | 'xhdpi' | 'xxhdpi' | 'xxxhdpi'; 14type THEME = 'light' | 'dark'; 15 16const IMAGE_CACHE_NAME = 'splash-android'; 17const SPLASH_SCREEN_FILENAME = 'splashscreen_image.png'; 18const DRAWABLES_CONFIGS: { 19 [key in DRAWABLE_SIZE]: { 20 modes: { 21 [key in THEME]: { 22 path: string; 23 }; 24 }; 25 dimensionsMultiplier: number; 26 }; 27} = { 28 default: { 29 modes: { 30 light: { 31 path: `./res/drawable/${SPLASH_SCREEN_FILENAME}`, 32 }, 33 dark: { 34 path: `./res/drawable-night/${SPLASH_SCREEN_FILENAME}`, 35 }, 36 }, 37 dimensionsMultiplier: 1, 38 }, 39 mdpi: { 40 modes: { 41 light: { 42 path: `./res/drawable-mdpi/${SPLASH_SCREEN_FILENAME}`, 43 }, 44 dark: { 45 path: `./res/drawable-night-mdpi/${SPLASH_SCREEN_FILENAME}`, 46 }, 47 }, 48 dimensionsMultiplier: 1, 49 }, 50 hdpi: { 51 modes: { 52 light: { 53 path: `./res/drawable-hdpi/${SPLASH_SCREEN_FILENAME}`, 54 }, 55 dark: { 56 path: `./res/drawable-night-hdpi/${SPLASH_SCREEN_FILENAME}`, 57 }, 58 }, 59 dimensionsMultiplier: 1.5, 60 }, 61 xhdpi: { 62 modes: { 63 light: { 64 path: `./res/drawable-xhdpi/${SPLASH_SCREEN_FILENAME}`, 65 }, 66 dark: { 67 path: `./res/drawable-night-xhdpi/${SPLASH_SCREEN_FILENAME}`, 68 }, 69 }, 70 dimensionsMultiplier: 2, 71 }, 72 xxhdpi: { 73 modes: { 74 light: { 75 path: `./res/drawable-xxhdpi/${SPLASH_SCREEN_FILENAME}`, 76 }, 77 dark: { 78 path: `./res/drawable-night-xxhdpi/${SPLASH_SCREEN_FILENAME}`, 79 }, 80 }, 81 dimensionsMultiplier: 3, 82 }, 83 xxxhdpi: { 84 modes: { 85 light: { 86 path: `./res/drawable-xxxhdpi/${SPLASH_SCREEN_FILENAME}`, 87 }, 88 dark: { 89 path: `./res/drawable-night-xxxhdpi/${SPLASH_SCREEN_FILENAME}`, 90 }, 91 }, 92 dimensionsMultiplier: 4, 93 }, 94}; 95 96export const withAndroidSplashImages: ConfigPlugin = (config) => { 97 return withDangerousMod(config, [ 98 'android', 99 async (config) => { 100 await setSplashImageDrawablesAsync(config, config.modRequest.projectRoot); 101 return config; 102 }, 103 ]); 104}; 105 106/** 107 * Deletes all previous splash_screen_images and copies new one to desired drawable directory. 108 * If path isn't provided then no new image is placed in drawable directories. 109 * @see https://developer.android.com/training/multiscreen/screendensities 110 * 111 * @param androidMainPath Absolute path to the main directory containing code and resources in Android project. In general that would be `android/app/src/main`. 112 */ 113export async function setSplashImageDrawablesAsync( 114 config: Pick<ExpoConfig, 'android' | 'splash'>, 115 projectRoot: string 116) { 117 await clearAllExistingSplashImagesAsync(projectRoot); 118 119 const splash = getAndroidSplashConfig(config); 120 const darkSplash = getAndroidDarkSplashConfig(config); 121 122 await Promise.all([ 123 setSplashImageDrawablesForThemeAsync(splash, 'light', projectRoot), 124 setSplashImageDrawablesForThemeAsync(darkSplash, 'dark', projectRoot), 125 ]); 126} 127 128async function clearAllExistingSplashImagesAsync(projectRoot: string) { 129 const androidMainPath = path.join(projectRoot, 'android/app/src/main'); 130 131 await Promise.all( 132 Object.values(DRAWABLES_CONFIGS).map(async ({ modes }) => { 133 await Promise.all( 134 Object.values(modes).map(async ({ path: filePath }) => { 135 if (await fs.pathExists(path.resolve(androidMainPath, filePath))) { 136 await fs.remove(path.resolve(androidMainPath, filePath)); 137 } 138 }) 139 ); 140 }) 141 ); 142} 143 144export async function setSplashImageDrawablesForThemeAsync( 145 config: SplashScreenConfig | null, 146 theme: 'dark' | 'light', 147 projectRoot: string 148) { 149 if (!config) return; 150 const androidMainPath = path.join(projectRoot, 'android/app/src/main'); 151 152 await Promise.all( 153 ['mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi'].map(async (imageKey) => { 154 // @ts-ignore 155 const image = config[imageKey]; 156 if (image) { 157 // Using this method will cache the images in `.expo` based on the properties used to generate them. 158 // this method also supports remote URLs and using the global sharp instance. 159 const { source } = await generateImageAsync({ projectRoot, cacheType: IMAGE_CACHE_NAME }, { 160 src: image, 161 } as any); 162 163 // Get output path for drawable. 164 const outputPath = path.join( 165 androidMainPath, 166 // @ts-ignore 167 DRAWABLES_CONFIGS[imageKey].modes[theme].path 168 ); 169 // Ensure directory exists. 170 const folder = path.dirname(outputPath); 171 await fs.ensureDir(folder); 172 // Write image buffer to the file system. 173 await fs.writeFile(outputPath, source); 174 } 175 return null; 176 }) 177 ); 178} 179