import Ionicons from '@expo/vector-icons/build/Ionicons'; import MaterialCommunityIcons from '@expo/vector-icons/build/MaterialCommunityIcons'; import { BarCodeScanner } from 'expo-barcode-scanner'; import { AutoFocus, BarCodePoint, BarCodeScanningResult, Camera, CameraCapturedPicture, CameraType, FlashMode, PermissionStatus, WhiteBalance, } from 'expo-camera'; import * as FileSystem from 'expo-file-system'; import React from 'react'; import { Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import * as Svg from 'react-native-svg'; import GalleryScreen from './GalleryScreen'; import { face, landmarks } from '../../components/Face'; const flashModeOrder = { off: FlashMode.on, on: FlashMode.auto, auto: FlashMode.torch, torch: FlashMode.off, }; const flashIcons: Record = { off: 'flash-off', on: 'flash', auto: 'flash-outline', torch: 'flashlight', }; const wbOrder: Record = { auto: WhiteBalance.sunny, sunny: WhiteBalance.cloudy, cloudy: WhiteBalance.shadow, shadow: WhiteBalance.fluorescent, fluorescent: WhiteBalance.incandescent, incandescent: WhiteBalance.auto, }; const wbIcons: Record = { auto: 'white-balance-auto', sunny: 'white-balance-sunny', cloudy: 'cloud', shadow: 'umbrella-beach', fluorescent: 'white-balance-iridescent', incandescent: 'white-balance-incandescent', }; const photos: CameraCapturedPicture[] = []; interface State { flash: FlashMode; zoom: number; autoFocus: AutoFocus; type: CameraType; depth: number; whiteBalance: WhiteBalance; ratio: string; ratios: any[]; barcodeScanning: boolean; faceDetecting: boolean; faces: any[]; cornerPoints?: BarCodePoint[]; barcodeData: string; newPhotos: boolean; permissionsGranted: boolean; permission?: PermissionStatus; pictureSize?: any; pictureSizes: any[]; pictureSizeId: number; showGallery: boolean; showMoreOptions: boolean; mode: string; recording: boolean; } export default class CameraScreen extends React.Component { readonly state: State = { flash: FlashMode.off, zoom: 0, autoFocus: AutoFocus.on, type: CameraType.back, depth: 0, whiteBalance: WhiteBalance.auto, ratio: '16:9', ratios: [], barcodeScanning: false, faceDetecting: false, faces: [], cornerPoints: undefined, barcodeData: '', newPhotos: false, permissionsGranted: false, pictureSizes: [], pictureSizeId: 0, showGallery: false, showMoreOptions: false, mode: 'picture', recording: false, }; camera?: Camera; componentDidMount() { if (Platform.OS !== 'web') { this.ensureDirectoryExistsAsync(); } Camera.requestCameraPermissionsAsync().then(({ status }) => { this.setState({ permission: status, permissionsGranted: status === 'granted' }); }); } async ensureDirectoryExistsAsync() { try { await FileSystem.makeDirectoryAsync(FileSystem.documentDirectory + 'photos'); } catch { // Directory exists } } getRatios = async () => this.camera!.getSupportedRatiosAsync(); toggleView = () => this.setState((state) => ({ showGallery: !state.showGallery, newPhotos: false })); toggleMoreOptions = () => this.setState((state) => ({ showMoreOptions: !state.showMoreOptions })); toggleFacing = () => this.setState((state) => ({ type: state.type === CameraType.back ? CameraType.front : CameraType.back, })); toggleFlash = () => this.setState((state) => ({ flash: flashModeOrder[state.flash] })); setRatio = (ratio: string) => this.setState({ ratio }); toggleWB = () => this.setState((state) => ({ whiteBalance: wbOrder[state.whiteBalance] })); toggleFocus = () => this.setState((state) => ({ autoFocus: state.autoFocus === AutoFocus.on ? AutoFocus.off : AutoFocus.on, })); zoomOut = () => this.setState((state) => ({ zoom: state.zoom - 0.1 < 0 ? 0 : state.zoom - 0.1 })); zoomIn = () => this.setState((state) => ({ zoom: state.zoom + 0.1 > 1 ? 1 : state.zoom + 0.1 })); setFocusDepth = (depth: number) => this.setState({ depth }); toggleBarcodeScanning = () => this.setState((state) => ({ barcodeScanning: !state.barcodeScanning })); toggleFaceDetection = () => this.setState((state) => ({ faceDetecting: !state.faceDetecting })); takePicture = async () => { if (this.camera) { await this.camera.takePictureAsync({ onPictureSaved: this.onPictureSaved }); } }; recordVideo = async () => { if (this.camera) { this.setState((state) => ({ recording: !state.recording })); if (this.state.recording) { this.camera.stopRecording(); return Promise.resolve(); } else { return await this.camera.recordAsync(); } } }; takeVideo = async () => { const result = await this.recordVideo(); if (result?.uri) { await FileSystem.moveAsync({ from: result.uri, to: `${FileSystem.documentDirectory}photos/${Date.now()}.${result.uri.split('.')[1]}`, }); } }; changeMode = () => { this.setState((state) => ({ mode: state.mode === 'picture' ? 'video' : 'picture' })); }; handleMountError = ({ message }: { message: string }) => console.error(message); onPictureSaved = async (photo: CameraCapturedPicture) => { if (Platform.OS === 'web') { photos.push(photo); } else { await FileSystem.moveAsync({ from: photo.uri, to: `${FileSystem.documentDirectory}photos/${Date.now()}.jpg`, }); } this.setState({ newPhotos: true }); }; onBarCodeScanned = (code: BarCodeScanningResult) => { console.log('Found: ', code); this.setState(() => ({ barcodeData: code.data, cornerPoints: code.cornerPoints, })); }; onFacesDetected = ({ faces }: { faces: any }) => this.setState({ faces }); collectPictureSizes = async () => { if (this.camera) { const { ratio } = this.state; const pictureSizes = (await this.camera.getAvailablePictureSizesAsync(ratio)) || []; let pictureSizeId = 0; if (Platform.OS === 'ios') { pictureSizeId = pictureSizes.indexOf('High'); } else { // returned array is sorted in ascending order - default size is the largest one pictureSizeId = pictureSizes.length - 1; } this.setState({ pictureSizes, pictureSizeId, pictureSize: pictureSizes[pictureSizeId] }); } }; previousPictureSize = () => this.changePictureSize(1); nextPictureSize = () => this.changePictureSize(-1); changePictureSize = (direction: number) => { this.setState((state) => { let newId = state.pictureSizeId + direction; const length = state.pictureSizes.length; if (newId >= length) { newId = 0; } else if (newId < 0) { newId = length - 1; } return { pictureSize: state.pictureSizes[newId], pictureSizeId: newId, }; }); }; renderGallery() { return ; } renderFaces = () => ( {this.state.faces.map(face)} ); renderLandmarks = () => ( {this.state.faces.map(landmarks)} ); renderNoPermissions = () => ( {this.state.permission && ( Permission {this.state.permission.toLowerCase()}! You'll need to enable the camera permission to continue. )} ); renderTopBar = () => ( AF ); renderBottomBar = () => ( {this.state.recording ? ( ) : ( )} {this.state.newPhotos && } ); renderMoreOptions = () => ( Picture quality {this.state.pictureSize} ); renderBarCode = () => { const origin: BarCodePoint | undefined = this.state.cornerPoints ? this.state.cornerPoints[0] : undefined; return ( {origin && ( {this.state.barcodeData} )} `${coord.x},${coord.y}`).join(' ')} stroke="green" strokeWidth={10} /> ); }; renderCamera = () => ( (this.camera = ref!)} style={styles.camera} onCameraReady={this.collectPictureSizes} type={this.state.type} flashMode={FlashMode[this.state.flash]} autoFocus={AutoFocus[this.state.autoFocus]} zoom={this.state.zoom} whiteBalance={WhiteBalance[this.state.whiteBalance]} ratio={this.state.ratio} pictureSize={this.state.pictureSize} onMountError={this.handleMountError} onFacesDetected={this.state.faceDetecting ? this.onFacesDetected : undefined} faceDetectorSettings={{ tracking: true, }} barCodeScannerSettings={{ barCodeTypes: [ BarCodeScanner.Constants.BarCodeType.qr, BarCodeScanner.Constants.BarCodeType.pdf417, ], }} onBarCodeScanned={this.state.barcodeScanning ? this.onBarCodeScanned : undefined}> {this.renderTopBar()} {this.renderBottomBar()} {this.state.faceDetecting && this.renderFaces()} {this.state.faceDetecting && this.renderLandmarks()} {this.state.barcodeScanning && this.renderBarCode()} {this.state.showMoreOptions && this.renderMoreOptions()} ); render() { const cameraScreenContent = this.state.permissionsGranted ? this.renderCamera() : this.renderNoPermissions(); const content = this.state.showGallery ? this.renderGallery() : cameraScreenContent; return {content}; } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000', }, camera: { flex: 1, justifyContent: 'space-between', }, topBar: { flex: 0.2, backgroundColor: 'transparent', flexDirection: 'row', justifyContent: 'space-around', paddingTop: 8, }, bottomBar: { paddingBottom: 12, backgroundColor: 'transparent', justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center', }, noPermissions: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 10, backgroundColor: '#f8fdff', }, gallery: { flex: 1, flexDirection: 'row', flexWrap: 'wrap', }, toggleButton: { flex: 0.25, height: 40, marginHorizontal: 2, marginBottom: 10, marginTop: 20, padding: 5, alignItems: 'center', justifyContent: 'center', }, autoFocusLabel: { fontSize: 20, fontWeight: 'bold', }, bottomButton: { flex: 0.3, height: 58, justifyContent: 'center', alignItems: 'center', }, newPhotosDot: { position: 'absolute', top: 0, right: -5, width: 8, height: 8, borderRadius: 4, backgroundColor: '#4630EB', }, options: { position: 'absolute', top: 84, right: 24, width: 200, backgroundColor: '#000000BA', borderRadius: 4, padding: 16, }, detectors: { flex: 0.5, justifyContent: 'space-around', alignItems: 'center', flexDirection: 'row', }, pictureQualityLabel: { fontSize: 10, marginVertical: 3, color: 'white', }, pictureSizeContainer: { flex: 0.5, alignItems: 'center', paddingTop: 10, }, pictureSizeChooser: { alignItems: 'center', justifyContent: 'space-between', flexDirection: 'row', }, pictureSizeLabel: { flex: 1, alignItems: 'center', justifyContent: 'center', }, facesContainer: { position: 'absolute', bottom: 0, right: 0, left: 0, top: 0, }, row: { flexDirection: 'row', }, barcode: { position: 'absolute', borderWidth: 2, borderColor: 'red', }, });