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