世博会中的相机预览变形

时间:2019-10-30 23:19:57

标签: android reactjs react-native expo

我正在使用来自expo软件包的Camera,但遇到相机预览失真的问题。预览使图像在横向视图中看起来更宽,在纵向视图中看起来更细。我发现的大多数解决方案都没有使用Expo-Camera。

相关代码:

camera.page.js:

import React from 'react';
import { View, Text } from 'react-native';
import { Camera } from 'expo-camera';
import * as Permissions from 'expo-permissions'
import { Platform } from 'react-native';

import styles from './styles';
import Toolbar from './toolbar.component';

const DESIRED_RATIO = "18:9";

export default class CameraPage extends React.Component {
    camera = null;

    state = {
        hasCameraPermission: null,
    };

    async componentDidMount() {
        const camera = await Permissions.askAsync(Permissions.CAMERA);
        const audio = await Permissions.askAsync(Permissions.AUDIO_RECORDING);
        const hasCameraPermission = (camera.status === 'granted' && audio.status === 'granted');

        this.setState({ hasCameraPermission });
    };


    render() {
        const { hasCameraPermission } = this.state;

        if (hasCameraPermission === null) {
            return <View />;
        } else if (hasCameraPermission === false) {
            return <Text>Access to camera has been denied.</Text>;
        }

        return (
          <React.Fragment>
            <View>
              <Camera
                ref={camera => this.camera = camera}
                style={styles.preview}
                />
            </View>
            <Toolbar/>
          </React.Fragment>

        );
    };
};

styles.js:

import { StyleSheet, Dimensions } from 'react-native';

const { width: winWidth, height: winHeight } = Dimensions.get('window');
export default StyleSheet.create({
    preview: {
        height: winHeight,
        width: winWidth,
        position: 'absolute',
        left: 0,
        top: 0,
        right: 0,
        bottom: 0,
        paddingBottom: 1000,
    },
    alignCenter: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
    },
    bottomToolbar: {
        width: winWidth,
        position: 'absolute',
        height: 100,
        bottom: 0,
    },
    captureBtn: {
        width: 60,
        height: 60,
        borderWidth: 2,
        borderRadius: 60,
        borderColor: "#FFFFFF",
    },
    captureBtnActive: {
        width: 80,
        height: 80,
    },
    captureBtnInternal: {
        width: 76,
        height: 76,
        borderWidth: 2,
        borderRadius: 76,
        backgroundColor: "red",
        borderColor: "transparent",
    },
});

该如何解决?

2 个答案:

答案 0 :(得分:6)

这很乏味。

问题

基本上,问题在于摄像头预览与屏幕的宽高比不同。据我所知,这只是Android上的一个问题,其中:

  1. 每个相机制造商都支持不同的长宽比
  2. 每个电话制造商都会创建不同的屏幕长宽比

理论

解决此问题的方法实质上是:

  1. 弄清楚屏幕的长宽比(和方向)
const { height, width } = Dimensions.get('window');
const screenRatio = height / width;
  1. 等待相机准备就绪
const [isRatioSet, setIsRatioSet] = useState(false);

// the camera must be loaded in order to 
// access the supported ratios
const setCameraReady = async() => {
  if (!isRatioSet) {
    await prepareRatio();
  }
};

return (
  <Camera
    onCameraReady={setCameraReady}
    ref={(ref) => {
      setCamera(ref);
    }}>
  </Camera>
);
  1. 弄清楚相机支持的长宽比
const ratios = await camera.getSupportedRatiosAsync();

这将返回格式为['w:h']的字符串数组,因此您可能会看到类似这样的内容:

[ '4:3', '1:1', '16:9' ]
  1. 在高度不超过屏幕比例的屏幕上找到相机最接近的宽高比(假设您需要水平缓冲区,而不是垂直缓冲区)

基本上,您想要在此处进行的操作是遍历受支持的摄像机比例,并确定其中哪一个比例与屏幕比例最接近。任何一个过高的东西我们都扔掉,因为在此示例中,我们希望预览占据屏幕的整个宽度,并且我们不介意预览是否比纵向模式下的屏幕短。

a)获取屏幕宽高比

因此,假设屏幕为480w x 800h,那么高度/宽度的宽高比为1.666...。如果我们处于横向模式,则将采用宽度/高度。

b)获取支持的相机长宽比

然后,我们查看每个摄像机的纵横比,并计算宽度/高度。我们之所以进行此计算,而不是像在屏幕上那样计算高度/宽度,是因为相机的纵横比在横向模式下始终为

所以:

  • 方面=>计算
  • 4:3 => 1.3333
  • 1:1 => 1
  • 16:9 => 1.77777

c)计算支持的相机长宽比

对于每一个,我们从屏幕的纵横比中减去以找到差异。超过长边屏幕长宽比的任何东西都将被丢弃:

  • 方面=>计算=>与屏幕的差异
  • 4:3 => 1.333... => 0.333...最靠近而不能越过!
  • 1:1 => 1 => 0.666...(最差比赛)
  • 16:9 => 1.777... => -0.111...(太宽了)

d)最短的相机纵横比与屏幕纵横比匹配

因此,我们在此屏幕上为此摄像机选择4:3宽高比。

e)计算摄像机宽高比与屏幕宽高比之间的差异,以进行填充和定位。

要将预览放置在屏幕中央,我们可以计算屏幕高度和摄像机预览的缩放高度之间的差值的一半。

verticalPadding = (screenHeight - bestRatio * screenWidth) / 2

在一起:

