Skip to content

Instantly share code, notes, and snippets.

@devethan
Created March 11, 2024 21:49
Show Gist options
  • Save devethan/cb0ebddae25ff10cccc7ff3de580ce95 to your computer and use it in GitHub Desktop.
Save devethan/cb0ebddae25ff10cccc7ff3de580ce95 to your computer and use it in GitHub Desktop.
iOS context menu in React-native
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