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