onCameraReady function is triggered on camera close

80 Views Asked by At

The problem is, when I open camera the function inside onCameraReady isn't executed.

My Component:

import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { type BarCodeScannedCallback, PermissionStatus } from 'expo-barcode-scanner';
import { Camera } from 'expo-camera';
import { useState, useCallback, useRef } from 'react';
import { View, StyleSheet, Platform, Pressable, Linking } from 'react-native';
import ScannerMarker from '@screens/CodeScanner/ScannerMarker';
import Button from '@components/Button';
import CustomText from '@components/CustomText';
import Layout from '@layouts/Layout';
import CloseIcon from '/assets/closeModal.svg';
import useCameraPermission from '@hooks/useCameraPermission';
import { globalStyles } from '@constants/styles';
import { moderateScale } from '@utils/dimensions';
import { barCodeScannerSettings, chooseBestRatio } from './helpers';
import { styles } from './styles';

const closeButtonSize = moderateScale(30);

const CodeScanner = () => {
  const [cameraRatio, setCameraRatio] = useState('16:9');
  const [scanned, setScanned] = useState(false);
  const cameraRef = useRef<Camera>(null);
  const { goBack } = useNavigation();
  const { t } = useTypedTranslation();
  const { status, canAskAgain, requestPermissions } = useCameraPermission();

  const isCameraPermission = status === PermissionStatus.GRANTED;
  const buttonText = canAskAgain ? "grant permission" : "open settings";

  useFocusEffect(
    useCallback(() => {
      requestPermissions();

      return () => setScanned(false);
    }, [])
  );

  const handleBarCodeScanned: BarCodeScannedCallback = ({ data }) => {
    setScanned(true);
    goBack();

    alert(`Bar code scanned! Received: ${data}`);
  };

  const onCameraReady = async () => {
    if (Platform.OS === 'android') {
      const cameraRatios = await cameraRef.current?.getSupportedRatiosAsync();
      const chosenRatio = chooseBestRatio(cameraRatios);
      setCameraRatio(prevState => chosenRatio ?? prevState);
    }
  };

  if (!status) {
    return null;
  }

  if (status === (PermissionStatus.DENIED || PermissionStatus.UNDETERMINED)) {
    return (
      <Layout>
        <View style={styles.permissionDeniedContainer}>
          <CustomText style={styles.permissionDeniedText} fontWeight="semi-bold">
            No access to the camera
          </CustomText>
          <Button
            onPress={canAskAgain ? requestPermissions : Linking.openSettings}
            type="secondary"
            containerStyle={styles.buttonContainer}
            textStyle={styles.buttonText}
          >
            {buttonText}
          </Button>
        </View>
      </Layout>
    );
  }

  return (
    <View style={styles.container}>
      <Camera
        ref={cameraRef}
        barCodeScannerSettings={barCodeScannerSettings}
        onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
        style={[StyleSheet.absoluteFill, styles.codeScanner]}
        ratio={cameraRatio}
        onCameraReady={onCameraReady}
      >
        {isCameraPermission && (
          <>
            <ScannerMarker />
            <View style={styles.closeContainer}>
              <Pressable style={styles.closeButton} onPress={goBack}>
                <CloseIcon
                  width={closeButtonSize}
                  height={closeButtonSize}
                  color={globalStyles.colors.white}
                />
              </Pressable>
            </View>
          </>
        )}
      </Camera>
    </View>
  );
};

export default CodeScanner;

The function executes when I closed the Camera components. The most interesting thing is when I add console.log() before if check inside onCameraReady function - it works and executes correct when Camera component is open

Modified function:

const onCameraReady = async () => {
    console.log('trigger setting camera Ratio');

    if (Platform.OS === 'android') {
      const cameraRatios = await cameraRef.current?.getSupportedRatiosAsync();
      const chosenRatio = chooseBestRatio(cameraRatios);
      setCameraRatio(prevState => chosenRatio ?? prevState);
    }
};

In the first case without console.log - the ratio isn't set correctly and the camera preview is distorted, and in second case the ratio is set correctly and preview looks good.

