Camera Preview in Expo is Distorted

前端 未结 1 1181
闹比i
闹比i 2021-01-26 04:09

I\'m using Camera that comes from expo package and I\'m having trouble with camera preview distortion. The preview makes images appear wider in landscape view and thinner in por

相关标签:
1条回答
  • 2021-01-26 04:30

    This one is kind of tedious.

    Problem

    Basically the problem is that the camera preview is a different width/height ratio from your screen. As far as I can tell, this is only a problem on Android where:

    1. Each camera manufacturer supports different aspect ratios
    2. Each phone manufacturer creates different screen aspect ratios

    Theory

    The way to solve this is essentially to:

    1. Figure out the aspect ratio (and orientation) of the screen
    const { height, width } = Dimensions.get('window');
    const screenRatio = height / width;
    
    1. Wait for camera to be ready
    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. Figure out the supported aspect ratios of the camera
    const ratios = await camera.getSupportedRatiosAsync();
    

    This will return an array of strings with the format ['w:h'], so you might see something like this:

    [ '4:3', '1:1', '16:9' ]
    
    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.

    verticalPadding = (screenHeight - bestRatio * screenWidth) / 2
    

    All together:

    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. 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.
    <Camera
      style={[styles.cameraPreview, {marginTop: imagePadding, marginBottom: imagePadding}]}
      onCameraReady={setCameraReady}
      ratio={ratio}
      ref={(ref) => {
        setCamera(ref);
      }}
    />
    

    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.

    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,
      }
    });
    

    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.

    0 讨论(0)
提交回复
热议问题