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