Criando um Diário Multimídia com React Native: Câmera e Áudio
Olá, futuros desenvolvedores de sistemas! 🚀
Hoje vamos elevar nosso nível e construir uma aplicação mais completa em React Native usando o Expo. Nosso projeto será um “Diário Multimídia”, um aplicativo que não só tira fotos, mas também grava e reproduz notas de voz.
Este código é um exemplo fantástico para entendermos como gerenciar múltiplas permissões (câmera e microfone), controlar hardware, gerenciar o estado da aplicação de forma reativa e lidar com o ciclo de vida dos componentes para evitar problemas de memória.
Vamos mergulhar nesse código e desvendar cada um de seus segredos!
Visão Geral da Aplicação
O aplicativo funciona da seguinte forma:
- Solicita permissões para Câmera e Microfone assim que é iniciado.
- Exibe uma interface principal com duas seções: uma para fotografia e outra para notas de voz.
- Na seção de câmera: exibe a visão da câmera, permite tirar uma foto e, após a captura, mostra uma prévia da imagem com a opção de tirar outra.
- Na seção de áudio: permite iniciar uma gravação, parar a gravação, e depois reproduzir o áudio gravado. A interface dos botões muda dinamicamente para refletir o estado atual (gravando, parado, etc.).
Os Pilares do Código: Hooks do React
Antes de olharmos as funções específicas, vamos entender as ferramentas principais do React que dão vida a este app: os Hooks.
- 🧠
useState
: Pense nele como a “memória” do nosso componente. UsamosuseState
para criar variáveis de estado. Toda vez que o valor de uma dessas variáveis muda (usando sua funçãoset...
), o React automaticamente redesenha a parte da tela que depende dela. No nosso código, ele controla tudo: se temos permissão, qual a URI da foto tirada, se estamos gravando áudio, etc. - useEffect
useEffect
: É o nosso “gerente de efeitos colaterais”. Ele permite executar código em momentos específicos, como quando o app abre pela primeira vez. Usamos ele para:- Solicitar as permissões de câmera e áudio logo no início.
- Limpar recursos da memória. No código, há um
useEffect
genial que “descarrega” o som da memória assim que ele não é mais necessário, prevenindo memory leaks.
- useRef
useRef
: É como um “controle remoto” para um elemento específico da tela. EnquantouseState
causa uma nova renderização ao ser alterado,useRef
nos dá uma referência direta a um elemento (como o componente<Camera>
) sem redesenhar a tela. Usamos ocameraRef
para chamar funções do componente da câmera diretamente, comocameraRef.current.takePictureAsync()
.
O Coração da Aplicação: Funções e Lógica
Agora, vamos analisar o fluxo de trabalho principal do app.
1. Gerenciamento de Permissões e Ciclo de Vida
JavaScript
useEffect(() => {
(async () => {
// Solicita permissão para câmera e áudio
const statusCamera = await Camera.requestCameraPermissionsAsync();
setTemPermissaoCamera(statusCamera.status === 'granted');
const statusAudio = await Audio.requestPermissionsAsync();
setTemPermissaoAudio(statusAudio.status === 'granted');
})();
}, []); // O array vazio [] garante que isso rode apenas uma vez.
Este bloco é a porta de entrada. Ele usa uma função assíncrona (async
) auto-executável para pedir as duas permissões em sequência e atualizar os respectivos estados.
JavaScript
// Efeito para limpar o objeto de som da memória
useEffect(() => {
return som ? () => { som.unloadAsync(); } : undefined;
}, [som]);
Este é um conceito profissional e muito importante! Este useEffect
“escuta” por mudanças na variável som
. Quando o componente está prestes a ser desmontado ou o som
muda, ele executa a função de retorno (cleanup), chamando som.unloadAsync()
para liberar a memória.
2. Capturando a Imagem 📸
JavaScript
async function tirarFoto() {
if (cameraRef.current && cameraPronta) {
const foto = await cameraRef.current.takePictureAsync();
setUriDaImagem(foto.uri);
}
}
A função tirarFoto
é simples e direta:
- Verifica se a referência da câmera existe (
cameraRef.current
) e se a câmera está pronta (cameraPronta
). Isso evita erros. - Chama o método
takePictureAsync()
através da referência. - Salva a URI (o caminho local) da foto no estado
uriDaImagem
, o que faz a tela trocar a visão da câmera pela prévia da imagem.
3. Dominando o Áudio 🎤
O controle de áudio é um ciclo de três partes:
iniciarGravacao
:- Configura o modo de áudio com
Audio.setAudioModeAsync()
, essencial para garantir a compatibilidade, especialmente no iOS. - Cria uma nova instância de gravação com
Audio.Recording.createAsync()
. - Salva o objeto da gravação no estado
gravacao
.
- Configura o modo de áudio com
pararGravacao
:- Chama
gravacao.stopAndUnloadAsync()
para parar a gravação e liberar o gravador da memória. - Pega a URI do arquivo de áudio com
gravacao.getURI()
. - Salva essa URI no estado
uriDaGravacao
e limpa o estadogravacao
.
- Chama
reproduzirSom
:- Usa
Audio.Sound.createAsync()
para carregar o arquivo de áudio a partir dauriDaGravacao
. - Salva o objeto de som no estado
som
. - Chama
sound.playAsync()
para tocar o áudio.
- Usa
Construindo a Interface (UI): O Poder da Renderização Condicional
A mágica do React brilha na seção de renderização (o return
). A tela não é estática; ela reage às mudanças de estado.
Verificação de Permissões
JavaScript
if (temPermissaoCamera === null || temPermissaoAudio === null) {
return <View style={styles.centralizado}><Text>Solicitando permissões...</Text></View>;
}
if (temPermissaoCamera === false) {
// Mostra erro da câmera
}
Primeiro, o código lida com os casos de carregamento e permissão negada. Isso garante que o usuário sempre veja uma tela apropriada para o estado atual do aplicativo.
Interface da Câmera vs. Prévia da Imagem
JavaScript
{uriDaImagem ? (
<View>
<Image source={{ uri: uriDaImagem }} />
<Button title="Tirar Outra Foto" onPress={resetarFoto} />
</View>
) : (
<View>
<Camera ref={cameraRef} />
<Button title="Tirar Foto" onPress={tirarFoto} />
</View>
)}
Aqui, um operador ternário (condição ? valor_se_verdadeiro : valor_se_falso
) decide o que mostrar. Se uriDaImagem
tiver um valor (ou seja, uma foto foi tirada), ele mostra o componente <Image>
. Caso contrário, mostra o componente <Camera>
.
Botões de Áudio Dinâmicos
JavaScript
<Button
title={gravacao ? 'Parando Gravação...' : 'Gravar Áudio'}
onPress={gravacao ? pararGravacao : iniciarGravacao}
color={gravacao ? '#FF4500' : '#32CD32'}
/>
Este botão de gravação é um exemplo brilhante de UI reativa. Seu title
, sua função onPress
e sua color
mudam dependendo se existe um objeto de gravacao
no estado. Da mesma forma, o botão “Reproduzir” é desabilitado (disabled={...}
) de forma inteligente para evitar que o usuário tente tocar um som que não existe ou enquanto uma gravação está em andamento.
Código Completo
import React, { useState, useEffect, useRef } from 'react';
import { StyleSheet, Text, View, Button, Image, Platform, Alert } from 'react-native';
import { Camera } from 'expo-camera';
import { Audio } from 'expo-av';
export default function App() {
// =================================================================
// --- ESTADOS (STATES) ---
// Usamos o 'useState' para criar variáveis que, quando alteradas,
// fazem o React redesenhar a tela automaticamente.
// =================================================================
// Estados para permissões
const [temPermissaoCamera, setTemPermissaoCamera] = useState(null);
const [temPermissaoAudio, setTemPermissaoAudio] = useState(null);
// Estados para a câmera
const cameraRef = useRef(null); // 'useRef' cria uma referência para acessar o componente da Câmera diretamente.
const [uriDaImagem, setUriDaImagem] = useState(null); // Armazena o caminho (URI) da foto tirada.
const [cameraPronta, setCameraPronta] = useState(false); // Verifica se a câmera está pronta para uso.
// Estados para o áudio
const [gravacao, setGravacao] = useState(null); // Armazena o objeto da gravação em andamento.
const [som, setSom] = useState(null); // Armazena o objeto do som para reprodução.
const [uriDaGravacao, setUriDaGravacao] = useState(null); // Armazena o caminho (URI) do áudio gravado.
// =================================================================
// --- EFEITOS (EFFECTS) ---
// O 'useEffect' executa código em momentos específicos do ciclo de
// vida do componente, como na primeira vez que ele aparece na tela.
// =================================================================
// Efeito para solicitar permissões ao iniciar o app.
// O array vazio `[]` no final significa que este código só roda uma vez.
useEffect(() => {
(async () => {
// Solicita permissão para usar a câmera.
const statusCamera = await Camera.requestCameraPermissionsAsync();
setTemPermissaoCamera(statusCamera.status === 'granted');
// Solicita permissão para usar o microfone.
const statusAudio = await Audio.requestPermissionsAsync();
setTemPermissaoAudio(statusAudio.status === 'granted');
})();
}, []);
// Efeito para limpar o objeto de som da memória quando ele não for mais usado.
// Isso evita vazamentos de memória.
useEffect(() => {
return som
? () => {
console.log('Descarregando o som da memória...');
som.unloadAsync();
}
: undefined;
}, [som]);
// =================================================================
// --- FUNÇÕES DE ÁUDIO ---
// =================================================================
async function iniciarGravacao() {
try {
if (!temPermissaoAudio) {
Alert.alert("Erro", "Permissão de áudio não concedida.");
return;
}
// Define o modo de áudio, necessário para gravação no iOS.
await Audio.setAudioModeAsync({
allowsRecordingIOS: true,
playsInSilentModeIOS: true, // Permite tocar som mesmo no modo silencioso
});
console.log('Iniciando gravação de áudio...');
const { recording } = await Audio.Recording.createAsync(
Audio.RecordingOptionsPresets.HIGH_QUALITY // Define a qualidade da gravação
);
setGravacao(recording); // Armazena o objeto da gravação no estado.
console.log('Gravação iniciada.');
} catch (err) {
console.error('Falha ao iniciar a gravação', err);
}
}
async function pararGravacao() {
if (!gravacao) return; // Se não houver gravação, não faz nada.
console.log('Parando gravação...');
setGravacao(undefined); // Limpa o estado da gravação.
await gravacao.stopAndUnloadAsync(); // Para e libera o gravador da memória.
const uri = gravacao.getURI(); // Pega o caminho do arquivo gravado.
setUriDaGravacao(uri); // Salva o caminho no estado para poder reproduzir depois.
console.log('Gravação parada e salva em:', uri);
}
async function reproduzirSom() {
if (!uriDaGravacao) {
Alert.alert('Aviso', 'Nenhum áudio foi gravado ainda.');
return;
}
console.log('Carregando e reproduzindo o som...');
const { sound } = await Audio.Sound.createAsync({ uri: uriDaGravacao });
setSom(sound); // Salva o objeto de som no estado.
await sound.playAsync(); // Toca o som.
}
// =================================================================
// --- FUNÇÕES DA CÂMERA ---
// =================================================================
async function tirarFoto() {
// Verifica se a referência da câmera existe e se ela está pronta.
if (cameraRef.current && cameraPronta) {
try {
const foto = await cameraRef.current.takePictureAsync();
setUriDaImagem(foto.uri); // Salva o caminho da foto no estado.
console.log('Foto tirada com sucesso:', foto.uri);
} catch (error) {
console.error('Erro ao tirar a foto:', error);
Alert.alert('Erro', 'Não foi possível tirar a foto.');
}
} else {
Alert.alert('Aviso', 'A câmera não está pronta.');
}
}
// Função para limpar a imagem e mostrar a câmera novamente.
function resetarFoto() {
setUriDaImagem(null);
}
// =================================================================
// --- RENDERIZAÇÃO (O QUE APARECE NA TELA) ---
// =================================================================
// Se as permissões ainda não foram verificadas, mostra uma tela de carregamento.
if (temPermissaoCamera === null || temPermissaoAudio === null) {
return <View style={styles.centralizado}><Text>Solicitando permissões...</Text></View>;
}
// Se a permissão da câmera foi negada, mostra uma mensagem de erro.
if (temPermissaoCamera === false) {
return <View style={styles.centralizado}><Text>Acesso à câmera negado. Por favor, habilite nas configurações do seu celular.</Text></View>;
}
// Se a permissão do áudio foi negada, mostra uma mensagem de erro.
if (temPermissaoAudio === false) {
return <View style={styles.centralizado}><Text>Acesso ao microfone negado. Por favor, habilite nas configurações do seu celular.</Text></View>;
}
// Se todas as permissões foram concedidas, mostra a interface principal.
return (
<View style={styles.container}>
<Text style={styles.titulo}>📝 Meu Diário Multimídia 📸</Text>
{/* Seção da Câmera */}
<View style={styles.secao}>
<Text style={styles.tituloSecao}>Fotografia</Text>
{/* Renderização Condicional: Se 'uriDaImagem' existir, mostra a imagem. Senão, mostra a câmera. */}
{uriDaImagem ? (
<View style={styles.containerMidia}>
<Image source={{ uri: uriDaImagem }} style={styles.previaImagem} />
<Button title="Tirar Outra Foto" onPress={resetarFoto} color="#1E90FF"/>
</View>
) : (
<View style={styles.containerMidia}>
<Camera
ref={cameraRef}
style={styles.camera}
type={Camera.Constants.Type.back} // Usa a câmera traseira.
onCameraReady={() => setCameraPronta(true)} // Avisa quando a câmera estiver pronta.
/>
<View style={styles.espacoBotao} />
<Button title="Tirar Foto" onPress={tirarFoto} disabled={!cameraPronta} />
</View>
)}
</View>
{/* Seção do Áudio */}
<View style={styles.secao}>
<Text style={styles.tituloSecao}>Nota de Voz</Text>
<Button
title={gravacao ? 'Parando Gravação...' : 'Gravar Áudio'}
onPress={gravacao ? pararGravacao : iniciarGravacao}
color={gravacao ? '#FF4500' : '#32CD32'} // Muda a cor do botão se estiver gravando.
/>
<View style={styles.espacoBotao} />
<Button
title="Reproduzir Gravação"
onPress={reproduzirSom}
disabled={!uriDaGravacao || !!gravacao} // Desabilita o botão se não houver gravação ou se estiver gravando.
/>
{/* Mostra um texto de status quando o áudio está pronto. */}
{uriDaGravacao && !gravacao && <Text style={styles.textoStatus}>Áudio pronto para tocar!</Text>}
</View>
</View>
);
}
// =================================================================
// --- ESTILOS (STYLES) ---
// Define a aparência dos componentes.
// =================================================================
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f0f4f7',
paddingTop: 50,
paddingHorizontal: 20,
},
centralizado: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
titulo: {
fontSize: 26,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
color: '#333',
},
secao: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 20,
marginBottom: 20,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
tituloSecao: {
fontSize: 20,
fontWeight: '600',
textAlign: 'center',
marginBottom: 15,
color: '#444',
},
containerMidia: {
alignItems: 'center',
},
camera: {
width: '100%',
aspectRatio: 1,
borderRadius: 8,
overflow: 'hidden',
},
previaImagem: {
width: '100%',
aspectRatio: 1,
borderRadius: 8,
marginBottom: 15,
},
espacoBotao: {
height: 10,
},
textoStatus: {
marginTop: 10,
color: 'green',
textAlign: 'center',
fontWeight: 'bold',
},
});
Conclusão
Este “Diário Multimídia” é uma aula prática sobre como construir aplicações robustas em React Native. Os principais aprendizados são:
- Gerenciamento de Estado: Como usar
useState
para controlar a interface de forma declarativa. - Controle de Hardware: Como usar as APIs do Expo (
expo-camera
,expo-av
) para interagir com os recursos nativos do celular. - Programação Assíncrona: O uso de
async/await
é fundamental para lidar com operações que levam tempo, como permissões e I/O de arquivos. - Ciclo de Vida e Memória: A importância de gerenciar o ciclo de vida dos componentes e limpar recursos com
useEffect
para criar apps eficientes e sem falhas.
Agora, o desafio para vocês é expandir este projeto! Que tal adicionar a opção de salvar as fotos e áudios na galeria do celular? Ou permitir a troca entre a câmera frontal e traseira? As possibilidades são infinitas.
Bons estudos e mãos ao código!
Share this content:
Publicar comentário