求助:React Native实现类似Google Pay的框选式相机拍照功能
React Native实现取景框式相机(仅捕获框内区域)
核心思路是:先获取全屏相机画面,再根据取景框的位置/尺寸裁剪对应区域,同时在UI层叠加半透明遮罩+取景框引导用户,和Google Pay的扫码逻辑一致。以下是基于react-native-vision-camera(当前维护的主流相机库)的完整实现方案:
步骤1:安装依赖
npm install react-native-vision-camera react-native-image-editor
步骤2:配置权限
- iOS:在
Info.plist中添加相机权限声明<key>NSCameraUsageDescription</key> <string>需要使用相机进行拍摄</string> - Android:在
AndroidManifest.xml中添加权限<uses-permission android:name="android.permission.CAMERA" />
步骤3:完整组件实现
import React, { useState, useRef, useEffect } from 'react'; import { View, StyleSheet, TouchableOpacity, Text, Dimensions } from 'react-native'; import { Camera, useCameraDevices, takePhoto } from 'react-native-vision-camera'; import ImageEditor from 'react-native-image-editor'; // 定义取景框尺寸(可根据需求调整) const SCREEN_WIDTH = Dimensions.get('window').width; const FRAME_SIZE = SCREEN_WIDTH * 0.7; // 取景框宽度为屏幕70% const FRAME_TOP = (Dimensions.get('window').height - FRAME_SIZE) / 2; // 垂直居中 const FrameCamera = () => { const devices = useCameraDevices('back'); const cameraDevice = devices.back; const cameraRef = useRef(null); const [hasPermission, setHasPermission] = useState(false); // 请求相机权限 useEffect(() => { (async () => { const status = await Camera.requestCameraPermission(); setHasPermission(status === 'authorized'); })(); }, []); // 拍摄并裁剪图像 const handleCapture = async () => { if (!cameraRef.current) return; try { // 拍摄全屏照片 const photo = await takePhoto(cameraRef.current, { quality: 0.8 }); const imageSize = { width: photo.width, height: photo.height }; const screenSize = Dimensions.get('window'); // 计算预览与实际图像的比例适配,避免裁剪偏移 const previewRatio = screenSize.width / screenSize.height; const imageRatio = imageSize.width / imageSize.height; let cropRegion; if (imageRatio > previewRatio) { // 图像比预览宽,上下存在黑边 const visibleImageHeight = imageSize.width / previewRatio; const verticalOffset = (imageSize.height - visibleImageHeight) / 2; cropRegion = { offset: { x: ((screenSize.width - FRAME_SIZE) / 2 / screenSize.width) * imageSize.width, y: verticalOffset + (FRAME_TOP / screenSize.height) * visibleImageHeight }, size: { width: (FRAME_SIZE / screenSize.width) * imageSize.width, height: (FRAME_SIZE / screenSize.height) * visibleImageHeight } }; } else { // 图像比预览高,左右存在黑边 const visibleImageWidth = imageSize.height * previewRatio; const horizontalOffset = (imageSize.width - visibleImageWidth) / 2; cropRegion = { offset: { x: horizontalOffset + ((screenSize.width - FRAME_SIZE) / 2 / screenSize.width) * visibleImageWidth, y: (FRAME_TOP / screenSize.height) * imageSize.height }, size: { width: (FRAME_SIZE / screenSize.width) * visibleImageWidth, height: (FRAME_SIZE / screenSize.height) * imageSize.height } }; } // 执行裁剪 const croppedUri = await ImageEditor.cropImage(photo.uri, cropRegion); // 后续可处理裁剪后的图片:显示、上传或保存 console.log('裁剪完成,URI:', croppedUri); } catch (error) { console.error('拍摄/裁剪失败:', error); } }; if (!cameraDevice || !hasPermission) return <View />; return ( <View style={styles.container}> {/* 相机预览 */} <Camera style={StyleSheet.absoluteFill} device={cameraDevice} ref={cameraRef} isActive={true} /> {/* 半透明遮罩+取景框 */} <View style={styles.maskWrapper}> <View style={[styles.mask, styles.topMask]} /> <View style={styles.middleRow}> <View style={styles.mask} /> <View style={styles.frame} /> <View style={styles.mask} /> </View> <View style={[styles.mask, styles.bottomMask]} /> </View> {/* 拍摄按钮 */} <TouchableOpacity style={styles.captureBtn} onPress={handleCapture}> <Text style={styles.captureText}>拍摄</Text> </TouchableOpacity> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1 }, maskWrapper: { flex: 1 }, mask: { backgroundColor: 'rgba(0,0,0,0.7)' }, topMask: { height: FRAME_TOP }, middleRow: { flexDirection: 'row', height: FRAME_SIZE }, bottomMask: { flex: 1 }, frame: { width: FRAME_SIZE, borderWidth: 2, borderColor: '#ffffff', borderStyle: 'solid' }, captureBtn: { position: 'absolute', bottom: 30, alignSelf: 'center', backgroundColor: '#fff', paddingHorizontal: 20, paddingVertical: 12, borderRadius: 25 }, captureText: { fontSize: 16, fontWeight: '600' } }); export default FrameCamera;
关键注意点
- 比例适配:必须处理相机预览与实际拍摄图像的比例差异,否则裁剪区域会出现偏移(这是很多开发者踩坑的核心点)
- 性能优化:裁剪操作会占用一定资源,建议在拍摄后将裁剪逻辑放在异步任务中执行,避免阻塞UI
- 替代方案:如果使用
react-native-camera-kit,UI层的遮罩/取景框逻辑完全一致,仅需替换拍摄API,裁剪步骤通用
内容的提问来源于stack exchange,提问作者dev soft
相关产品推荐
相关产品推荐

