Files
digital-pilates/app/food-camera.tsx
richarjiang 02883869fe feat: Implement Food Camera Screen and Floating Food Overlay
- Added FoodCameraScreen for capturing food images with camera functionality.
- Integrated image picker for selecting images from the gallery.
- Created FloatingFoodOverlay for quick access to food library and scanning options.
- Updated NutritionRadarCard to utilize FloatingFoodOverlay for adding food.
- Enhanced ExploreScreen layout and styles for better user experience.
- Removed unused SafeAreaView from ExploreScreen.
- Updated profile edit screen to remove unnecessary state variables.
- Updated avatar image source in profile edit screen.
- Added ExpoCamera dependency for camera functionalities.
2025-09-03 19:17:26 +08:00

436 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { Ionicons } from '@expo/vector-icons';
import { CameraType, CameraView, useCameraPermissions } from 'expo-camera';
import * as ImagePicker from 'expo-image-picker';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useRef, useState } from 'react';
import {
Alert,
Dimensions,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
export default function FoodCameraScreen() {
const router = useRouter();
const params = useLocalSearchParams<{ mealType?: string }>();
const cameraRef = useRef<CameraView>(null);
const [currentMealType, setCurrentMealType] = useState<MealType>(
(params.mealType as MealType) || 'dinner'
);
const [facing, setFacing] = useState<CameraType>('back');
const [permission, requestPermission] = useCameraPermissions();
// 餐次选择选项
const mealOptions = [
{ key: 'breakfast' as const, label: '早餐', icon: '☀️' },
{ key: 'lunch' as const, label: '午餐', icon: '🌤️' },
{ key: 'dinner' as const, label: '晚餐', icon: '🌙' },
{ key: 'snack' as const, label: '加餐', icon: '🍎' },
];
if (!permission) {
// 权限仍在加载中
return (
<SafeAreaView style={styles.container}>
<HeaderBar
title="食物拍摄"
onBack={() => router.back()}
transparent={true}
/>
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>...</Text>
</View>
</SafeAreaView>
);
}
if (!permission.granted) {
// 没有相机权限
return (
<SafeAreaView style={styles.container}>
<HeaderBar
title="食物拍摄"
onBack={() => router.back()}
transparent={true}
/>
<View style={styles.permissionContainer}>
<Ionicons name="camera-outline" size={64} color="#999" />
<Text style={styles.permissionTitle}></Text>
<Text style={styles.permissionText}>
访
</Text>
<TouchableOpacity
style={styles.permissionButton}
onPress={requestPermission}
>
<Text style={styles.permissionButtonText}>访</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
// 切换相机前后摄像头
function toggleCameraFacing() {
setFacing(current => (current === 'back' ? 'front' : 'back'));
}
// 拍摄照片
const takePicture = async () => {
if (cameraRef.current) {
try {
const photo = await cameraRef.current.takePictureAsync({
quality: 0.8,
base64: false,
});
if (photo) {
// TODO: 处理拍摄的照片,可以传递到下一个页面进行食物识别
console.log('照片拍摄成功:', photo.uri);
Alert.alert('拍摄成功', '照片已保存,后续会添加食物识别功能');
// router.push(`/food-recognition?imageUri=${photo.uri}&mealType=${currentMealType}`);
}
} catch (error) {
console.error('拍照失败:', error);
Alert.alert('拍照失败', '请重试');
}
}
};
// 从相册选择照片
const pickImageFromGallery = async () => {
try {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 0.8,
});
if (!result.canceled && result.assets[0]) {
const imageUri = result.assets[0].uri;
console.log('从相册选择的照片:', imageUri);
Alert.alert('选择成功', '照片已选择,后续会添加食物识别功能');
// router.push(`/food-recognition?imageUri=${imageUri}&mealType=${currentMealType}`);
}
} catch (error) {
console.error('选择照片失败:', error);
Alert.alert('选择失败', '请重试');
}
};
// AR功能暂时显示提示
const handleARPress = () => {
Alert.alert('AR功能', 'AR食物识别功能即将推出');
};
// 餐次选择
const handleMealTypeChange = (mealType: MealType) => {
setCurrentMealType(mealType);
};
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
{/* 相机视图 */}
<CameraView
ref={cameraRef}
style={styles.camera}
facing={facing}
ratio="16:9"
>
{/* 头部导航 */}
<HeaderBar
title=""
onBack={() => router.back()}
transparent={true}
backColor={'#fff'}
/>
{/* 取景框和提示文本 */}
<View style={styles.overlayContainer}>
<Text style={styles.hintText}></Text>
{/* 取景框 */}
<View style={styles.viewfinderContainer}>
<View style={styles.viewfinder}>
{/* 四个角的装饰 */}
<View style={[styles.corner, styles.topLeft]} />
<View style={[styles.corner, styles.topRight]} />
<View style={[styles.corner, styles.bottomLeft]} />
<View style={[styles.corner, styles.bottomRight]} />
</View>
</View>
</View>
{/* 餐次选择器 */}
<View style={styles.mealTypeContainer}>
{mealOptions.map((option) => (
<TouchableOpacity
key={option.key}
style={[
styles.mealTypeButton,
currentMealType === option.key && styles.mealTypeButtonActive
]}
onPress={() => handleMealTypeChange(option.key)}
>
<Text style={styles.mealTypeIcon}>{option.icon}</Text>
<Text style={[
styles.mealTypeText,
currentMealType === option.key && styles.mealTypeTextActive
]}>
{option.label}
</Text>
</TouchableOpacity>
))}
</View>
{/* 底部控制栏 */}
<SafeAreaView style={styles.bottomContainer}>
<View style={styles.controlsContainer}>
{/* 相册按钮 */}
{/* <TouchableOpacity style={styles.controlButton} onPress={pickImageFromGallery}>
<View style={styles.albumButton}>
<Ionicons name="images-outline" size={24} color="#FFF" />
</View>
<Text style={styles.controlButtonText}>相册</Text>
</TouchableOpacity> */}
{/* 拍照按钮 */}
<TouchableOpacity style={styles.captureButton} onPress={takePicture}>
<View style={styles.captureButtonInner} />
</TouchableOpacity>
{/* AR按钮 */}
{/* <TouchableOpacity style={styles.controlButton} onPress={handleARPress}>
<View style={styles.arButton}>
<Text style={styles.arButtonText}>AR</Text>
</View>
<Text style={styles.controlButtonText}>AR</Text>
</TouchableOpacity> */}
</View>
</SafeAreaView>
</CameraView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
camera: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#000',
},
loadingText: {
color: '#FFF',
fontSize: 16,
},
permissionContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#000',
paddingHorizontal: 40,
},
permissionTitle: {
color: '#FFF',
fontSize: 20,
fontWeight: '600',
marginTop: 20,
marginBottom: 10,
},
permissionText: {
color: '#CCC',
fontSize: 16,
textAlign: 'center',
marginBottom: 30,
lineHeight: 22,
},
permissionButton: {
backgroundColor: Colors.light.primary,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 24,
},
permissionButtonText: {
color: '#FFF',
fontSize: 16,
fontWeight: '600',
},
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 10,
},
overlayContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 100,
},
hintText: {
color: '#FFF',
fontSize: 16,
fontWeight: '500',
marginBottom: 30,
textAlign: 'center',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
},
viewfinderContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
viewfinder: {
width: 280,
height: 280,
position: 'relative',
},
corner: {
position: 'absolute',
width: 30,
height: 30,
borderColor: '#FFF',
borderWidth: 3,
},
topLeft: {
top: 0,
left: 0,
borderRightWidth: 0,
borderBottomWidth: 0,
},
topRight: {
top: 0,
right: 0,
borderLeftWidth: 0,
borderBottomWidth: 0,
},
bottomLeft: {
bottom: 0,
left: 0,
borderRightWidth: 0,
borderTopWidth: 0,
},
bottomRight: {
bottom: 0,
right: 0,
borderLeftWidth: 0,
borderTopWidth: 0,
},
mealTypeContainer: {
flexDirection: 'row',
justifyContent: 'center',
paddingHorizontal: 20,
marginBottom: 20,
},
mealTypeButton: {
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 8,
marginHorizontal: 8,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
minWidth: 70,
},
mealTypeButtonActive: {
backgroundColor: 'rgba(255, 255, 255, 0.9)',
},
mealTypeIcon: {
fontSize: 20,
marginBottom: 2,
},
mealTypeText: {
color: '#FFF',
fontSize: 12,
fontWeight: '500',
},
mealTypeTextActive: {
color: '#333',
},
bottomContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
controlsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
paddingVertical: 20,
paddingHorizontal: 40,
},
controlButton: {
alignItems: 'center',
},
controlButtonText: {
color: '#FFF',
fontSize: 12,
marginTop: 8,
fontWeight: '500',
},
albumButton: {
width: 50,
height: 50,
borderRadius: 10,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: '#FFF',
},
captureButton: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: '#FFF',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 4,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
captureButtonInner: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#FFF',
borderWidth: 2,
borderColor: '#333',
},
arButton: {
width: 50,
height: 50,
borderRadius: 25,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: '#FFF',
},
arButtonText: {
color: '#FFF',
fontSize: 14,
fontWeight: 'bold',
},
});