import { Camera as OriginalCamera } from 'expo-camera';
import React, { useCallback, useState, useEffect } from 'react';
import { View } from 'react-native';
import type { LayoutChangeEvent } from 'react-native';
type CameraRef = OriginalCamera;
type CameraRefCallback = (node: CameraRef) => void;
type Dimensions = { width: number; height: number };
// This Camera component will automatically pick the appropriate ratio and
// dimensions to fill the given layout properties, and it will resize according
// to the same logic as resizeMode: cover. If somehow something goes wrong while
// attempting to autosize, it will just fill the given layout and use the
// default aspect ratio, likely resulting in skew.
export function Camera(props: OriginalCamera['props']) {
const [dimensions, onLayout] = useComponentDimensions();
const [suggestedAspectRatio, suggestedDimensions, ref] = useAutoSize(dimensions);
const [cameraIsReady, setCameraIsReady] = useState(false);
const { style, ...rest } = props;
const { width, height } = suggestedDimensions || {};
return (
setCameraIsReady(true)}
ref={cameraIsReady ? ref : undefined}
ratio={suggestedAspectRatio ?? undefined}
style={
suggestedDimensions && width && height
? {
position: 'absolute',
width,
height,
...(height! > width!
? { top: -(height! - dimensions!.height) / 2 }
: { left: -(width! - dimensions!.width) / 2 }),
}
: { flex: 1 }
}
{...rest}
/>
);
}
function useAutoSize(
dimensions: Dimensions | null
): [string | null, Dimensions | null, CameraRefCallback] {
const [supportedAspectRatios, ref] = useSupportedAspectRatios();
const [suggestedAspectRatio, setSuggestedAspectRatio] = useState(null);
const [suggestedDimensions, setSuggestedDimensions] = useState(null);
useEffect(() => {
const suggestedAspectRatio = findClosestAspectRatio(supportedAspectRatios, dimensions);
const suggestedDimensions = calculateSuggestedDimensions(dimensions, suggestedAspectRatio);
if (!suggestedAspectRatio || !suggestedDimensions) {
setSuggestedAspectRatio(null);
setSuggestedDimensions(null);
} else {
setSuggestedAspectRatio(suggestedAspectRatio);
setSuggestedDimensions(suggestedDimensions);
}
}, [dimensions, supportedAspectRatios]);
return [suggestedAspectRatio, suggestedDimensions, ref];
}
// Get the supported aspect ratios from the camera ref when the node is available
// NOTE: this will fail if the camera isn't ready yet. So we need to avoid setting the
// ref until the camera ready callback has fired
function useSupportedAspectRatios(): [string[] | null, CameraRefCallback] {
const [aspectRatios, setAspectRatios] = useState(null);
const ref = useCallback(
(node: CameraRef | null) => {
async function getSupportedAspectRatiosAsync(node: OriginalCamera) {
try {
const result = await node.getSupportedRatiosAsync();
setAspectRatios(result);
} catch (e) {
console.error(e);
}
}
if (node !== null) {
getSupportedAspectRatiosAsync(node);
}
},
[setAspectRatios]
);
return [aspectRatios, ref];
}
const useComponentDimensions = (): [Dimensions | null, (e: any) => void] => {
const [dimensions, setDimensions] = useState(null);
const onLayout = useCallback(
(event: LayoutChangeEvent) => {
const { width, height } = event.nativeEvent.layout;
setDimensions({ width, height });
},
[setDimensions]
);
return [dimensions, onLayout];
};
function ratioStringToNumber(ratioString: string) {
const [a, b] = ratioString.split(':');
return parseInt(a, 10) / parseInt(b, 10);
}
function findClosestAspectRatio(
supportedAspectRatios: string[] | null,
dimensions: Dimensions | null
) {
if (!supportedAspectRatios || !dimensions) {
return null;
}
try {
const dimensionsRatio =
Math.max(dimensions.height, dimensions.width) / Math.min(dimensions.height, dimensions.width);
const aspectRatios = [...supportedAspectRatios];
aspectRatios.sort((a: string, b: string) => {
const ratioA = ratioStringToNumber(a);
const ratioB = ratioStringToNumber(b);
return Math.abs(dimensionsRatio - ratioA) - Math.abs(dimensionsRatio - ratioB);
});
return aspectRatios[0];
} catch (e) {
// If something unexpected happens just bail out
console.error(e);
return null;
}
}
function calculateSuggestedDimensions(
containerDimensions: Dimensions | null,
ratio: string | null
) {
if (!ratio || !containerDimensions) {
return null;
}
try {
const ratioNumber = ratioStringToNumber(ratio);
const width = containerDimensions.width;
const height = width * ratioNumber;
return { width, height };
} catch (e) {
// If something unexpected happens just bail out
console.error(e);
return null;
}
}