Skip to content

Instantly share code, notes, and snippets.

@cristiangu
Last active August 5, 2023 15:47
Show Gist options
  • Save cristiangu/74ba83cd4ea243cda92f5b84a6a2c889 to your computer and use it in GitHub Desktop.
Save cristiangu/74ba83cd4ea243cda92f5b84a6a2c889 to your computer and use it in GitHub Desktop.
React Native Animated Bottom Modal in a Native Stack Navigator
<RootStack.Navigator>
<RootStack.Screen name="other-screen" component={OtherScreen} />
<RootStack.Group
screenOptions={{
presentation: 'containedTransparentModal',
animation: 'slide_from_bottom',
}}>
<RootStack.Screen name="g-modal" component={GModal} />
</RootStack.Group>
</RootStack.Navigator>
// [...]
return (
<Pressable onPress={closeModal} style={{ flex: 1 }}>
<View
style={{
flex: 1,
backgroundColor: 'transparent',
justifyContent: 'flex-end',
}}>
<Pressable>
{*/ This 👆 Pressable will take in all the touch events happening inside. */}
<PanGestureHandler
onEnded={onPanGestureEnd}
onGestureEvent={onPanGestureEvent}>
{*/ [...] */}
import React, { useCallback } from 'react';
import { Animated, Pressable, View } from 'react-native';
import {
HandlerStateChangeEvent,
PanGestureHandler,
} from 'react-native-gesture-handler';
const GBottomInfiniteView = React.memo(({ translateY }: any) => {
return (
<Animated.View
style={[
{
zIndex: 0,
position: 'absolute',
width: '100%',
height: 1000,
backgroundColor: 'white',
},
{
transform: [
{
translateY: translateY.interpolate({
inputRange: [0, 1000],
outputRange: [1000, 2000],
}),
},
],
},
]}
/>
);
});
const GModal = ({ navigation }: any) => {
const translateY = React.useRef(new Animated.Value(0)).current;
const onPanGestureEvent = Animated.event(
[
{
nativeEvent: {
translationY: translateY,
},
},
],
{ useNativeDriver: true },
);
const closeModal = useCallback(() => {
navigation.goBack();
}, [navigation]);
const onPanGestureEnd = useCallback(
(event: HandlerStateChangeEvent) => {
// @ts-expect-error
if (event.nativeEvent.translationY > 150) {
closeModal();
return;
}
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
friction: 5,
tension: 10,
}).start();
},
[closeModal, translateY],
);
const translateYTransform = {
transform: [
{
translateY: translateY,
},
],
};
return (
<Pressable onPress={closeModal} style={{ flex: 1 }}>
<View
style={{
flex: 1,
backgroundColor: 'transparent',
justifyContent: 'flex-end',
}}>
<Pressable>
<PanGestureHandler
onEnded={onPanGestureEnd}
onGestureEvent={onPanGestureEvent}>
<Animated.View
style={[
{
borderTopStartRadius: 20,
borderTopEndRadius: 20,
backgroundColor: 'white',
paddingTop: 8,
paddingHorizontal: 24,
justifyContent: 'center',
alignItems: 'center',
// ios shadow
shadowColor: '#101010',
shadowOffset: { width: 4, height: 8 },
shadowOpacity: 0.25,
shadowRadius: 12,
// android shadow
elevation: 12,
},
translateYTransform,
]}>
<View
style={{ width: '100%', height: 300, backgroundColor: 'red' }}
/>
</Animated.View>
</PanGestureHandler>
</Pressable>
<GBottomInfiniteView translateY={translateY} />
</View>
</Pressable>
);
};
const GModalMemo = React.memo(GModal);
export { GModalMemo as GModal };
import React, { useCallback } from 'react';
import { Animated, View } from 'react-native';
import {
HandlerStateChangeEvent,
PanGestureHandler,
} from 'react-native-gesture-handler';
return (
<View
style={{
flex: 1,
backgroundColor: 'blue',
justifyContent: 'flex-end',
}}>
{/*
this view will cover the entire screen
when the modal is presented
*/}
</View>
);
return (
<View
style={{
flex: 1,
backgroundColor: "blue",
justifyContent: "flex-end",
}}
>
<PanGestureHandler>
<Animated.View
style={[
{
borderTopStartRadius: 20,
borderTopEndRadius: 20,
backgroundColor: "red",
paddingTop: 8,
paddingHorizontal: 24,
justifyContent: "center",
alignItems: "center",
},
]}
>
<View
style={{ width: "100%", height: 300, backgroundColor: "orange" }}
/>
</Animated.View>
</PanGestureHandler>
</View>
);
// Gesture's Y axis value, will be used to animate our content view; up and downd.
const translateY = React.useRef(new Animated.Value(0)).current;
// This will be fired at least 60 times per second with an event containing
// gesture location data including the new Y axis value.
const onPanGestureEvent = Animated.event(
[
{
nativeEvent: {
translationY: translateY,
},
},
],
{ useNativeDriver: true }
);
const closeModal = useCallback(() => {
navigation.goBack();
}, [navigation]);
// Called when the user lifted his finger from the screen.
const onPanGestureEnd = useCallback(
(event: HandlerStateChangeEvent) => {
// If the user swiped down more than 150 points, close the modal.
if (event.nativeEvent.translationY > 150) {
closeModal();
return;
}
// This is optional, you can try to see how it looks without it.
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
friction: 5,
tension: 10,
}).start();
},
[closeModal, translateY]
);
return (
<View
style={{
flex: 1,
backgroundColor: "blue",
justifyContent: "flex-end",
}}
>
<PanGestureHandler
onEnded={onPanGestureEnd}
onGestureEvent={onPanGestureEvent}
>
<Animated.View
style={[
{
borderTopStartRadius: 20,
borderTopEndRadius: 20,
backgroundColor: "red",
paddingTop: 8,
paddingHorizontal: 24,
justifyContent: "center",
alignItems: "center",
},
{
transform: [
{
translateY: translateY,
},
],
},
]}
>
<View
style={{ width: "100%", height: 300, backgroundColor: "orange" }}
/>
</Animated.View>
</PanGestureHandler>
</View>
);
const GBottomInfiniteView = React.memo(({ translateY }: any) => {
return (
<Animated.View
style={[
{
zIndex: 0,
position: 'absolute',
width: '100%',
height: 1000, // it can be any big value
backgroundColor: 'purple', // change it to `white` after you saw it on your device
},
{
transform: [
{
translateY: translateY.interpolate({
inputRange: [0, 1000],
outputRange: [1000, 2000],
}),
},
],
},
]}
/>
);
});
return (
{/* [...] */}
</PanGestureHandler>
<GBottomInfiniteView translateY={translateY} />
</View>
);
{/* [...] */}
<Animated.View
style={[
{
backgroundColor: 'white',
paddingTop: 8,
paddingHorizontal: 24,
justifyContent: 'center',
alignItems: 'center',
// ios shadow
shadowColor: '#101010',
shadowOffset: { width: 4, height: 8 },
shadowOpacity: 0.25,
shadowRadius: 12,
// android shadow
elevation: 12,
},
{
transform: [
{
translateY: translateY,
},
],
},
]}>
<View
style={{ width: '100%', height: 300, backgroundColor: 'red' }}
/>
</Animated.View>
{/* [...] */}
import { Pressable } from 'react-native';
// [...]
return (
<Pressable onPress={closeModal} style={{ flex: 1 }}>
<View
style={{
flex: 1,
backgroundColor: 'transparent',
justifyContent: 'flex-end',
}}>
<PanGestureHandler
{*/ [...] */}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment