expo-camera
Escaneamento de código de barras, lanterna e limites de desempenho de visualização - o cookbook do expo-camera para telas de escaneamento, auditoria e captura de campo do Expo SDK 57.
Busque em todas as páginas da documentação
Escaneamento de código de barras, lanterna e limites de desempenho de visualização - o cookbook do expo-camera para telas de escaneamento, auditoria e captura de campo do Expo SDK 57.
Cartão de receita de referência rápida - pronto para copiar e colar.
npx expo install expo-camera// app.config.ts
export default {
plugins: [
[
"expo-camera",
{
cameraPermission:
"Permita que o FieldPro fotografe etiquetas de equipamentos para ordens de serviço.",
microphonePermission:
"Permita que o FieldPro grave áudio com vídeos de inspeção.",
recordAudioAndroid: true,
barcodeScannerEnabled: true,
},
],
],
};import { CameraView, useCameraPermissions } from "expo-camera";
import { useCallback, useRef, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
export function QrScanner({ onScan }: { onScan: (data: string) => void }) {
const [permission, requestPermission] = useCameraPermissions();
const [torch, setTorch] = useState(false);
const last = useRef(0);
const handleBarcode = useCallback(
({ data }: { data: string }) => {
const now = Date.now();
if (now - last.current < 1200) return;
last.current = now;
onScan(data);
},
[onScan]
);
if (!permission?.granted) {
return (
<View style={styles.gate}>
<Text>Escaneie tags de ativos para abrir a ordem de serviço correta.</Text>
<Pressable onPress={requestPermission}>
<Text style={styles.link}>Permitir câmera</Text>
</Pressable>
</View>
);
}
return (
<View style={styles.root}>
<CameraView
style={styles.camera}
facing="back"
enableTorch={torch}
barcodeScannerSettings={{ barcodeTypes: ["qr", "code128"] }}
onBarcodeScanned={handleBarcode}
/>
<Pressable style={styles.torch} onPress={() => setTorch((t) => !t)}>
<Text style={styles.torchText}>{torch ? "Lanterna ligada" : "Lanterna desligada"}</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1 },
camera: { flex: 1 },
gate: { flex: 1, padding: 24, gap: 12, justifyContent: "center" },
link: { color: "#2563eb" },
torch: { position: "absolute", top: 56, right: 16, padding: 10, backgroundColor: "rgba(0,0,0,0.5)", borderRadius: 8 },
torchText: { color: "#fff", fontWeight: "600" },
});Quando usar isso:
Quando evitar:
expo-image-picker launchCameraAsync tem menos código.expo-image-picker em vez disso.Fluxos de captura: Para fallback de biblioteca, portões de permissão e visualizações seguras em memória, consulte expo-camera & expo-image-picker.
Tela de escaneamento com tratamento de código de barras com debounce, alternância de lanterna, montagem ciente do foco e captura de imagem reduzida.
npx expo install expo-camera expo-image-manipulator// src/scan/InspectionCapture.tsx
import { CameraView, useCameraPermissions } from "expo-camera";
import * as ImageManipulator from "expo-image-manipulator";
import { useIsFocused } from "@react-navigation/native";
import { useCallback, useRef, useState } from "react";
import { Image } from "expo-image";
import { Pressable, StyleSheet, Text, View } from "react-native";
type Props = {
onScan: (payload: string) => void;
onPhoto: (uri: string) => void;
};
export function InspectionCapture({ onScan, onPhoto }: Props) {
const isFocused = useIsFocused();
const cameraRef = useRef<CameraView>(null);
const [permission, requestPermission] = useCameraPermissions();
const [ready, setReady] = useState(false);
const [torch, setTorch] = useState(false);
const [preview, setPreview] = useState<string | null>(null);
const lastScan = useRef(0);
const onBarcodeScanned = useCallback(
({ data }: { data: string }) => {
if (!isFocused) return;
const now = Date.now();
if (now - lastScan.current < 1500) return;
lastScan.current = now;
onScan(data);
},
[isFocused, onScan]
);
async function captureLabel() {
if (!ready) return;
const photo = await cameraRef.current?.takePictureAsync({ quality: 0.8 });
if (!photo?.uri) return;
const resized = await ImageManipulator.manipulateAsync(
photo.uri,
[{ resize: { width: 1600 } }],
{ compress: 0.75, format: ImageManipulator.SaveFormat.JPEG }
);
setPreview(resized.uri);
onPhoto(resized.uri);
}
if (!permission?.granted) {
return (
<View style={styles.gate}>
<Text>Fotografe placas de série e escaneie códigos QR em equipamentos.</Text>
<Pressable onPress={requestPermission}>
<Text style={styles.link}>Continuar</Text>
</Pressable>
</View>
);
}
if (preview) {
return (
<View style={styles.preview}>
<Image source={{ uri: preview }} contentFit="contain" style={styles.previewImage} />
<Pressable onPress={() => setPreview(null)}>
<Text>Retirar</Text>
</Pressable>
</View>
);
}
if (!isFocused) {
return <View style={styles.placeholder} />;
}
return (
<View style={styles.root}>
<CameraView
ref={cameraRef}
style={styles.camera}
facing="back"
enableTorch={torch}
onCameraReady={() => setReady(true)}
barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
onBarcodeScanned={onBarcodeScanned}
/>
<View style={styles.toolbar}>
<Pressable onPress={() => setTorch((t) => !t)}>
<Text style={styles.tool}>{torch ? "Lanterna ligada" : "Lanterna"}</Text>
</Pressable>
<Pressable onPress={captureLabel} disabled={!ready}>
<Text style={styles.tool}>Capturar etiqueta</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1 },
camera: { flex: 1 },
gate: { flex: 1, padding: 24, gap: 12, justifyContent: "center" },
link: { color: "#2563eb", fontWeight: "600" },
placeholder: { flex: 1, backgroundColor: "#111" },
preview: { flex: 1, padding: 16, gap: 12 },
previewImage: { flex: 1, borderRadius: 8 },
toolbar: {
position: "absolute",
bottom: 40,
left: 16,
right: 16,
flexDirection: "row",
justifyContent: "space-between",
},
tool: { color: "#fff", fontWeight: "600", padding: 12, backgroundColor: "rgba(0,0,0,0.55)", borderRadius: 8 },
});O que isso demonstra:
useIsFocused libera a câmera quando a aba ou o modal está oculto.onCameraReady controla takePictureAsync - chamar cedo produz erros ou quadros em branco.enableTorch alterna a luz para contraste do código de barras em corredores escuros.onBarcodeScanned com debounce evita navegações duplicadas.expo-image mantém a memória limitada.| Modo | API | Melhor para |
|---|---|---|
| Visualização em linha | CameraView + onBarcodeScanned | Sobreposição personalizada, lanterna, UX na mesma tela |
| Scanner do sistema | CameraView.launchScanner() | Folha de scanner nativa do iOS 16+ / Android ML Kit |
| Listener moderno | CameraView.onModernBarcodeScanned | Eventos de scanner nativos sem visualização em linha |
import { CameraView } from "expo-camera";
async function openSystemScanner() {
await CameraView.launchScanner({ barcodeTypes: ["qr"] });
}data com Zod antes de router.push - nunca confie em URLs escaneados cegamente.onBarcodeScanned={scanned ? undefined : handler}.barcodeScannerEnabled: false no plugin de configuração quando você enviar aplicativos apenas de fotos - binário menor.<CameraView enableTorch={torchOn} facing="back" />useEffect de limpeza - deixá-la ligada consome bateria e incomoda os usuários.quality moderada em vez de resolução total sem luz.Drivers de custo da visualização:
1. Resolução e proporção (a prop `ratio` no Android muda o tipo de escala)
2. Múltiplas instâncias `CameraView` montadas (proibido - apenas uma sessão ativa)
3. Abas ocultas ainda montadas (use controle de foco)
4. Escaneamento de código de barras + animações de navegação de 60fps em Android de baixo custo| Técnica | Efeito |
|---|---|
| Desmontar ao perder o foco | Libera o hardware da câmera para outros aplicativos |
pausePreview() quando a folha cobre a câmera | Reduz a GPU enquanto o modal está aberto |
pictureSize menor quando suportado | Buffers de análise menores para imagens |
Tela apenas de escaneamento sem pictureSize 4K | Bloqueio mais rápido e menor calor |
// Pause quando uma folha inferior cobrir a visualização
useEffect(() => {
if (!sheetOpen) return;
cameraRef.current?.pausePreview();
return () => {
cameraRef.current?.resumePreview();
};
}, [sheetOpen]);takePictureAsync enquanto a visualização estiver pausada no Android - isso gera um erro.const photo = await cameraRef.current?.takePictureAsync({
quality: 0.8,
skipProcessing: false,
});
const video = await cameraRef.current?.recordAsync({ maxDuration: 60 });recordAsync grava no cache - envie via expo-file-system, não base64.scanned.CameraView - visualização preta ou travamento. Correção: uma sessão por aplicativo; desmonte abas ocultas.<Image>.Platform.OS e limites de tamanho.| Alternativa | Use quando | Não use quando |
|---|---|---|
CameraView | UI de escaneamento personalizada, lanterna, vídeo | Câmera do sistema com um toque |
launchCameraAsync | UX da câmera nativa suficiente | Visualizador com marca |
launchScanner() | QR rápido sem UI personalizada | Precisa de sobreposição de lanterna na mesma tela |
| react-native-vision-camera | Processadores de quadro, contadores de FPS | Captura padrão do Expo |
npx expo install expo-cameraAdicione strings de permissão através do plugin de configuração e, em seguida, reconstrua se as permissões nativas mudaram.
useIsFocused() for falso.CameraView montado roubam a única sessão de câmera.Versões da Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026