1import { Camera as OriginalCamera } from 'expo-camera'; 2import React, { useCallback, useState, useEffect } from 'react'; 3import { View } from 'react-native'; 4import type { LayoutChangeEvent } from 'react-native'; 5 6type CameraRef = OriginalCamera; 7type CameraRefCallback = (node: CameraRef) => void; 8type Dimensions = { width: number; height: number }; 9 10// This Camera component will automatically pick the appropriate ratio and 11// dimensions to fill the given layout properties, and it will resize according 12// to the same logic as resizeMode: cover. If somehow something goes wrong while 13// attempting to autosize, it will just fill the given layout and use the 14// default aspect ratio, likely resulting in skew. 15export function Camera(props: OriginalCamera['props']) { 16 const [dimensions, onLayout] = useComponentDimensions(); 17 const [suggestedAspectRatio, suggestedDimensions, ref] = useAutoSize(dimensions); 18 const [cameraIsReady, setCameraIsReady] = useState(false); 19 const { style, ...rest } = props; 20 const { width, height } = suggestedDimensions || {}; 21 22 return ( 23 <View 24 onLayout={onLayout} 25 style={[ 26 { 27 overflow: 'hidden', 28 backgroundColor: '#000', 29 alignItems: 'center', 30 justifyContent: 'center', 31 }, 32 style, 33 ]}> 34 <OriginalCamera 35 onCameraReady={() => setCameraIsReady(true)} 36 ref={cameraIsReady ? ref : undefined} 37 ratio={suggestedAspectRatio ?? undefined} 38 style={ 39 suggestedDimensions && width && height 40 ? { 41 position: 'absolute', 42 width, 43 height, 44 ...(height! > width! 45 ? { top: -(height! - dimensions!.height) / 2 } 46 : { left: -(width! - dimensions!.width) / 2 }), 47 } 48 : { flex: 1 } 49 } 50 {...rest} 51 /> 52 </View> 53 ); 54} 55 56function useAutoSize( 57 dimensions: Dimensions | null 58): [string | null, Dimensions | null, CameraRefCallback] { 59 const [supportedAspectRatios, ref] = useSupportedAspectRatios(); 60 const [suggestedAspectRatio, setSuggestedAspectRatio] = useState<string | null>(null); 61 const [suggestedDimensions, setSuggestedDimensions] = useState<Dimensions | null>(null); 62 63 useEffect(() => { 64 const suggestedAspectRatio = findClosestAspectRatio(supportedAspectRatios, dimensions); 65 const suggestedDimensions = calculateSuggestedDimensions(dimensions, suggestedAspectRatio); 66 67 if (!suggestedAspectRatio || !suggestedDimensions) { 68 setSuggestedAspectRatio(null); 69 setSuggestedDimensions(null); 70 } else { 71 setSuggestedAspectRatio(suggestedAspectRatio); 72 setSuggestedDimensions(suggestedDimensions); 73 } 74 }, [dimensions, supportedAspectRatios]); 75 76 return [suggestedAspectRatio, suggestedDimensions, ref]; 77} 78 79// Get the supported aspect ratios from the camera ref when the node is available 80// NOTE: this will fail if the camera isn't ready yet. So we need to avoid setting the 81// ref until the camera ready callback has fired 82function useSupportedAspectRatios(): [string[] | null, CameraRefCallback] { 83 const [aspectRatios, setAspectRatios] = useState<string[] | null>(null); 84 85 const ref = useCallback( 86 (node: CameraRef | null) => { 87 async function getSupportedAspectRatiosAsync(node: OriginalCamera) { 88 try { 89 const result = await node.getSupportedRatiosAsync(); 90 setAspectRatios(result); 91 } catch (e) { 92 console.error(e); 93 } 94 } 95 96 if (node !== null) { 97 getSupportedAspectRatiosAsync(node); 98 } 99 }, 100 [setAspectRatios] 101 ); 102 103 return [aspectRatios, ref]; 104} 105 106const useComponentDimensions = (): [Dimensions | null, (e: any) => void] => { 107 const [dimensions, setDimensions] = useState<Dimensions | null>(null); 108 109 const onLayout = useCallback( 110 (event: LayoutChangeEvent) => { 111 const { width, height } = event.nativeEvent.layout; 112 setDimensions({ width, height }); 113 }, 114 [setDimensions] 115 ); 116 117 return [dimensions, onLayout]; 118}; 119 120function ratioStringToNumber(ratioString: string) { 121 const [a, b] = ratioString.split(':'); 122 return parseInt(a, 10) / parseInt(b, 10); 123} 124 125function findClosestAspectRatio( 126 supportedAspectRatios: string[] | null, 127 dimensions: Dimensions | null 128) { 129 if (!supportedAspectRatios || !dimensions) { 130 return null; 131 } 132 133 try { 134 const dimensionsRatio = 135 Math.max(dimensions.height, dimensions.width) / Math.min(dimensions.height, dimensions.width); 136 137 const aspectRatios = [...supportedAspectRatios]; 138 aspectRatios.sort((a: string, b: string) => { 139 const ratioA = ratioStringToNumber(a); 140 const ratioB = ratioStringToNumber(b); 141 return Math.abs(dimensionsRatio - ratioA) - Math.abs(dimensionsRatio - ratioB); 142 }); 143 144 return aspectRatios[0]; 145 } catch (e) { 146 // If something unexpected happens just bail out 147 console.error(e); 148 return null; 149 } 150} 151 152function calculateSuggestedDimensions( 153 containerDimensions: Dimensions | null, 154 ratio: string | null 155) { 156 if (!ratio || !containerDimensions) { 157 return null; 158 } 159 160 try { 161 const ratioNumber = ratioStringToNumber(ratio); 162 const width = containerDimensions.width; 163 const height = width * ratioNumber; 164 return { width, height }; 165 } catch (e) { 166 // If something unexpected happens just bail out 167 console.error(e); 168 return null; 169 } 170} 171