Skip to content

Commit

Permalink
Merge pull request #703 from tonwhales/release/v2.0.0
Browse files Browse the repository at this point in the history
[ANDROID]: aspect ratio fix for camera
  • Loading branch information
dvlkv authored Jan 10, 2024
2 parents 2a3f3db + 757c752 commit 91d1bf3
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 9 deletions.
51 changes: 42 additions & 9 deletions app/fragments/utils/ScannerFragment.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Text, View, StyleSheet, Pressable, Platform, Linking, Alert, Image } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as Application from 'expo-application';
Expand All @@ -17,6 +17,7 @@ import { BarCodeScanner, BarCodeScannerResult } from 'expo-barcode-scanner';
import { Camera, FlashMode } from 'expo-camera';
import { useTheme } from '../../engine/hooks';
import { Typography } from '../../components/styles';
import { useCameraAspectRatio } from '../../utils/useCameraAspectRatio';

import FlashOn from '../../../assets/ic-flash-on.svg';
import FlashOff from '../../../assets/ic-flash-off.svg';
Expand All @@ -38,6 +39,21 @@ export const ScannerFragment = systemFragment(() => {
const [isActive, setActive] = useState(true);
const [flashOn, setFlashOn] = useState(false);

const cameraRef = useRef<Camera>(null);

// Screen Ratio and image padding for Android
// The issue arises from the discrepancy between the camera preview's aspect ratio and the screen's aspect ratio.
// Possible causes:
// 1. Different camera manufacturers support different aspect ratios.
// 2. Different phone manufacturers design screens with varying aspect ratios.
const { ratio, imagePadding, prepareRatio } = useCameraAspectRatio();

const onCameraReady = useCallback(() => {
if (!!cameraRef.current) {
prepareRatio(cameraRef.current);
}
}, []);

const onReadFromMedia = useCallback(async () => {
try {
const { status } = await RNImagePicker.requestMediaLibraryPermissionsAsync()
Expand Down Expand Up @@ -163,9 +179,15 @@ export const ScannerFragment = systemFragment(() => {

<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
<Camera
ref={cameraRef}
onBarCodeScanned={!isActive ? undefined : onScanned}
style={StyleSheet.absoluteFill}
style={[
StyleSheet.absoluteFill,
Platform.select({ android: { marginTop: imagePadding, marginBottom: imagePadding } })
]}
flashMode={flashOn ? FlashMode.torch : FlashMode.off}
onCameraReady={onCameraReady}
ratio={ratio}
/>
</View>

Expand All @@ -191,12 +213,17 @@ export const ScannerFragment = systemFragment(() => {
</Text>
</View>
<View style={{ flexGrow: 1 }} />
<View style={{
flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center',
paddingHorizontal: 16,
marginBottom: safeArea.bottom === 0 ? 24 : safeArea.bottom + 24
}}>
<View style={[
{
flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center',
paddingHorizontal: 16
},
Platform.select({
android: { marginBottom: imagePadding + 16 },
ios: { marginBottom: safeArea.bottom === 0 ? 24 : safeArea.bottom + 24 },
}),
]}>
<Pressable style={(props) => {
return {
opacity: props.pressed ? 0.5 : 1,
Expand Down Expand Up @@ -233,7 +260,13 @@ export const ScannerFragment = systemFragment(() => {
</Pressable>
</View>
<ScreenHeader
style={{ position: 'absolute', top: Platform.OS === 'android' ? 32 : 0, left: 0, right: 0 }}
style={[
{ position: 'absolute', left: 0, right: 0 },
Platform.select({
android: { top: imagePadding },
ios: { top: 0 },
})
]}
onClosePressed={() => {
setActive(false);
setTimeout(() => {
Expand Down
54 changes: 54 additions & 0 deletions app/utils/useCameraAspectRatio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Camera } from "expo-camera";
import { useState } from "react";
import { Dimensions, Platform } from "react-native";

export function useCameraAspectRatio() {
// Screen Ratio and image padding for Android
const [previewSettings, setPreviewSettings] = useState<{
imagePadding: number;
ratio: string | undefined;
}>({ imagePadding: 0, ratio: undefined });
const { height, width } = Dimensions.get('window');
const screenRatio = height / width;

// set the camera ratio and padding (portrait mode)
const prepareRatio = async (camera: Camera) => {
let desiredRatio = '4:3'; // Start with the system default
// This issue only affects Android
if (Platform.OS === 'android') {
const ratios = await camera.getSupportedRatiosAsync();

// Calculate the width/height of each of the supported camera ratios
// These width/height are measured in landscape mode
// find the ratio that is closest to the screen ratio without going over
let distances: { [key: string]: number } = {};
let realRatios: { [key: string]: number } = {};
let minDistance: string | null = null;
for (const ratio of (ratios ?? [])) {
const parts = ratio.split(':');
const realRatio = parseInt(parts[0]) / parseInt(parts[1]);
realRatios[ratio] = realRatio;
// ratio can't be taller than screen, so we don't want an abs()
const distance = screenRatio - realRatio;
distances[ratio] = distance;
if (minDistance == null) {
minDistance = ratio;
} else {
if (distance >= 0 && distance < distances[minDistance]) {
minDistance = ratio;
}
}
}
// set the best match
desiredRatio = minDistance ?? '4:3';
// calculate the diff between the camera width and the screen height
const remainder = Math.floor(
(height - realRatios[desiredRatio] * width) / 2
);
// set padding and ratio
setPreviewSettings({ imagePadding: remainder, ratio: desiredRatio });
}
};

return { ratio: previewSettings.ratio, imagePadding: previewSettings.imagePadding, prepareRatio };
}

0 comments on commit 91d1bf3

Please sign in to comment.