let distances = {};
let realRatios = {};
let minDistance = null;
for (const ratio of ratios) {
  const parts = ratio.split(':');
  const realRatio = parseInt(parts[0]) / parseInt(parts[1]);
  realRatios[ratio] = realRatio;
  // ratio can't be taller than screen, so we don't want an abs()
  const distance = screenRatio - realRatio; 
  distances[ratio] = realRatio;
  if (minDistance == null) {
    minDistance = ratio;
  } else {
    if (distance >= 0 && distance < distances[minDistance]) {
      minDistance = ratio;
    }
  }
}
// set the best match
desiredRatio = minDistance;
//  calculate the difference between the camera width and the screen height
const remainder = Math.floor(
  (height - realRatios[desiredRatio] * width) / 2
);
// set the preview padding and preview ratio
setImagePadding(remainder / 2);
  1. 设置<Camera>组件的样式为适当的缩放高度,以匹配所应用的相机纵横比并居中或在屏幕中居中显示。
<Camera
  style={[styles.cameraPreview, {marginTop: imagePadding, marginBottom: imagePadding}]}
  onCameraReady={setCameraReady}
  ratio={ratio}
  ref={(ref) => {
    setCamera(ref);
  }}
/>

需要注意的是,在横向模式下,相机的纵横比始终为width:height,但屏幕可能是纵向或横向。

执行

此示例仅支持纵向模式屏幕。要同时支持两种屏幕类型,您必须检查screen orientation并根据设备所处的方向更改计算。

import React, { useEffect, useState } from 'react';
import {StyleSheet, View, Text, Dimensions, Platform } from 'react-native';
import { Camera } from 'expo-camera';
import * as Permissions from 'expo-permissions';

export default function App() {
  //  camera permissions
  const [hasCameraPermission, setHasCameraPermission] = useState(null);
  const [camera, setCamera] = useState(null);

  // Screen Ratio and image padding
  const [imagePadding, setImagePadding] = useState(0);
  const [ratio, setRatio] = useState('4:3');  // default is 4:3
  const { height, width } = Dimensions.get('window');
  const screenRatio = height / width;
  const [isRatioSet, setIsRatioSet] =  useState(false);

  // on screen  load, ask for permission to use the camera
  useEffect(() => {
    async function getCameraStatus() {
      const { status } = await Permissions.askAsync(Permissions.CAMERA);
      setHasCameraPermission(status == 'granted');
    }
    getCameraStatus();
  }, []);

  // set the camera ratio and padding.
  // this code assumes a portrait mode screen
  const prepareRatio = async () => {
    let desiredRatio = '4:3';  // Start with the system default
    // This issue only affects Android
    if (Platform.OS === 'android') {
      const ratios = await camera.getSupportedRatiosAsync();

      // Calculate the width/height of each of the supported camera ratios
      // These width/height are measured in landscape mode
      // find the ratio that is closest to the screen ratio without going over
      let distances = {};
      let realRatios = {};
      let minDistance = null;
      for (const ratio of ratios) {
        const parts = ratio.split(':');
        const realRatio = parseInt(parts[0]) / parseInt(parts[1]);
        realRatios[ratio] = realRatio;
        // ratio can't be taller than screen, so we don't want an abs()
        const distance = screenRatio - realRatio; 
        distances[ratio] = realRatio;
        if (minDistance == null) {
          minDistance = ratio;
        } else {
          if (distance >= 0 && distance < distances[minDistance]) {
            minDistance = ratio;
          }
        }
      }
      // set the best match
      desiredRatio = minDistance;
      //  calculate the difference between the camera width and the screen height
      const remainder = Math.floor(
        (height - realRatios[desiredRatio] * width) / 2
      );
      // set the preview padding and preview ratio
      setImagePadding(remainder / 2);
      setRatio(desiredRatio);
      // Set a flag so we don't do this 
      // calculation each time the screen refreshes
      setIsRatioSet(true);
    }
  };

  // the camera must be loaded in order to access the supported ratios
  const setCameraReady = async() => {
    if (!isRatioSet) {
      await prepareRatio();
    }
  };

  if (hasCameraPermission === null) {
    return (
      <View style={styles.information}>
        <Text>Waiting for camera permissions</Text>
      </View>
    );
  } else if (hasCameraPermission === false) {
    return (
      <View style={styles.information}>
        <Text>No access to camera</Text>
      </View>
    );
  } else {
    return (
      <View style={styles.container}>
        {/* 
        We created a Camera height by adding margins to the top and bottom, 
        but we could set the width/height instead 
        since we know the screen dimensions
        */}
        <Camera
          style={[styles.cameraPreview, {marginTop: imagePadding, marginBottom: imagePadding}]}
          onCameraReady={setCameraReady}
          ratio={ratio}
          ref={(ref) => {
            setCamera(ref);
          }}>
        </Camera>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  information: { 
    flex: 1,
    justifyContent: 'center',
    alignContent: 'center',
    alignItems: 'center',
  },
  container: {
    flex: 1,
    backgroundColor: '#000',
    justifyContent: 'center',
  },
  cameraPreview: {
    flex: 1,
  }
});

结果

最后,使用比例保留的摄像机预览,使用顶部和底部的填充使预览居中:

Android screenshot

您也可以try this code out online或在Android上的Expo Snack上

答案 1 :(得分:0)

纵向模式下的简单解决方案:

import * as React from "react";
import { Camera } from "expo-camera";
import { Dimensions } from "react-native";

const CameraComponent = () => {
  const dimensions = useRef(Dimensions.get("window"));
  const screenWidth = dimensions.current.width;
  const height = Math.round((screenWidth * 16) / 9);
  return (
    <Camera
      ratio="16:9"
      style={{
        height: height,
        width: "100%",
      }}
    ></Camera>
  );
};

export default CameraComponent;