Created
March 11, 2024 21:49
-
-
Save devethan/cb0ebddae25ff10cccc7ff3de580ce95 to your computer and use it in GitHub Desktop.
iOS context menu in React-native
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {useState} from 'react'; | |
import { | |
Animated, | |
Image, | |
ImageBackground, | |
StyleSheet, | |
Pressable, | |
Text, | |
View, | |
} from 'react-native'; | |
import BackgroundImage from '../assets/background_image.png'; | |
import Logo from '../assets/logo.png'; | |
const ELEMENT_SIZE = 200; | |
const ELEMENT_START_SCALE_SIZE = 1.15; | |
const ELEMENT_FINISH_SCALE_SIZE = 1.1; | |
const MENU_SCALE_SIZE = 1.03; | |
export const SampleScreen = () => { | |
const [maskOpacity] = useState(new Animated.Value(0)); | |
const [maskScale] = useState(new Animated.Value(1)); | |
const [elementScale] = useState(new Animated.Value(1)); | |
const [elementBorderRadius] = useState(new Animated.Value(0)); | |
const [menuOpacity] = useState(new Animated.Value(0)); | |
const [menuScale] = useState(new Animated.Value(1)); | |
const animatedEffects = (isFocus: boolean) => | |
Animated.parallel([ | |
Animated.timing(elementBorderRadius, { | |
toValue: isFocus ? 20 : 0, | |
duration: 250, | |
useNativeDriver: true, | |
}), | |
Animated.timing(maskOpacity, { | |
toValue: isFocus ? 1 : 0, | |
duration: 250, | |
useNativeDriver: true, | |
}), | |
Animated.spring(elementScale, { | |
toValue: isFocus ? ELEMENT_FINISH_SCALE_SIZE : 1, | |
friction: 3, | |
tension: 25, | |
useNativeDriver: true, | |
}), | |
Animated.timing(maskScale, { | |
toValue: isFocus ? 1.1 : 1, | |
duration: 250, | |
useNativeDriver: true, | |
}), | |
Animated.sequence([ | |
Animated.delay(isFocus ? 125 : 0), | |
Animated.timing(menuOpacity, { | |
toValue: isFocus ? 1 : 0, | |
duration: isFocus ? 250 : 125, | |
useNativeDriver: true, | |
}), | |
Animated.spring(menuScale, { | |
toValue: isFocus ? MENU_SCALE_SIZE : 1, | |
friction: 3, | |
tension: 60, | |
useNativeDriver: true, | |
}), | |
]), | |
]); | |
const handleLongPress = () => { | |
console.log('Long press event'); | |
Animated.parallel([ | |
Animated.timing(elementScale, { | |
toValue: ELEMENT_START_SCALE_SIZE, | |
duration: 150, | |
useNativeDriver: true, | |
}), | |
Animated.timing(elementBorderRadius, { | |
toValue: 20, | |
duration: 250, | |
useNativeDriver: true, | |
}), | |
animatedEffects(true), | |
]).start(); | |
}; | |
const handleFocus = () => { | |
console.log('Press event'); | |
}; | |
const handleBlur = () => { | |
console.log('Blur event'); | |
animatedEffects(false).start(); | |
}; | |
return ( | |
<View style={styles.container}> | |
<ImageBackground | |
style={styles.backgroundContainer} | |
source={BackgroundImage}> | |
{/* Masked view which has blur effect */} | |
<Animated.View style={[styles.maskedContainer, {opacity: maskOpacity}]}> | |
<Pressable style={styles.maskImage} onPress={handleBlur}> | |
<Animated.View | |
style={[styles.maskImage, {transform: [{scale: maskScale}]}]}> | |
<ImageBackground | |
style={styles.maskImage} | |
source={BackgroundImage} | |
blurRadius={10} | |
/> | |
</Animated.View> | |
</Pressable> | |
</Animated.View> | |
{/* Target element view */} | |
<View style={styles.content}> | |
<Pressable onPress={handleFocus} onLongPress={handleLongPress}> | |
<Animated.View | |
style={[ | |
styles.element, | |
{ | |
borderRadius: elementBorderRadius, | |
transform: [{scale: elementScale}], | |
}, | |
]}> | |
<Image source={Logo} style={styles.logoImage} /> | |
</Animated.View> | |
</Pressable> | |
{/* Menu view */} | |
<Animated.View | |
style={[ | |
styles.menuContainer, | |
{opacity: menuOpacity, transform: [{scale: menuScale}]}, | |
]}> | |
<MenuItem>Rename</MenuItem> | |
<View style={styles.divider} /> | |
<MenuItem>Favourite</MenuItem> | |
<View style={styles.divider} /> | |
<MenuItem>Share</MenuItem> | |
<View style={styles.divider} /> | |
<MenuItem color={'red'}>Delete</MenuItem> | |
</Animated.View> | |
</View> | |
</ImageBackground> | |
</View> | |
); | |
}; | |
const MenuItem = ({ | |
children, | |
color = '#000', | |
}: { | |
children: string; | |
color?: string; | |
}) => { | |
return ( | |
<View style={styles.menuItem}> | |
<Text style={[styles.menuLabel, {color}]}>{children}</Text> | |
</View> | |
); | |
}; | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
}, | |
backgroundContainer: { | |
flex: 1, | |
position: 'relative', | |
alignItems: 'center', | |
justifyContent: 'center', | |
}, | |
maskedContainer: { | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
right: 0, | |
bottom: 0, | |
}, | |
maskImage: { | |
flex: 1, | |
}, | |
content: { | |
position: 'relative', | |
}, | |
element: { | |
zIndex: 2, | |
overflow: 'hidden', | |
}, | |
logoImage: { | |
width: ELEMENT_SIZE, | |
height: ELEMENT_SIZE, | |
}, | |
menuContainer: { | |
position: 'absolute', | |
top: ELEMENT_SIZE * ELEMENT_FINISH_SCALE_SIZE + 8, | |
right: (-1 * (ELEMENT_SIZE * (ELEMENT_FINISH_SCALE_SIZE - 1))) / 2, | |
width: 180, | |
backgroundColor: 'white', | |
borderRadius: 20, | |
paddingVertical: 4, | |
}, | |
menuItem: { | |
paddingHorizontal: 16, | |
paddingVertical: 12, | |
}, | |
menuLabel: { | |
fontSize: 14, | |
fontWeight: '400', | |
}, | |
divider: { | |
backgroundColor: '#DBDBDD', | |
height: 1, | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment