xref: /expo/home/components/Camera.android.tsx (revision bb8f4f99)
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