You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 22:30:30