1import Ionicons from '@expo/vector-icons/build/Ionicons'; 2import * as FaceDetector from 'expo-face-detector'; 3import React from 'react'; 4import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; 5 6const pictureSize = 150; 7 8interface State { 9 selected: boolean; 10 faces: FaceDetector.FaceFeature[]; 11 image?: FaceDetector.Image; 12} 13 14export default class Photo extends React.Component< 15 { uri: string; onSelectionToggle: (uri: string, selected: boolean) => void }, 16 State 17> { 18 readonly state: State = { 19 selected: false, 20 faces: [], 21 }; 22 _mounted = false; 23 24 componentDidMount() { 25 this._mounted = true; 26 } 27 28 componentWillUnmount() { 29 this._mounted = false; 30 } 31 32 toggleSelection = () => { 33 this.setState( 34 (state) => ({ selected: !state.selected }), 35 () => this.props.onSelectionToggle(this.props.uri, this.state.selected) 36 ); 37 }; 38 39 detectFace = () => 40 FaceDetector.detectFacesAsync(this.props.uri, { 41 detectLandmarks: FaceDetector.FaceDetectorLandmarks.none, 42 runClassifications: FaceDetector.FaceDetectorClassifications.all, 43 }) 44 .then(this.facesDetected) 45 .catch(this.handleFaceDetectionError); 46 47 facesDetected = ({ 48 faces, 49 image, 50 }: { 51 faces: FaceDetector.FaceFeature[]; 52 image: FaceDetector.Image; 53 }) => { 54 this.setState({ 55 faces, 56 image, 57 }); 58 }; 59 60 getImageDimensions = ({ width, height }: FaceDetector.Image) => { 61 if (width > height) { 62 const scaledHeight = (pictureSize * height) / width; 63 return { 64 width: pictureSize, 65 height: scaledHeight, 66 67 scaleX: pictureSize / width, 68 scaleY: scaledHeight / height, 69 70 offsetX: 0, 71 offsetY: (pictureSize - scaledHeight) / 2, 72 }; 73 } else { 74 const scaledWidth = (pictureSize * width) / height; 75 return { 76 width: scaledWidth, 77 height: pictureSize, 78 79 scaleX: scaledWidth / width, 80 scaleY: pictureSize / height, 81 82 offsetX: (pictureSize - scaledWidth) / 2, 83 offsetY: 0, 84 }; 85 } 86 }; 87 88 // tslint:disable-next-line no-console 89 handleFaceDetectionError = (error: any) => console.warn(error); 90 91 renderFaces = () => this.state.image && this.state.faces && this.state.faces.map(this.renderFace); 92 93 renderFace = (face: FaceDetector.FaceFeature, index: number) => { 94 const { scaleX, scaleY, offsetX, offsetY } = this.getImageDimensions(this.state.image!); 95 const layout = { 96 top: offsetY + face.bounds.origin.y * scaleY, 97 left: offsetX + face.bounds.origin.x * scaleX, 98 width: face.bounds.size.width * scaleX, 99 height: face.bounds.size.height * scaleY, 100 }; 101 102 return ( 103 <View 104 key={index} 105 style={[ 106 styles.face, 107 layout, 108 { 109 transform: [ 110 { perspective: 600 }, 111 { rotateZ: `${(face.rollAngle || 0).toFixed(0)}deg` }, 112 { rotateY: `${(face.yawAngle || 0).toFixed(0)}deg` }, 113 ], 114 }, 115 ]}> 116 {face.smilingProbability && ( 117 <Text style={styles.faceText}> {(face.smilingProbability * 100).toFixed(0)}%</Text> 118 )} 119 </View> 120 ); 121 }; 122 123 render() { 124 const { uri } = this.props; 125 return ( 126 <TouchableOpacity 127 style={styles.pictureWrapper} 128 onLongPress={this.detectFace} 129 onPress={this.toggleSelection} 130 activeOpacity={1}> 131 <Image style={styles.picture} source={{ uri }} /> 132 {this.state.selected && <Ionicons name="md-checkmark-circle" size={30} color="#4630EB" />} 133 <View style={styles.facesContainer}>{this.renderFaces()}</View> 134 </TouchableOpacity> 135 ); 136 } 137} 138 139const styles = StyleSheet.create({ 140 picture: { 141 position: 'absolute', 142 bottom: 0, 143 right: 0, 144 left: 0, 145 top: 0, 146 resizeMode: 'contain', 147 }, 148 pictureWrapper: { 149 width: pictureSize, 150 height: pictureSize, 151 alignItems: 'center', 152 justifyContent: 'center', 153 margin: 5, 154 }, 155 facesContainer: { 156 position: 'absolute', 157 bottom: 0, 158 right: 0, 159 left: 0, 160 top: 0, 161 }, 162 face: { 163 borderWidth: 2, 164 borderRadius: 2, 165 position: 'absolute', 166 borderColor: '#FFD700', 167 justifyContent: 'center', 168 backgroundColor: 'rgba(0, 0, 0, 0.5)', 169 }, 170 faceText: { 171 color: '#FFD700', 172 fontWeight: 'bold', 173 textAlign: 'center', 174 margin: 2, 175 fontSize: 10, 176 backgroundColor: 'transparent', 177 }, 178}); 179