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