Is it possible to trigger function by execute console.log() ? I have never met with problem like above. Maybe someone can help?

I want add this CodeScanner component to scan QR Codes, I have been tried expo-barcode-scanner, but it have problem with fit camera preview to all the screen, and I stay with expo-camera.

I need to detect supported ratios, and pick the best fit, becuase this app would be use by different customers with different devices

1

There are 1 best solutions below

0
Guigui On

I don't have a solution to your problem per se. But I myself was working on android expo camera ratios and decided to mix your code with mine.

The following elementary code works just fine for me, onCameraReady is triggered at startup, as it should be. I hope this will help you :

import { Camera, CameraType } from "expo-camera";
import React, { useEffect, useRef, useState } from "react";
import { Button, Dimensions, Platform, StyleSheet, Text, TouchableOpacity, View } from "react-native";
import MyAppText_Italic from "./components/appFontsTextWrappers/MyAppText_Italic";

const phoneScreenWidth = Dimensions.get("window").width;
const phoneScreenHeight = Dimensions.get("window").height;

function convertRatioToNumber(ratio) {    
  const [numerator, denominator] = ratio.split(":").map(Number);
  return numerator / denominator;
}

export default function CameraScreen_Expo_Test() {
  const [type, setType] = useState(CameraType.back);
  const [permission, requestPermission] = Camera.useCameraPermissions();
  const cameraRef = useRef<Camera>(null);
  const [cameraRatio, setCameraRatio] = useState("4:3");

  if (!permission) {

    return <View />;
  }

  if (!permission.granted) {

    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "black" }}>
        <MyAppText_Italic style={{ textAlign: "center", fontSize: 17, color: "white", marginBottom: 15 }}>We need access to the camera</MyAppText_Italic>
        <Button color={"#5C0272"} onPress={requestPermission} title="Authorize" />
        <MyAppText_Italic style={{ textAlign: "center", fontSize: 12, marginTop: 15, maxWidth: "80%" }}>
   If the button does not work, please allow the app to use the camera in your phone settings
        </MyAppText_Italic>
      </View>
    );
  }

  const onCameraReady = async () => {
    console.log("onCameraReady");

    if (Platform.OS === "android") {

      const cameraRatios = await cameraRef.current?.getSupportedRatiosAsync();
      console.log("cameraRatios", cameraRatios);

      const chosenRatio = findBestCameraRatio(cameraRatios);
      console.log("chosenRatio", chosenRatio);
      setCameraRatio((prevState) => chosenRatio ?? prevState); 
    }
  };

  function findBestCameraRatio(cameraRatios) {

    if (!cameraRatios || cameraRatios.length === 0) {
      return null;
    }

    const targetRatio = 4 / 3;
    let closestRatio = cameraRatios[0]; 

    let smallestDifference = Math.abs(convertRatioToNumber(cameraRatios[0]) - targetRatio); 

    cameraRatios.forEach((ratio) => {

      const [width, height] = ratio.split(":").map(Number);
      const currentRatio = width / height;
      const currentDifference = Math.abs(currentRatio - targetRatio);

      if (currentDifference < smallestDifference) {
        closestRatio = ratio;
        smallestDifference = currentDifference;
      }
    });
    return closestRatio;
  }

  function toggleCameraType() {
    setType((current) => (current === CameraType.back ? CameraType.front : CameraType.back));
  }

  return (
    <View style={styles.container}>
      <Camera
        style={[styles.camera, { width: phoneScreenWidth, height: phoneScreenWidth * convertRatioToNumber(cameraRatio) }]}
        type={type}
        ratio={cameraRatio}
        ref={cameraRef}
        onCameraReady={onCameraReady}
      >
        <View style={styles.buttonContainer}>
          <TouchableOpacity style={styles.button} onPress={toggleCameraType}>
            <Text style={styles.text}>Flip Camera</Text>
          </TouchableOpacity>
        </View>
      </Camera>
    </View>
  );
}

By the way, the way you handle camera permission looks much cleaner than my method, would you mind sharing your @hooks/useCameraPermission ?