import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { triggerHapticFeedback } from '@/utils/haptics'; import { Ionicons } from '@expo/vector-icons'; import Voice from '@react-native-voice/voice'; import { BlurView } from 'expo-blur'; import { router, useLocalSearchParams } from 'expo-router'; import React, { useEffect, useRef, useState } from 'react'; import { Alert, Animated, Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; const { width } = Dimensions.get('window'); type VoiceRecordState = 'idle' | 'listening' | 'processing' | 'result'; export default function VoiceRecordScreen() { const theme = useColorScheme() ?? 'light'; const colorTokens = Colors[theme]; const { mealType = 'dinner' } = useLocalSearchParams<{ mealType?: string }>(); // 状态管理 const [recordState, setRecordState] = useState('idle'); const [recognizedText, setRecognizedText] = useState(''); const [isListening, setIsListening] = useState(false); // 动画相关 const scaleAnimation = useRef(new Animated.Value(1)).current; const pulseAnimation = useRef(new Animated.Value(1)).current; const waveAnimation = useRef(new Animated.Value(0)).current; useEffect(() => { // 初始化语音识别 Voice.onSpeechStart = onSpeechStart; Voice.onSpeechRecognized = onSpeechRecognized; Voice.onSpeechEnd = onSpeechEnd; Voice.onSpeechError = onSpeechError; Voice.onSpeechResults = onSpeechResults; Voice.onSpeechPartialResults = onSpeechPartialResults; Voice.onSpeechVolumeChanged = onSpeechVolumeChanged; return () => { Voice.destroy().then(Voice.removeAllListeners); }; }, []); // 启动脉动动画 const startPulseAnimation = () => { Animated.loop( Animated.sequence([ Animated.timing(pulseAnimation, { toValue: 1.2, duration: 800, useNativeDriver: true, }), Animated.timing(pulseAnimation, { toValue: 1, duration: 800, useNativeDriver: true, }), ]) ).start(); }; // 启动波浪动画 const startWaveAnimation = () => { Animated.loop( Animated.timing(waveAnimation, { toValue: 1, duration: 1500, useNativeDriver: false, }) ).start(); }; // 停止所有动画 const stopAnimations = () => { pulseAnimation.stopAnimation(); waveAnimation.stopAnimation(); scaleAnimation.setValue(1); pulseAnimation.setValue(1); waveAnimation.setValue(0); }; // 语音识别回调 const onSpeechStart = () => { setIsListening(true); setRecordState('listening'); startPulseAnimation(); startWaveAnimation(); }; const onSpeechRecognized = () => { console.log('语音识别中...'); }; const onSpeechEnd = () => { setIsListening(false); setRecordState('processing'); stopAnimations(); }; const onSpeechError = (error: any) => { console.log('语音识别错误:', error); setIsListening(false); setRecordState('idle'); stopAnimations(); Alert.alert('录音失败', '请检查麦克风权限或稍后重试'); }; const onSpeechResults = (event: any) => { const text = event.value?.[0] || ''; setRecognizedText(text); setRecordState('result'); stopAnimations(); }; const onSpeechPartialResults = (event: any) => { const text = event.value?.[0] || ''; setRecognizedText(text); }; const onSpeechVolumeChanged = (event: any) => { // 根据音量调整动画 const volume = event.value || 0; const scale = 1 + (volume * 0.1); scaleAnimation.setValue(Math.min(scale, 1.5)); }; // 开始录音 const startRecording = async () => { try { setRecognizedText(''); setRecordState('idle'); triggerHapticFeedback('impactMedium'); await Voice.start('zh-CN'); // 设置为中文识别 } catch (error) { console.log('启动语音识别失败:', error); Alert.alert('录音失败', '无法启动语音识别,请检查权限设置'); } }; // 停止录音 const stopRecording = async () => { try { await Voice.stop(); triggerHapticFeedback('impactLight'); } catch (error) { console.log('停止语音识别失败:', error); } }; // 重新录音 const retryRecording = () => { setRecognizedText(''); setRecordState('idle'); startRecording(); }; // 确认并返回结果 const confirmResult = () => { triggerHapticFeedback('impactMedium'); // TODO: 处理识别结果,可以传递给食物分析页面 router.back(); }; const handleBack = () => { if (isListening) { stopRecording(); } router.back(); }; // 获取状态对应的UI文本 const getStatusText = () => { switch (recordState) { case 'idle': return '点击开始录音'; case 'listening': return '正在聆听...'; case 'processing': return 'AI处理中...'; case 'result': return '识别完成'; default: return ''; } }; // 获取主按钮配置 const getMainButtonConfig = () => { switch (recordState) { case 'idle': return { onPress: startRecording, color: '#7B68EE', icon: 'mic', size: 80, }; case 'listening': return { onPress: stopRecording, color: '#FF6B6B', icon: 'stop', size: 80, }; case 'processing': return { onPress: () => { }, color: '#FFA07A', icon: 'hourglass', size: 80, }; case 'result': return { onPress: confirmResult, color: '#4ECDC4', icon: 'checkmark', size: 80, }; } }; const buttonConfig = getMainButtonConfig(); return ( {/* 录音动画区域 */} {/* 背景波浪效果 */} {recordState === 'listening' && ( <> {[1, 2, 3].map((index) => ( ))} )} {/* 主录音按钮 */} {/* 状态文本 */} {getStatusText()} {recordState === 'listening' && ( 说出您想记录的食物内容 )} {/* 识别结果 */} {recognizedText && ( 识别结果: {recognizedText} {recordState === 'result' && ( 重新录音 确认使用 )} )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, content: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 20, }, animationContainer: { alignItems: 'center', justifyContent: 'center', marginBottom: 40, height: 200, width: 200, }, waveRing: { position: 'absolute', width: 160, height: 160, borderRadius: 80, backgroundColor: 'rgba(123, 104, 238, 0.1)', borderWidth: 1, borderColor: 'rgba(123, 104, 238, 0.2)', }, recordButton: { width: 160, height: 160, borderRadius: 80, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 8, }, shadowOpacity: 0.3, shadowRadius: 12, elevation: 12, }, recordButtonInner: { width: '100%', height: '100%', alignItems: 'center', justifyContent: 'center', borderRadius: 80, }, statusContainer: { alignItems: 'center', marginBottom: 30, }, statusText: { fontSize: 18, fontWeight: '600', marginBottom: 8, }, hintText: { fontSize: 14, textAlign: 'center', lineHeight: 20, }, resultContainer: { position: 'absolute', bottom: 100, left: 20, right: 20, borderRadius: 16, overflow: 'hidden', }, resultContent: { padding: 20, backgroundColor: 'rgba(255, 255, 255, 0.9)', borderRadius: 16, }, resultLabel: { fontSize: 12, fontWeight: '500', marginBottom: 8, }, resultText: { fontSize: 16, fontWeight: '600', lineHeight: 24, marginBottom: 16, }, resultActions: { flexDirection: 'row', gap: 12, }, actionButton: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, gap: 6, }, retryButton: { backgroundColor: 'rgba(123, 104, 238, 0.1)', borderWidth: 1, borderColor: '#7B68EE', }, confirmButton: { backgroundColor: '#7B68EE', }, retryButtonText: { fontSize: 14, fontWeight: '500', color: '#7B68EE', }, confirmButtonText: { fontSize: 14, fontWeight: '500', color: 'white', }, });