Expo中的相机预览失真

Bla*_*lak 8 android reactjs react-native expo

我正在使用来自 expo 包的相机,但我遇到了相机预览失真的问题。预览使图像在横向视图中显得更宽,在纵向视图中显得更薄。我发现的大多数解决方案都没有使用 expo-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>

        );
    };
};

Run Code Online (Sandbox Code Playgroud)

样式.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",
    },
});

Run Code Online (Sandbox Code Playgroud)

我能做些什么来解决这个问题?

Ado*_*zis 27

这个有点乏味。

问题

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

  1. 每个相机制造商支持不同的纵横比
  2. 每个手机制造商创建不同的屏幕纵横比

理论

解决这个问题的方法主要是:

  1. 找出屏幕的纵横比(和方向)
const { height, width } = Dimensions.get('window');
const screenRatio = height / width;
Run Code Online (Sandbox Code Playgroud)
  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>
);
Run Code Online (Sandbox Code Playgroud)
  1. 找出相机支持的纵横比
const ratios = await camera.getSupportedRatiosAsync();
Run Code Online (Sandbox Code Playgroud)

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

[ '4:3', '1:1', '16:9' ]
Run Code Online (Sandbox Code Playgroud)
  1. Find the camera's closest aspect ratio to the screen where the height does not exceed the screen ratio (assuming you want a horizontal buffer, not a vertical buffer)

Essentially what you are trying to do here is to loop through the supported camera ratios and determine which of them are the closest in proportion to the screen. Any that are too tall we toss out since in this example we want to the preview to take up the entire width of the screen and we don't mind if the preview is shorter than the screen in portrait mode.

a) Get screen aspect ratio

So let's say that the screen is 480w x 800h, then the aspect ratio of the height / width is 1.666... If we were in landscape mode, we would do width / height.

b) Get supported camera aspect ratios

Then we look at each camera aspect ratio and calculate the width / height. The reason we calculate this and not the height / width like we do the screen is that the camera aspect ratios are always in landscape mode.

So:

  • Aspect => calculation
  • 4:3 => 1.3333
  • 1:1 => 1
  • 16:9 => 1.77777

c) Calculate supported camera aspect ratios

For each one, we subtract from the aspect ratio of the screen to find the difference. Any that exceed the aspect ratio of the screen on the long side are discarded:

  • Aspect => calculation => difference from screen
  • 4:3 => 1.333... => 0.333... (closest without going over!)
  • 1:1 => 1 => 0.666... (worst match)
  • 16:9 => 1.777... => -0.111... (too wide)

d) closest shortest camera aspect ratio matching screen aspect ratio

So we pick the 4:3 aspect ratio for this camera on this screen.

e) Calculate difference between camera aspect ratio and screen aspect ratio for padding and positioning.

To position the preview in the center of the screen, we can calculate half the difference between the screen height and the scaled height of the camera preview.

[ '4:3', '1:1', '16:9' ]
Run Code Online (Sandbox Code Playgroud)

All together:

verticalPadding = (screenHeight - bestRatio * screenWidth) / 2
Run Code Online (Sandbox Code Playgroud)
  1. Style the <Camera> component to have the appropriate scaled height to match the applied camera aspect ratio and to be centered or whatever in the screen.
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);
Run Code Online (Sandbox Code Playgroud)

Something to note is that the camera aspect ratios are always width:height in landscape mode, but your screen might be in either portrait or landscape.

Execution

This example only supports a portrait-mode screen. To support both screen types, you'll have to check the screen orientation and change the calculations based on which orientation the device is in.

<Camera
  style={[styles.cameraPreview, {marginTop: imagePadding, marginBottom: imagePadding}]}
  onCameraReady={setCameraReady}
  ratio={ratio}
  ref={(ref) => {
    setCamera(ref);
  }}
/>
Run Code Online (Sandbox Code Playgroud)

You can play with the Expo Snack here

Results

And finally, a camera preview with preserved proportions, which uses padding on the top and bottom to center the preview:

安卓截图

You can also try this code out online or in your Android on Expo Snack.


abu*_*ick 6

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

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

const CameraComponent = () => {
  const {width} = useWindowDimensions();
  const height = Math.round((width * 16) / 9);
  return (
    <Camera
      ratio="16:9"
      style={{
        height: height,
        width: "100%",
      }}
    ></Camera>
  );
};

export default CameraComponent;
Run Code Online (Sandbox Code Playgroud)