Skip to content

Commit 47735a9

Browse files
authored
fix: android barcode scanning (#258 by @frankcalise)
* fix(barcode-scanning): native directories to reflect namespace * fix(example-app): android work around for camera from image picker see expo/expo#39480 for more details * fix(example-app): utilize camera modal on face detection screen * fix(example-app): adjust some styles on camera modal
1 parent 62c1933 commit 47735a9

13 files changed

Lines changed: 464 additions & 37 deletions

File tree

apps/ExampleApp/app.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
{
7575
"photosPermission": "This app uses the photo library to select images for Machine Learning purposes. i.e. Object and Image detection."
7676
}
77-
]
77+
],
78+
"expo-asset"
7879
],
7980
"experiments": {
8081
"tsconfigPaths": true
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import React, { useRef, useState } from "react"
2+
import {
3+
Modal,
4+
View,
5+
TouchableOpacity,
6+
ActivityIndicator,
7+
ViewStyle,
8+
TextStyle,
9+
} from "react-native"
10+
import { useSafeAreaInsets } from "react-native-safe-area-context"
11+
import { CameraView, useCameraPermissions, CameraType } from "expo-camera"
12+
import { Text } from "./Text"
13+
import { Button } from "./Button"
14+
import { colors, spacing } from "../theme"
15+
16+
interface CameraModalProps {
17+
visible: boolean
18+
onCapture: (uri: string) => void
19+
onClose: () => void
20+
}
21+
22+
export function CameraModal({ visible, onCapture, onClose }: CameraModalProps) {
23+
const cameraRef = useRef<CameraView>(null)
24+
const [permission, requestPermission] = useCameraPermissions()
25+
const [isCapturing, setIsCapturing] = useState(false)
26+
const [facing, setFacing] = useState<CameraType>("back")
27+
const insets = useSafeAreaInsets()
28+
29+
const toggleFacing = () => {
30+
setFacing((current) => (current === "back" ? "front" : "back"))
31+
}
32+
33+
const handleCapture = async () => {
34+
if (!cameraRef.current || isCapturing) return
35+
36+
setIsCapturing(true)
37+
try {
38+
const photo = await cameraRef.current.takePictureAsync({
39+
quality: 0.5,
40+
})
41+
if (photo?.uri) {
42+
onCapture(photo.uri)
43+
}
44+
} catch (error) {
45+
console.error("Error capturing photo:", error)
46+
} finally {
47+
setIsCapturing(false)
48+
}
49+
}
50+
51+
if (!permission) {
52+
return null
53+
}
54+
55+
if (!permission.granted) {
56+
return (
57+
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
58+
<View style={[$container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
59+
<View style={$permissionContainer}>
60+
<Text style={$permissionText}>Camera permission is required to take photos.</Text>
61+
<Button
62+
text="Grant Permission"
63+
preset="reversed"
64+
onPress={requestPermission}
65+
style={$permissionButton}
66+
/>
67+
<Button
68+
text="Cancel"
69+
preset="default"
70+
onPress={onClose}
71+
style={$cancelButton}
72+
textStyle={$cancelButtonText}
73+
/>
74+
</View>
75+
</View>
76+
</Modal>
77+
)
78+
}
79+
80+
return (
81+
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
82+
<View style={$container}>
83+
<CameraView ref={cameraRef} style={$camera} facing={facing}>
84+
<View style={[$overlay, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
85+
<View style={$topBar}>
86+
<TouchableOpacity style={$closeButton} onPress={onClose}>
87+
<Text style={$closeButtonText}></Text>
88+
</TouchableOpacity>
89+
<TouchableOpacity style={$flipButton} onPress={toggleFacing}>
90+
<Text style={$flipButtonText}></Text>
91+
</TouchableOpacity>
92+
</View>
93+
<View style={$bottomBar}>
94+
<TouchableOpacity
95+
style={[$captureButton, isCapturing && $captureButtonDisabled]}
96+
onPress={handleCapture}
97+
disabled={isCapturing}
98+
>
99+
{isCapturing ? (
100+
<ActivityIndicator color={colors.palette.neutral900} />
101+
) : (
102+
<View style={$captureButtonInner} />
103+
)}
104+
</TouchableOpacity>
105+
</View>
106+
</View>
107+
</CameraView>
108+
</View>
109+
</Modal>
110+
)
111+
}
112+
113+
const $container: ViewStyle = {
114+
flex: 1,
115+
backgroundColor: colors.palette.neutral900,
116+
}
117+
118+
const $camera: ViewStyle = {
119+
flex: 1,
120+
}
121+
122+
const $overlay: ViewStyle = {
123+
flex: 1,
124+
justifyContent: "space-between",
125+
}
126+
127+
const $topBar: ViewStyle = {
128+
flexDirection: "row",
129+
justifyContent: "space-between",
130+
padding: spacing.md,
131+
}
132+
133+
const $flipButton: ViewStyle = {
134+
width: 44,
135+
height: 44,
136+
borderRadius: 22,
137+
backgroundColor: colors.palette.overlay50,
138+
justifyContent: "center",
139+
alignItems: "center",
140+
paddingBottom: 8,
141+
paddingRight: 2,
142+
}
143+
144+
const $flipButtonText: TextStyle = {
145+
color: colors.palette.neutral100,
146+
fontSize: 28,
147+
textAlign: "center",
148+
// marginTop: -8,
149+
alignItems: "center",
150+
alignSelf: "center",
151+
}
152+
153+
const $closeButton: ViewStyle = {
154+
width: 44,
155+
height: 44,
156+
borderRadius: 22,
157+
backgroundColor: colors.palette.overlay50,
158+
justifyContent: "center",
159+
alignItems: "center",
160+
}
161+
162+
const $closeButtonText: TextStyle = {
163+
color: colors.palette.neutral100,
164+
fontSize: 20,
165+
fontWeight: "bold",
166+
}
167+
168+
const $bottomBar: ViewStyle = {
169+
flexDirection: "row",
170+
justifyContent: "center",
171+
paddingBottom: 40,
172+
}
173+
174+
const $captureButton: ViewStyle = {
175+
width: 80,
176+
height: 80,
177+
borderRadius: 40,
178+
backgroundColor: colors.palette.neutral100,
179+
justifyContent: "center",
180+
alignItems: "center",
181+
borderWidth: 4,
182+
borderColor: "rgba(255,255,255,0.5)",
183+
}
184+
185+
const $captureButtonDisabled: ViewStyle = {
186+
opacity: 0.7,
187+
}
188+
189+
const $captureButtonInner: ViewStyle = {
190+
width: 60,
191+
height: 60,
192+
borderRadius: 30,
193+
backgroundColor: colors.palette.neutral100,
194+
}
195+
196+
const $permissionContainer: ViewStyle = {
197+
flex: 1,
198+
justifyContent: "center",
199+
alignItems: "center",
200+
padding: spacing.lg,
201+
}
202+
203+
const $permissionText: TextStyle = {
204+
fontSize: 16,
205+
textAlign: "center",
206+
marginBottom: spacing.lg,
207+
color: colors.palette.neutral100,
208+
}
209+
210+
const $permissionButton: ViewStyle = {
211+
minWidth: 200,
212+
marginBottom: spacing.sm,
213+
}
214+
215+
const $cancelButton: ViewStyle = {
216+
minWidth: 200,
217+
backgroundColor: colors.transparent,
218+
borderColor: colors.palette.neutral100,
219+
}
220+
221+
const $cancelButtonText: TextStyle = {
222+
color: colors.palette.neutral100,
223+
}

apps/ExampleApp/app/components/ImageSelector.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Text } from "app/components/Text"
66
import { useExampleImage, UseExampleImageStatus, SelectedImage } from "../utils/useExampleImage"
77
import { RNMLKitImageView } from "./RNMLKitImageView"
88
import { Button } from "./Button"
9+
import { CameraModal } from "./CameraModal"
910
import { colors } from "../theme"
1011
import { ImageFilter, ImageGrouper } from "../utils/useExampleImage/examples"
1112
import { BoundingBox } from "@infinitered/react-native-mlkit-core"
@@ -36,7 +37,7 @@ export const ImageSelector = observer(function ImageSelector({
3637
}: ImageSelectorProps) {
3738
const [_status, _setStatus] = React.useState<UseExampleImageStatus>("init")
3839

39-
const { image, takePhoto, selectPhoto, clearPhoto } = useExampleImage({
40+
const { image, takePhoto, selectPhoto, clearPhoto, showCameraModal, onCameraCapture, onCameraClose } = useExampleImage({
4041
filter: images?.filter ?? "all",
4142
groupBy: images?.groupBy ?? "none",
4243
})
@@ -94,6 +95,11 @@ export const ImageSelector = observer(function ImageSelector({
9495
/>
9596
)}
9697
</View>
98+
<CameraModal
99+
visible={showCameraModal}
100+
onCapture={onCameraCapture}
101+
onClose={onCameraClose}
102+
/>
97103
</View>
98104
)
99105
})

apps/ExampleApp/app/screens/FaceDetectionScreen/FaceDetectionScreen.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Screen, Text, Button, RNMLKitImageView, ScreenTitle } from "~/component
77
import { useTypedNavigation } from "~/navigators/useTypedNavigation"
88
import { colors } from "~/theme"
99
import { useExampleImage } from "~/utils/useExampleImage"
10+
import { CameraModal } from "~/components/CameraModal"
1011
import { BOX_COLORS } from "~/screens"
1112
import { BoundingBox } from "@infinitered/react-native-mlkit-core"
1213
import {
@@ -33,7 +34,7 @@ const FaceDetectionScreenComponent: FC<FaceDetectionScreenProps> = observer(
3334
function FaceDetectionScreen() {
3435
const navigation = useTypedNavigation<"FaceDetection">()
3536

36-
const { image, clearPhoto, takePhoto, selectPhoto } = useExampleImage()
37+
const { image, clearPhoto, takePhoto, selectPhoto, showCameraModal, onCameraCapture, onCameraClose } = useExampleImage()
3738

3839
const {
3940
faces,
@@ -99,6 +100,11 @@ const FaceDetectionScreenComponent: FC<FaceDetectionScreenProps> = observer(
99100
) : (
100101
<Button text={"Select Photo"} onPress={selectPhoto} style={$button} />
101102
)}
103+
<CameraModal
104+
visible={showCameraModal}
105+
onCapture={onCameraCapture}
106+
onClose={onCameraClose}
107+
/>
102108
</Screen>
103109
)
104110
},

0 commit comments

Comments
 (0)