Created
October 9, 2025 05:33
-
-
Save ashaffah/14f7f9381e6d1d8678be456ec25463fb to your computer and use it in GitHub Desktop.
React Native Expo Camera Modal Component
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 { | |
| StyleSheet, | |
| Text, | |
| TouchableOpacity, | |
| View, | |
| Modal, | |
| Animated, | |
| Dimensions, | |
| } from 'react-native'; | |
| import React, {useRef, useState, useEffect} from 'react'; | |
| import {CameraView, type FlashMode} from 'expo-camera'; | |
| import IonicIcons from 'react-native-vector-icons/Ionicons'; | |
| export default function CameraModal({visible, onClose, onBarcodeScanned}: any) { | |
| const cameraRef = useRef<null>(null); | |
| const [flash, setFlash] = useState<FlashMode>('off'); | |
| const scanLineAnim = useRef(new Animated.Value(0)).current; | |
| const screenWidth = Dimensions.get('window').width; | |
| const screenHeight = Dimensions.get('window').height; | |
| const scanPositionTop = (screenHeight - SCAN_AREA_SIZE) / 2; | |
| const scanPositionLeft = (screenWidth - SCAN_AREA_SIZE) / 2; | |
| useEffect(() => { | |
| if (visible) { | |
| Animated.loop( | |
| Animated.sequence([ | |
| Animated.timing(scanLineAnim, { | |
| toValue: 1, | |
| duration: 1500, | |
| useNativeDriver: true, | |
| }), | |
| Animated.timing(scanLineAnim, { | |
| toValue: 0, | |
| duration: 1500, | |
| useNativeDriver: true, | |
| }), | |
| ]), | |
| ).start(); | |
| } else { | |
| scanLineAnim.setValue(0); | |
| } | |
| }, [visible, scanLineAnim]); | |
| const scanLineTranslate = scanLineAnim.interpolate({ | |
| inputRange: [0, 1], | |
| outputRange: [0, SCAN_AREA_SIZE - 40], | |
| }); | |
| return ( | |
| <Modal visible={visible} animationType="slide" transparent={false}> | |
| <CameraView | |
| // eslint-disable-next-line react-native/no-inline-styles | |
| style={{flex: 1}} | |
| ref={cameraRef} | |
| enableTorch={flash === 'on'} | |
| barcodeScannerSettings={{ | |
| barcodeTypes: ['qr'], | |
| }} | |
| onBarcodeScanned={res => { | |
| onBarcodeScanned(res.data); | |
| }}> | |
| {/* Overlay for dimming outside scan area */} | |
| <View style={styles.overlay}> | |
| <View style={[styles.topOverlay, {height: scanPositionTop + -0.8}]} /> | |
| <View | |
| style={[styles.bottomOverlay, {height: scanPositionTop + -1.2}]} | |
| /> | |
| <View | |
| style={[ | |
| styles.leftOverlay, | |
| { | |
| top: scanPositionTop - 1, | |
| height: SCAN_AREA_SIZE + 2, | |
| width: scanPositionLeft + 1, | |
| }, | |
| ]} | |
| /> | |
| <View | |
| style={[ | |
| styles.rightOverlay, | |
| { | |
| top: scanPositionTop - 1, | |
| height: SCAN_AREA_SIZE + 2, | |
| width: scanPositionLeft + 1, | |
| }, | |
| ]} | |
| /> | |
| <View | |
| style={[ | |
| styles.scanArea, | |
| {top: scanPositionTop, left: scanPositionLeft}, | |
| ]}> | |
| {/* Corner borders for scan area */} | |
| <View style={[styles.corner, styles.topLeft]} /> | |
| <View style={[styles.corner, styles.topRight]} /> | |
| <View style={[styles.corner, styles.bottomLeft]} /> | |
| <View style={[styles.corner, styles.bottomRight]} /> | |
| {/* Animated scan line */} | |
| <Animated.View | |
| style={[ | |
| styles.scanLine, | |
| {transform: [{translateY: scanLineTranslate}]}, | |
| ]} | |
| /> | |
| </View> | |
| </View> | |
| {/* Instructions */} | |
| <View style={styles.instructionsContainer}> | |
| <Text style={styles.instructions}>Scan QR Code</Text> | |
| </View> | |
| {/* Controls */} | |
| <View style={styles.controls}> | |
| <TouchableOpacity | |
| style={styles.button} | |
| onPress={() => setFlash(flash === 'off' ? 'on' : 'off')}> | |
| <IonicIcons | |
| name={flash === 'off' ? 'flash-outline' : 'flash'} | |
| size={24} | |
| color={'white'} | |
| /> | |
| <Text style={styles.buttonText}>Flash</Text> | |
| </TouchableOpacity> | |
| <TouchableOpacity style={styles.button} onPress={onClose}> | |
| <IonicIcons name="close-outline" size={24} color={'white'} /> | |
| <Text style={styles.buttonText}>Close</Text> | |
| </TouchableOpacity> | |
| </View> | |
| </CameraView> | |
| </Modal> | |
| ); | |
| } | |
| const SCAN_AREA_SIZE = 280; | |
| const styles = StyleSheet.create({ | |
| overlay: { | |
| ...StyleSheet.absoluteFillObject, | |
| }, | |
| topOverlay: { | |
| position: 'absolute', | |
| top: 0, | |
| left: 0, | |
| right: 0, | |
| backgroundColor: 'rgba(0,0,0,0.6)', | |
| }, | |
| bottomOverlay: { | |
| position: 'absolute', | |
| bottom: 0, | |
| left: 0, | |
| right: 0, | |
| backgroundColor: 'rgba(0,0,0,0.6)', | |
| }, | |
| leftOverlay: { | |
| position: 'absolute', | |
| left: 0, | |
| backgroundColor: 'rgba(0,0,0,0.6)', | |
| }, | |
| rightOverlay: { | |
| position: 'absolute', | |
| right: 0, | |
| backgroundColor: 'rgba(0,0,0,0.6)', | |
| }, | |
| scanArea: { | |
| position: 'absolute', | |
| width: SCAN_AREA_SIZE, | |
| height: SCAN_AREA_SIZE, | |
| justifyContent: 'center', | |
| alignItems: 'center', | |
| overflow: 'hidden', | |
| }, | |
| corner: { | |
| position: 'absolute', | |
| width: 30, | |
| height: 30, | |
| borderColor: 'white', | |
| }, | |
| topLeft: { | |
| top: 0, | |
| left: 0, | |
| borderTopWidth: 4, | |
| borderLeftWidth: 4, | |
| }, | |
| topRight: { | |
| top: 0, | |
| right: 0, | |
| borderTopWidth: 4, | |
| borderRightWidth: 4, | |
| }, | |
| bottomLeft: { | |
| bottom: 0, | |
| left: 0, | |
| borderBottomWidth: 4, | |
| borderLeftWidth: 4, | |
| }, | |
| bottomRight: { | |
| bottom: 0, | |
| right: 0, | |
| borderBottomWidth: 4, | |
| borderRightWidth: 4, | |
| }, | |
| scanLine: { | |
| position: 'absolute', | |
| width: SCAN_AREA_SIZE - 40, | |
| height: 2, | |
| backgroundColor: 'white', | |
| left: 20, | |
| top: 20, | |
| shadowColor: 'white', | |
| shadowOffset: {width: 0, height: 0}, | |
| shadowOpacity: 0.8, | |
| shadowRadius: 8, | |
| elevation: 5, | |
| }, | |
| instructionsContainer: { | |
| position: 'absolute', | |
| top: 40, | |
| width: '100%', | |
| alignItems: 'center', | |
| }, | |
| instructions: { | |
| color: 'white', | |
| fontSize: 20, | |
| fontWeight: 'bold', | |
| textShadowColor: 'black', | |
| textShadowRadius: 2, | |
| }, | |
| controls: { | |
| position: 'absolute', | |
| bottom: 40, | |
| flexDirection: 'row', | |
| justifyContent: 'space-around', | |
| width: '100%', | |
| alignItems: 'center', | |
| }, | |
| button: { | |
| alignItems: 'center', | |
| padding: 10, | |
| borderRadius: 8, | |
| }, | |
| buttonText: { | |
| color: 'white', | |
| fontSize: 14, | |
| marginTop: 4, | |
| }, | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment