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