Archived. Written for Expo SDK 52. Newer SDKs have edge-to-edge built in, so parts of this are out of date.
Finally, after years of android-side bugs and complexities, we can use kind of simple way to show the same splash screen on both platforms. Thanks expo
"dependencies": {
"expo": "^52.0.40",
"react-native-edge-to-edge": "^1.4.3",
}
Simple example
The idea is to show icon instead of fullscreen splash screen.
// app.json
{
"expo": {
"icon": "./assets/icon.png",
"userInterfaceStyle": "dark",
"androidStatusBar": {
"barStyle": "light-content",
"backgroundColor": "#1F1F20",
"translucent": true
},
"plugins": [
"expo-build-properties",
"react-native-edge-to-edge",
[
"expo-splash-screen",
{
"backgroundColor": "#1F1F20",
"image": "./assets/icon.png",
"resizeMode": "cover",
"imageWidth": 200
}
]
]
}
}
Use your onReady (fonts, feed fetching, onLayout)
// App.tsx
import * as SplashScreen from "expo-splash-screen";
SplashScreen.preventAutoHideAsync();
const App = () => {
const [ready, setReady] = useState(false);
const onReady = () => {
setReady(true);
SplashScreen.hide();
};
return (
<GestureHandlerRootView>
<NavigationContainer onReady={onReady}>
<StackNavigator />
</NavigationContainer>
<AnimatedSplash ready={ready} />
<SystemBars style="auto" />
</GestureHandlerRootView>
);
};
Animate it your way
// AnimatedSplash.tsx
import { Image } from "expo-image";
import { useEffect, useState } from "react";
import { Platform, StyleSheet } from "react-native";
import Animated, {
Easing,
runOnJS,
useAnimatedStyle,
useSharedValue,
withDelay,
withTiming,
} from "react-native-reanimated";
const SPLASH_DELAY = Platform.OS == "android" ? 1000 : 200;
const SPLASH_DURATION = 1000;
export const AnimatedSplash = ({ ready }: { ready: boolean }) => {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
// const rotation = useSharedValue(0);
const [isSplashDone, setIsSplashDone] = useState(false);
const animatedStyles = useAnimatedStyle(() => ({
transform: [
{ scale: scale.value },
// { rotateZ: `${rotation.value}deg` }
],
opacity: opacity.value,
}));
useEffect(() => {
if (ready) {
scale.value = withDelay(
SPLASH_DELAY,
withTiming(
1.5,
{ duration: SPLASH_DURATION, easing: Easing.cubic },
(finished) => {
if (finished) {
runOnJS(setIsSplashDone)(true);
}
}
)
);
opacity.value = withDelay(
SPLASH_DELAY,
withTiming(0, {
duration: SPLASH_DURATION,
easing: Easing.cubic,
})
);
}
}, [ready]);
return (
<>
{!isSplashDone && (
<Animated.View style={[animatedStyles, styles.container]}>
<Image
style={styles.image}
contentFit="cover"
source={require("@assets/icon-transparent.png")}
/>
</Animated.View>
)}
</>
);
};
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: "#1F1F20",
justifyContent: "center",
alignItems: "center",
},
image: {
width: 200,
height: 200,
},
});
Other ways
- Interesting implementation with
react-native-masked-view: https://github.com/Solarin-Johnson/masked-splash-screen