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