1import Foundation from '@expo/vector-icons/build/Foundation'; 2import Ionicons from '@expo/vector-icons/build/Ionicons'; 3import MaterialCommunityIcons from '@expo/vector-icons/build/MaterialCommunityIcons'; 4import MaterialIcons from '@expo/vector-icons/build/MaterialIcons'; 5import Octicons from '@expo/vector-icons/build/Octicons'; 6import { BarCodeScanner } from 'expo-barcode-scanner'; 7import { 8 AutoFocus, 9 BarCodePoint, 10 BarCodeScanningResult, 11 Camera, 12 CameraType, 13 FlashMode, 14 PermissionStatus, 15 WhiteBalance, 16} from 'expo-camera'; 17import Constants from 'expo-constants'; 18import * as FileSystem from 'expo-file-system'; 19import React from 'react'; 20import { Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; 21import { isIphoneX } from 'react-native-iphone-x-helper'; 22import * as Svg from 'react-native-svg'; 23 24import { face, landmarks } from '../../components/Face'; 25import GalleryScreen from './GalleryScreen'; 26 27interface Picture { 28 width: number; 29 height: number; 30 uri: string; 31 base64?: string; 32 exif?: any; 33} 34 35type FlashModeString = keyof typeof FlashMode; 36type AutoFocusString = keyof typeof AutoFocus; 37type WhiteBalanceString = keyof typeof WhiteBalance; 38 39const flashModeOrder: { [key: string]: FlashModeString } = { 40 off: 'on', 41 on: 'auto', 42 auto: 'torch', 43 torch: 'off', 44}; 45 46const flashIcons: { [key: string]: string } = { 47 off: 'flash-off', 48 on: 'flash', 49 auto: 'flash-outline', 50 torch: 'flashlight', 51}; 52 53const wbOrder: { [key: string]: WhiteBalanceString } = { 54 auto: 'sunny', 55 sunny: 'cloudy', 56 cloudy: 'shadow', 57 shadow: 'fluorescent', 58 fluorescent: 'incandescent', 59 incandescent: 'auto', 60}; 61 62const wbIcons: { [key: string]: string } = { 63 auto: 'wb-auto', 64 sunny: 'wb-sunny', 65 cloudy: 'wb-cloudy', 66 shadow: 'beach-access', 67 fluorescent: 'wb-iridescent', 68 incandescent: 'wb-incandescent', 69}; 70 71const photos: Picture[] = []; 72 73interface State { 74 flash: FlashModeString; 75 zoom: number; 76 autoFocus: AutoFocusString; 77 type: CameraType; 78 depth: number; 79 whiteBalance: WhiteBalanceString; 80 ratio: string; 81 ratios: any[]; 82 barcodeScanning: boolean; 83 faceDetecting: boolean; 84 faces: any[]; 85 cornerPoints?: BarCodePoint[]; 86 barcodeData: string; 87 newPhotos: boolean; 88 permissionsGranted: boolean; 89 permission?: PermissionStatus; 90 pictureSize?: any; 91 pictureSizes: any[]; 92 pictureSizeId: number; 93 showGallery: boolean; 94 showMoreOptions: boolean; 95} 96 97// See: https://github.com/expo/expo/pull/10229#discussion_r490961694 98// eslint-disable-next-line @typescript-eslint/ban-types 99export default class CameraScreen extends React.Component<{}, State> { 100 readonly state: State = { 101 flash: FlashMode.off, 102 zoom: 0, 103 autoFocus: AutoFocus.on, 104 type: CameraType.back, 105 depth: 0, 106 whiteBalance: WhiteBalance.auto, 107 ratio: '16:9', 108 ratios: [], 109 barcodeScanning: false, 110 faceDetecting: false, 111 faces: [], 112 cornerPoints: undefined, 113 barcodeData: '', 114 newPhotos: false, 115 permissionsGranted: false, 116 pictureSizes: [], 117 pictureSizeId: 0, 118 showGallery: false, 119 showMoreOptions: false, 120 }; 121 122 camera?: Camera; 123 124 componentDidMount() { 125 if (Platform.OS !== 'web') { 126 this.ensureDirectoryExistsAsync(); 127 } 128 Camera.requestCameraPermissionsAsync().then(({ status }) => { 129 this.setState({ permission: status, permissionsGranted: status === 'granted' }); 130 }); 131 } 132 133 async ensureDirectoryExistsAsync() { 134 try { 135 await FileSystem.makeDirectoryAsync(FileSystem.documentDirectory + 'photos'); 136 } catch (error) { 137 // tslint:disable-next-line no-console 138 console.log(error, 'Directory exists'); 139 } 140 } 141 142 getRatios = async () => this.camera!.getSupportedRatiosAsync(); 143 144 toggleView = () => 145 this.setState((state) => ({ showGallery: !state.showGallery, newPhotos: false })); 146 147 toggleMoreOptions = () => this.setState((state) => ({ showMoreOptions: !state.showMoreOptions })); 148 149 toggleFacing = () => 150 this.setState((state) => ({ 151 type: state.type === CameraType.back ? CameraType.front : CameraType.back, 152 })); 153 154 toggleFlash = () => this.setState((state) => ({ flash: flashModeOrder[state.flash] })); 155 156 setRatio = (ratio: string) => this.setState({ ratio }); 157 158 toggleWB = () => this.setState((state) => ({ whiteBalance: wbOrder[state.whiteBalance] })); 159 160 toggleFocus = () => 161 this.setState((state) => ({ autoFocus: state.autoFocus === 'on' ? 'off' : 'on' })); 162 163 zoomOut = () => this.setState((state) => ({ zoom: state.zoom - 0.1 < 0 ? 0 : state.zoom - 0.1 })); 164 165 zoomIn = () => this.setState((state) => ({ zoom: state.zoom + 0.1 > 1 ? 1 : state.zoom + 0.1 })); 166 167 setFocusDepth = (depth: number) => this.setState({ depth }); 168 169 toggleBarcodeScanning = () => 170 this.setState((state) => ({ barcodeScanning: !state.barcodeScanning })); 171 172 toggleFaceDetection = () => this.setState((state) => ({ faceDetecting: !state.faceDetecting })); 173 174 takePicture = () => { 175 if (this.camera) { 176 this.camera.takePictureAsync({ onPictureSaved: this.onPictureSaved }); 177 } 178 }; 179 180 // tslint:disable-next-line no-console 181 handleMountError = ({ message }: { message: string }) => console.error(message); 182 183 onPictureSaved = async (photo: Picture) => { 184 if (Platform.OS === 'web') { 185 photos.push(photo); 186 } else { 187 await FileSystem.moveAsync({ 188 from: photo.uri, 189 to: `${FileSystem.documentDirectory}photos/${Date.now()}.jpg`, 190 }); 191 } 192 this.setState({ newPhotos: true }); 193 }; 194 195 onBarCodeScanned = (code: BarCodeScanningResult) => { 196 console.log('Found: ', code); 197 this.setState((state) => ({ 198 barcodeData: code.data, 199 cornerPoints: code.cornerPoints, 200 })); 201 }; 202 203 onFacesDetected = ({ faces }: { faces: any }) => this.setState({ faces }); 204 205 collectPictureSizes = async () => { 206 if (this.camera) { 207 const { ratio } = this.state; 208 const pictureSizes = await this.camera.getAvailablePictureSizesAsync(ratio); 209 let pictureSizeId = 0; 210 if (Platform.OS === 'ios') { 211 pictureSizeId = pictureSizes.indexOf('High'); 212 } else { 213 // returned array is sorted in ascending order - default size is the largest one 214 pictureSizeId = pictureSizes.length - 1; 215 } 216 this.setState({ pictureSizes, pictureSizeId, pictureSize: pictureSizes[pictureSizeId] }); 217 } 218 }; 219 220 previousPictureSize = () => this.changePictureSize(1); 221 nextPictureSize = () => this.changePictureSize(-1); 222 223 changePictureSize = (direction: number) => { 224 this.setState((state) => { 225 let newId = state.pictureSizeId + direction; 226 const length = state.pictureSizes.length; 227 if (newId >= length) { 228 newId = 0; 229 } else if (newId < 0) { 230 newId = length - 1; 231 } 232 return { 233 pictureSize: state.pictureSizes[newId], 234 pictureSizeId: newId, 235 }; 236 }); 237 }; 238 239 renderGallery() { 240 const localPhotos = photos.map((photo) => photo.uri); 241 return <GalleryScreen onPress={this.toggleView} photos={localPhotos} />; 242 } 243 244 renderFaces = () => ( 245 <View style={styles.facesContainer} pointerEvents="none"> 246 {this.state.faces.map(face)} 247 </View> 248 ); 249 250 renderLandmarks = () => ( 251 <View style={styles.facesContainer} pointerEvents="none"> 252 {this.state.faces.map(landmarks)} 253 </View> 254 ); 255 256 renderNoPermissions = () => ( 257 <View style={styles.noPermissions}> 258 {this.state.permission && ( 259 <View> 260 <Text style={{ color: '#4630ec', fontWeight: 'bold', textAlign: 'center', fontSize: 24 }}> 261 Permission {this.state.permission.toLowerCase()}! 262 </Text> 263 <Text style={{ color: '#595959', textAlign: 'center', fontSize: 20 }}> 264 You'll need to enable the camera permission to continue. 265 </Text> 266 </View> 267 )} 268 </View> 269 ); 270 271 renderTopBar = () => ( 272 <View style={styles.topBar}> 273 <TouchableOpacity style={styles.toggleButton} onPress={this.toggleFacing}> 274 <Ionicons name="camera-reverse" size={32} color="white" /> 275 </TouchableOpacity> 276 <TouchableOpacity style={styles.toggleButton} onPress={this.toggleFlash}> 277 <Ionicons name={flashIcons[this.state.flash] as any} size={28} color="white" /> 278 </TouchableOpacity> 279 <TouchableOpacity style={styles.toggleButton} onPress={this.toggleWB}> 280 <MaterialIcons name={wbIcons[this.state.whiteBalance] as any} size={32} color="white" /> 281 </TouchableOpacity> 282 <TouchableOpacity style={styles.toggleButton} onPress={this.toggleFocus}> 283 <Text 284 style={[ 285 styles.autoFocusLabel, 286 { color: this.state.autoFocus === 'on' ? 'white' : '#6b6b6b' }, 287 ]}> 288 AF 289 </Text> 290 </TouchableOpacity> 291 </View> 292 ); 293 294 renderBottomBar = () => ( 295 <View style={styles.bottomBar}> 296 <TouchableOpacity style={styles.bottomButton} onPress={this.toggleMoreOptions}> 297 <Octicons name="kebab-horizontal" size={30} color="white" /> 298 </TouchableOpacity> 299 <View style={{ flex: 0.4 }}> 300 <TouchableOpacity onPress={this.takePicture} style={{ alignSelf: 'center' }}> 301 <Ionicons name="ios-radio-button-on" size={70} color="white" /> 302 </TouchableOpacity> 303 </View> 304 <TouchableOpacity style={styles.bottomButton} onPress={this.toggleView}> 305 <View> 306 <Foundation name="thumbnails" size={30} color="white" /> 307 {this.state.newPhotos && <View style={styles.newPhotosDot} />} 308 </View> 309 </TouchableOpacity> 310 </View> 311 ); 312 313 renderMoreOptions = () => ( 314 <View style={styles.options}> 315 <View style={styles.detectors}> 316 <TouchableOpacity onPress={this.toggleFaceDetection}> 317 <MaterialIcons 318 name="tag-faces" 319 size={32} 320 color={this.state.faceDetecting ? 'white' : '#858585'} 321 /> 322 </TouchableOpacity> 323 <TouchableOpacity onPress={this.toggleBarcodeScanning}> 324 <MaterialCommunityIcons 325 name="barcode-scan" 326 size={32} 327 color={this.state.barcodeScanning ? 'white' : '#858585'} 328 /> 329 </TouchableOpacity> 330 </View> 331 332 <View style={styles.pictureSizeContainer}> 333 <Text style={styles.pictureQualityLabel}>Picture quality</Text> 334 <View style={styles.pictureSizeChooser}> 335 <TouchableOpacity onPress={this.previousPictureSize} style={{ padding: 6 }}> 336 <Ionicons name="arrow-back" size={14} color="white" /> 337 </TouchableOpacity> 338 <View style={styles.pictureSizeLabel}> 339 <Text style={{ color: 'white' }}>{this.state.pictureSize}</Text> 340 </View> 341 <TouchableOpacity onPress={this.nextPictureSize} style={{ padding: 6 }}> 342 <Ionicons name="arrow-forward" size={14} color="white" /> 343 </TouchableOpacity> 344 </View> 345 </View> 346 </View> 347 ); 348 renderBarCode = () => { 349 const origin: BarCodePoint | undefined = this.state.cornerPoints 350 ? this.state.cornerPoints[0] 351 : undefined; 352 return ( 353 <Svg.Svg style={styles.barcode} pointerEvents="none"> 354 {origin && ( 355 <Svg.Text fill="#CF4048" stroke="#CF4048" fontSize="14" x={origin.x} y={origin.y - 8}> 356 {this.state.barcodeData} 357 </Svg.Text> 358 )} 359 360 <Svg.Polygon 361 points={this.state.cornerPoints?.map((coord) => `${coord.x},${coord.y}`).join(' ')} 362 stroke="green" 363 strokeWidth={10} 364 /> 365 </Svg.Svg> 366 ); 367 }; 368 renderCamera = () => ( 369 <View style={{ flex: 1 }}> 370 <Camera 371 ref={(ref) => (this.camera = ref!)} 372 style={styles.camera} 373 onCameraReady={this.collectPictureSizes} 374 type={this.state.type} 375 flashMode={FlashMode[this.state.flash]} 376 autoFocus={AutoFocus[this.state.autoFocus]} 377 zoom={this.state.zoom} 378 whiteBalance={WhiteBalance[this.state.whiteBalance]} 379 ratio={this.state.ratio} 380 pictureSize={this.state.pictureSize} 381 onMountError={this.handleMountError} 382 onFacesDetected={this.state.faceDetecting ? this.onFacesDetected : undefined} 383 faceDetectorSettings={{ 384 tracking: true, 385 }} 386 barCodeScannerSettings={{ 387 barCodeTypes: [ 388 BarCodeScanner.Constants.BarCodeType.qr, 389 BarCodeScanner.Constants.BarCodeType.pdf417, 390 ], 391 }} 392 onBarCodeScanned={this.state.barcodeScanning ? this.onBarCodeScanned : undefined}> 393 {this.renderTopBar()} 394 395 {this.renderBottomBar()} 396 </Camera> 397 {this.state.faceDetecting && this.renderFaces()} 398 {this.state.faceDetecting && this.renderLandmarks()} 399 {this.state.barcodeScanning && this.renderBarCode()} 400 {this.state.showMoreOptions && this.renderMoreOptions()} 401 </View> 402 ); 403 404 render() { 405 const cameraScreenContent = this.state.permissionsGranted 406 ? this.renderCamera() 407 : this.renderNoPermissions(); 408 const content = this.state.showGallery ? this.renderGallery() : cameraScreenContent; 409 return <View style={styles.container}>{content}</View>; 410 } 411} 412 413const styles = StyleSheet.create({ 414 container: { 415 flex: 1, 416 backgroundColor: '#000', 417 }, 418 camera: { 419 flex: 1, 420 justifyContent: 'space-between', 421 }, 422 topBar: { 423 flex: 0.2, 424 backgroundColor: 'transparent', 425 flexDirection: 'row', 426 justifyContent: 'space-around', 427 paddingTop: Constants.statusBarHeight / 2, 428 }, 429 bottomBar: { 430 paddingBottom: isIphoneX() ? 25 : 5, 431 backgroundColor: 'transparent', 432 justifyContent: 'space-between', 433 flexDirection: 'row', 434 }, 435 noPermissions: { 436 flex: 1, 437 alignItems: 'center', 438 justifyContent: 'center', 439 padding: 10, 440 backgroundColor: '#f8fdff', 441 }, 442 gallery: { 443 flex: 1, 444 flexDirection: 'row', 445 flexWrap: 'wrap', 446 }, 447 toggleButton: { 448 flex: 0.25, 449 height: 40, 450 marginHorizontal: 2, 451 marginBottom: 10, 452 marginTop: 20, 453 padding: 5, 454 alignItems: 'center', 455 justifyContent: 'center', 456 }, 457 autoFocusLabel: { 458 fontSize: 20, 459 fontWeight: 'bold', 460 }, 461 bottomButton: { 462 flex: 0.3, 463 height: 58, 464 justifyContent: 'center', 465 alignItems: 'center', 466 }, 467 newPhotosDot: { 468 position: 'absolute', 469 top: 0, 470 right: -5, 471 width: 8, 472 height: 8, 473 borderRadius: 4, 474 backgroundColor: '#4630EB', 475 }, 476 options: { 477 position: 'absolute', 478 bottom: 80, 479 left: 30, 480 width: 200, 481 height: 160, 482 backgroundColor: '#000000BA', 483 borderRadius: 4, 484 padding: 10, 485 }, 486 detectors: { 487 flex: 0.5, 488 justifyContent: 'space-around', 489 alignItems: 'center', 490 flexDirection: 'row', 491 }, 492 pictureQualityLabel: { 493 fontSize: 10, 494 marginVertical: 3, 495 color: 'white', 496 }, 497 pictureSizeContainer: { 498 flex: 0.5, 499 alignItems: 'center', 500 paddingTop: 10, 501 }, 502 pictureSizeChooser: { 503 alignItems: 'center', 504 justifyContent: 'space-between', 505 flexDirection: 'row', 506 }, 507 pictureSizeLabel: { 508 flex: 1, 509 alignItems: 'center', 510 justifyContent: 'center', 511 }, 512 facesContainer: { 513 position: 'absolute', 514 bottom: 0, 515 right: 0, 516 left: 0, 517 top: 0, 518 }, 519 row: { 520 flexDirection: 'row', 521 }, 522 barcode: { 523 position: 'absolute', 524 borderWidth: 2, 525 borderColor: 'red', 526 }, 527}); 528