🐛 Fish-eye (ultra-wide-angle) not working in android RN 0.72
What's happening?
I am trying to set zoom to x0.5(ultra wide camera) as same as the one on native camera.But the neutral zoom is always 1. On mobile native camera it displays 0.5x and can zoom-out but on android it doesn't work. I have read the documentation that says for 0.5x default will be greater than mini-zoom but in my case its always 1
I have tried on Samsung A52 ,one plus8. Its working fine on ios but not for android
Device & Environment Details: Device: Samsung A52, OnePlus8 OS Version: Android 14 Camera Library: react-native-ivision-camera ( version 4.0.0-beta.13) React Native Version: 0.72.6 targetSdkVersion = 34 minSdkVersion = 24
Reproduceable Code
const cameraRef = useRef<Camera | null>(null);
const device = useCameraDevice('front', {
physicalDevices: [
'wide-angle-camera',
'ultra-wide-angle-camera',
'telephoto-camera',
],
});
<Camera
ref={cameraRef}
style={[styles.camera]}
device={device!}
resizeMode="cover"
enableZoomGesture
photo={true}
zoom={zoom}
/>
Relevant log output
None
Camera Device
{
"sensorOrientation": "landscape-right",
"hardwareLevel": "limited",
"maxZoom": 8,
"minZoom": 1,
"maxExposure": 20,
"supportsLowLightBoost": false,
"neutralZoom": 1,
"physicalDevices": [
"wide-angle-camera"
],
"supportsFocus": true,
"supportsRawCapture": false,
"isMultiCam": false,
"minFocusDistance": 0,
"minExposure": -20,
"name": "1 (FRONT) androidx.camera.camera2",
"hasFlash": false,
"hasTorch": false,
"position": "front",
"id": "1"
}
Device
Samsung A-52, One Plus 8
VisionCamera Version
4.0.0-beta.13
Can you reproduce this issue in the VisionCamera Example app?
I didn't try (⚠️ your issue might get ignored & closed if you don't try this)
Additional information
- [ ] I am using Expo
- [ ] I have enabled Frame Processors (react-native-worklets-core)
- [x] I have read the Troubleshooting Guide
- [x] I agree to follow this project's Code of Conduct
- [x] I searched for similar issues in this repository and found none.
Guten Tag, Hans here. 🥨 It looks like you are experiencing a problem with ze zoom on Android. However, I notice that you haven’t provided any log output which we would need to diagnose ze issue better.
Please, can you gather log output using adb logcat while reproducing ze issue? This will help mrousavy find ze root cause.
Also, it seems you did not try reproducing ze issue in ze VisionCamera Example app. Please do that as well; if ze issue is not present in ze example, it might be specific to your setup.
Don’t forget to check documentation for ze zoom settings too! Thank you!
Note: If you think I made a mistake, please ping
@mrousavyto take a look.
UPDATED
After some more debugging i have found that
const devices = Camera.getAvailableCameraDevices(); const backCameras = devices.filter((d) => d.position === 'back');
my device returns ultra wide camera but the min zoom and neutral both are 1 which are incorrect
[ { "formats":[], "hardwareLevel":"full", "hasFlash":true, "hasTorch":true, "id":"0", "isMultiCam":false, "maxExposure":20, "maxZoom":8, "minExposure":-20, "minFocusDistance":10, "minZoom":1, "name":"0 (BACK) androidx.camera.camera2", "neutralZoom":1, "physicalDevices":[ "wide-angle-camera" ], "position":"back", "sensorOrientation":"landscape-left", "supportsFocus":true, "supportsLowLightBoost":false, "supportsRawCapture":false }, { "formats":[], "hardwareLevel":"limited", "hasFlash":false, "hasTorch":false, "id":"2", "isMultiCam":false, "maxExposure":20, "maxZoom":8, "minExposure":-20, "minFocusDistance":0, "minZoom":1, "name":"2 (BACK) androidx.camera.camera2", "neutralZoom":1, "physicalDevices":[ "ultra-wide-angle-camera" ], "position":"back", "sensorOrientation":"landscape-left", "supportsFocus":true, "supportsLowLightBoost":false, "supportsRawCapture":false } ]
const device = getCameraDevice(devices2, 'back', { physicalDevices: [ 'ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera', ], });
and when i use devices with this it only returns the one with wide angle camera and not the ultra wide angle camera. I am not sure what i am missing.
{ "formats":[], "hardwareLevel":"full", "hasFlash":true, "hasTorch":true, "id":"0", "isMultiCam":false, "maxExposure":20, "maxZoom":8, "minExposure":-20, "minFocusDistance":10, "minZoom":1, "name":"0 (BACK) androidx.camera.camera2", "neutralZoom":1, "physicalDevices":[ "wide-angle-camera" ], "position":"back", "sensorOrientation":"landscape-left", "supportsFocus":true, "supportsLowLightBoost":false, "supportsRawCapture":false }
UPDATED
After some more debugging i have found that
const devices = Camera.getAvailableCameraDevices(); const backCameras = devices.filter((d) => d.position === 'back');
my device returns ultra wide camera but the min zoom and neutral both are 1 which are incorrect
[ { "formats":[], "hardwareLevel":"full", "hasFlash":true, "hasTorch":true, "id":"0", "isMultiCam":false, "maxExposure":20, "maxZoom":8, "minExposure":-20, "minFocusDistance":10, "minZoom":1, "name":"0 (BACK) androidx.camera.camera2", "neutralZoom":1, "physicalDevices":[ "wide-angle-camera" ], "position":"back", "sensorOrientation":"landscape-left", "supportsFocus":true, "supportsLowLightBoost":false, "supportsRawCapture":false }, { "formats":[], "hardwareLevel":"limited", "hasFlash":false, "hasTorch":false, "id":"2", "isMultiCam":false, "maxExposure":20, "maxZoom":8, "minExposure":-20, "minFocusDistance":0, "minZoom":1, "name":"2 (BACK) androidx.camera.camera2", "neutralZoom":1, "physicalDevices":[ "ultra-wide-angle-camera" ], "position":"back", "sensorOrientation":"landscape-left", "supportsFocus":true, "supportsLowLightBoost":false, "supportsRawCapture":false } ]
const device = getCameraDevice(devices2, 'back', { physicalDevices: [ 'ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera', ], });
and when i use devices with this it only returns the one with wide angle camera and not the ultra wide angle camera. I am not sure what i am missing.
{ "formats":[], "hardwareLevel":"full", "hasFlash":true, "hasTorch":true, "id":"0", "isMultiCam":false, "maxExposure":20, "maxZoom":8, "minExposure":-20, "minFocusDistance":10, "minZoom":1, "name":"0 (BACK) androidx.camera.camera2", "neutralZoom":1, "physicalDevices":[ "wide-angle-camera" ], "position":"back", "sensorOrientation":"landscape-left", "supportsFocus":true, "supportsLowLightBoost":false, "supportsRawCapture":false }
I was reading old issues and Discord, if I understood correctly, it could be that Google is enabling the "ultra-wide-angle-camera" and "telephoto-camera" only for the native camera, making it impossible to access them. https://github.com/mrousavy/react-native-vision-camera/issues/1511#issuecomment-1759419457
The same issue on Iphone 11 Promax, even with demo ShadowLens app. It can't zoom to 0.5x But if I using another camera photo app like Soda it can zoom to 0.5x.
I recently developed an application with React Native Vision Camera and I'm having the same problem on iOS 18. I've been testing on an iPhone 16 Pro Max, and although it has a 0.5x zoom, it always reaches 1x. This problem is repeated in the example application and even in its ShadowLens application.
WORKAROUND
import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Image, Platform, SafeAreaView, StatusBar, StyleSheet, TouchableOpacity, View, } from 'react-native'; import { useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import * as native from '@react-navigation/native'; import { useIsFocused } from '@react-navigation/native'; import ImageResizer from '@bam.tech/react-native-image-resizer'; import { useDeviceOrientationChange } from 'react-native-orientation-locker'; import { Camera, CameraDevice, getCameraDevice, useCameraDevice, } from 'react-native-vision-camera'; import { Icon, IconActionButton, SnackbarManager, Text, useTheme, } from '@components'; import { Metrix, commonStyles } from '@theme'; import { NavigationService } from '@services'; import { CONSTANTS, SnackBarTypeEnum, TRANSLATION_KEYS, isAndroid, isIOS, } from '@constants'; import { useDarkTheme, useIsForegroundHook } from '@hooks'; import { appLogger, appendActivityIdToGenericErrorToast, logAttributesAndError, requestCameraPermission, } from '@utils'; import { ICameraError, IUserState, CameraPermissionStatusTypes } from '@types'; import type { CameraPermissionStatus } from 'react-native-vision-camera';
interface ICameraCapture { uri: string; width: number; height: number; photoOrientation: string; originalUri?: string; }
const CameraScreen = () => { const { params } = native.useRoute< native.RouteProp< { params: { launchCamera: any; imageQuality?: { width: number; height: number; quality: number }; }; }, 'params' >
(); const cameraRef = useRef<Camera | null>(null); const { colors }: { colors: any } = useTheme(); const { t } = useTranslation(); const isFocused = useIsFocused(); const isForeground = useIsForegroundHook(); const isDarkTheme = useDarkTheme(); const commonStyling = useMemo( () => commonStyles(colors, colors.primary), [colors] ); const styles = useMemo(() => style(colors), [colors]); const { activityId }: IUserState = useSelector((state) => state.User); const isActive = isFocused && isForeground;
const allPhysicalCameras = Camera.getAvailableCameraDevices(); // ?? getting all the available camera hardware devices
const allBackCameraPhysicalDevices = allPhysicalCameras?.filter( (d) => d.position === 'back' ); // ?? filtering only available back devices
let allSupportedDevice: CameraDevice | undefined = useCameraDevice('back', { physicalDevices: [ 'wide-angle-camera', 'ultra-wide-angle-camera', 'telephoto-camera', ], });
const ifAllSupportedDeviceAlreadyConsiderWideAngleCamera = allSupportedDevice?.physicalDevices.includes('ultra-wide-angle-camera');
let isUltraWideAnglePhysicalCameraDeviceAvailable: CameraDevice | undefined = undefined; let onlyUltraWideAngleCameraDevice: CameraDevice | undefined = undefined; // ?? in IOS and normal devices on android in which its supported it will work automatically
if (isAndroid && !ifAllSupportedDeviceAlreadyConsiderWideAngleCamera) { // ?? if allSupportedDevice in android already support the ultra wide angle camera then it won't go into this // ?? and the reason is that can't create 2 camera context when its already done by default // ?? finding if out of those back camera device is there any that supports ultra wide angle camera // ?? this is for android only cause ios handle this automatically isUltraWideAnglePhysicalCameraDeviceAvailable = allBackCameraPhysicalDevices?.find((item) => item?.physicalDevices.includes('ultra-wide-angle-camera') ); }
if (isUltraWideAnglePhysicalCameraDeviceAvailable && isAndroid) { // ?? this is only for android platforms where i have to check for devices having ultra wide camera // ?? in some cases ultra eide angle camera was present but hardware was limited causing it to be filtered out by getCameraDevice // ?? this function on base of ranking points will return the best hardware type // ?? so if i had 2 cameras wide and ultra wide but ultra wide = "limited" and wide = "full" the ranking system was always returning wide casuing 0.5x not work // ?? what i have done now is that i have bypassed ranking // ?? for 0.5x a separate camera context will be made available
onlyUltraWideAngleCameraDevice = getCameraDevice(
[isUltraWideAnglePhysicalCameraDeviceAvailable],
'back'
);
}
const { minZoom, maxZoom, neutralZoom } = allSupportedDevice || {}; const ultraWideZoom = isUltraWideAnglePhysicalCameraDeviceAvailable && isAndroid ? isUltraWideAnglePhysicalCameraDeviceAvailable?.minZoom : minZoom;
// ?? min zoom is 1 always if its a fish eye it will work as 0.5x for android and 1x for ios otherwise it will work as 1x // ?? so in the devices which has already minzoom and already support wide angle zoom it will take its value // ?? but in devices which has physical devices in backcamera but library wasn't supporting it then it will use isUltraWideAnglePhysicalCameraDeviceAvailable minZoom
const defaultZoom = neutralZoom; // ?? defaults to 1x this zoom is the one which is 1 in devices not having fish eye and 2x in devices having fish eye for ios
const twoXZoomFactor = isIOS ? 0.125 : 0.198; const twoXZoom = Math.min(neutralZoom! * 10, maxZoom!) * twoXZoomFactor;
const [cameraPermissionStatus, setCameraPermissionStatus] = useState<CameraPermissionStatus>('not-determined'); const [flash, setFlash] = useState<'off' | 'on' | 'auto'>('off'); const [isFlashAvailable, setIsFlashAvailable] = useState(true); const [isPictureTaken, setIsPictureTaken] = useState(false); const [photo, setPhoto] = useState<ICameraCapture | undefined>(undefined); const [zoom, setZoom] = useState(defaultZoom); const [isUltraWideZoomSelected, setIsUltraWideZoomSelected] = useState(false); const [currentDeviceOrientation, setCurrentDeviceOrientation] = useState( CONSTANTS.deviceOrientation.portrait );
const zoomLevels = Platform.select({ ios: [ ...(ifAllSupportedDeviceAlreadyConsiderWideAngleCamera //?? in IOS as well if it supports wide angle device then show 0.5x iphoneXr doesn't have wide angle camera ? [{ zoomValue: ultraWideZoom, label: 0.5 }] : []), { zoomValue: defaultZoom, label: 1 }, { zoomValue: twoXZoom, label: 2 }, ], android: [ ...(isUltraWideAnglePhysicalCameraDeviceAvailable || ifAllSupportedDeviceAlreadyConsiderWideAngleCamera ? [ { zoomValue: ifAllSupportedDeviceAlreadyConsiderWideAngleCamera ? ultraWideZoom : 0.5, label: ifAllSupportedDeviceAlreadyConsiderWideAngleCamera ? ultraWideZoom?.toFixed(1) : 0.5, }, ] : []), //?? all the supported devices that have min zoom available or the devices in which i fetched it { zoomValue: defaultZoom, label: 1 }, { zoomValue: twoXZoom, label: 2 }, ], });
useDeviceOrientationChange((o) => { let orientation: string = o; if ( [ CONSTANTS.deviceOrientation.unKnown, CONSTANTS.deviceOrientation.faceup, ].includes(orientation) ) { // ?? in case of unknown we will make it default portrait orientation = CONSTANTS.deviceOrientation.portrait; }
setCurrentDeviceOrientation(orientation);
});
useEffect(() => { getCameraPermissionStatus(); }, [isFocused]);
const getCameraPermissionStatus = async () => { try { const visionCameraPermissionStatus: CameraPermissionStatusTypes = await Camera.getCameraPermissionStatus(); setCameraPermissionStatus(visionCameraPermissionStatus);
if (visionCameraPermissionStatus !== 'granted') {
const permission = await Camera.requestCameraPermission();
setCameraPermissionStatus(permission);
}
} catch (error) {
appLogger('Permission ');
}
};
const handleZoomChange = (zoomValue: number | string) => { if ( isAndroid && isUltraWideAnglePhysicalCameraDeviceAvailable && Number(zoomValue) < 1 ) { // ?? if ultra wide zoom level is selected and there is an actual device that supports ultra wide angle camera // ?? and is android we will see if it has flash available or not // ?? if not then we will disable flash icon and set it as off setIsUltraWideZoomSelected(true); if (onlyUltraWideAngleCameraDevice) { setIsFlashAvailable(onlyUltraWideAngleCameraDevice?.hasFlash);
if (!onlyUltraWideAngleCameraDevice?.hasFlash) setFlash('off');
//?? onlyUltraWideAngleCameraDevice doesn't support flash then it will close the flash
}
} else if (!ifAllSupportedDeviceAlreadyConsiderWideAngleCamera) {
//?? otherwise in all cases we will always update flash icon even for ios
setIsUltraWideZoomSelected(false);
setIsFlashAvailable(allSupportedDevice?.hasFlash!);
}
setZoom(zoomValue);
};
const toggleFlashMode = () => { if (!isFlashAvailable) { return SnackbarManager.show( t(TRANSLATION_KEYS.FLASH_NOT_AVAILABLE), SnackBarTypeEnum.Warning, 2000 ); }
setFlash((prevMode) => {
if (prevMode === 'auto') {
return 'on';
} else if (prevMode === 'on') {
return 'off';
} else {
return 'auto';
}
});
};
const getFlashIcon = () => { switch (flash) { case 'auto': return 'flash-auto'; case 'on': return 'flash'; case 'off': return 'flash-off'; default: return 'flash-auto'; } };
const onCapturePicture = async () => {
if (cameraRef.current) {
const data = await cameraRef.current.takePhoto({ flash });
const picture = file://${data?.path};
// appLogger('DATA', JSON.stringify(data, null, 2));
const newWidth =
params && params?.imageQuality ? params?.imageQuality?.width : 700;
const newHeight =
params && params?.imageQuality ? params?.imageQuality?.height : 600;
const quality =
params && params?.imageQuality ? params?.imageQuality?.quality : 90;
let rotationAngle = 0;
// appLogger('currentDeviceOrientation', currentDeviceOrientation);
switch (currentDeviceOrientation) {
case CONSTANTS.deviceOrientation.landScapeLeft:
rotationAngle = 270;
break;
case CONSTANTS.deviceOrientation.portraitUpsideDown:
rotationAngle = 180;
break;
case CONSTANTS.deviceOrientation.landScapeRight:
rotationAngle = 90;
break;
default:
rotationAngle = 0;
}
// ?? rotating picture based on angle of when picture was taken
const [displayImage, resizedImage] = await Promise.all([
ImageResizer.createResizedImage(
picture,
1000,
1000,
'JPEG',
100,
rotationAngle
),
ImageResizer.createResizedImage(
// ?? returns size in bytes convert it to kbs making it in 5kb
picture,
newWidth,
newHeight,
'JPEG',
quality,
rotationAngle
),
]);
// ?? displayImage is just to display user and to rotate it to show in always portrait
// ?? resizedImage is the rotation + resized image that will be sent to S3
setIsPictureTaken(true);
appLogger('displayImage', JSON.stringify(displayImage, null, 2));
appLogger('resizedImage', JSON.stringify(resizedImage, null, 2));
appLogger('Image Size', resizedImage?.size / 1024, 'Kb');
const pictureData = {
uri: resizedImage?.uri,
width: resizedImage?.width,
height: resizedImage?.height,
photoOrientation: currentDeviceOrientation,
originalUri: displayImage?.uri,
};
setPhoto(pictureData);
}
};
const onRetryPicture = () => { setPhoto(undefined); setIsPictureTaken(false); setZoom(defaultZoom); };
const onPictureDone = async () => { appLogger('onPictureDone'); if (params && params?.launchCamera) { params?.launchCamera(photo); } // setIsPictureTaken(false); setPhoto(undefined); NavigationService.goBack(); };
const onCancel = async () => { NavigationService.goBack(); };
const showCameraInUseAlert = () => { const onOkPress = () => {}; Alert.alert( 'Camera Access Error', `Maximum number of cameras are currently in use.
Please close any apps that might be using the camera, such as:
- Camera, WhatsApp, Instagram, Snapchat
Steps to resolve:
- Close all apps that may be using the camera.
- Or Reload the camera by going back`, [ { text: 'Got it', onPress: onOkPress, style: 'default', }, ], { cancelable: false } ); };
const onCameraError = (error: ICameraError) => { const { code, message, cause, details, domain, stacktrace } = error;
if (String(code) === CONSTANTS.CAMERA_ERRORS.permissionDenied) {
SnackbarManager.show(
appendActivityIdToGenericErrorToast(
`${t(TRANSLATION_KEYS.CAMERA_PERMISSION_DENIED_TITLE)} ${t(
TRANSLATION_KEYS.CAMERA_PERMISSION_DENIED_MESSAGE
)}`
),
SnackBarTypeEnum.Danger,
5000
);
requestCameraPermission();
} else if (String(code) === CONSTANTS.CAMERA_ERRORS.maxCamerasOpen) {
showCameraInUseAlert();
}
};
const _renderCameraView = () => { const cameraDeviceContext = isAndroid && isUltraWideZoomSelected && isUltraWideAnglePhysicalCameraDeviceAvailable ? onlyUltraWideAngleCameraDevice : allSupportedDevice; // ?? This here camera context is being switched // ?? For android if the device have a wide angle camera but due to ranking system it was being filtered // ?? so if that device is found then whenever user presses 0.5x zoom it will switch to that onlyUltraWideAngleCameraDevice camera context // ?? and if user doesn't select that zoom level or for devices that be default support wide angle camera then it will use allSupportedDevice camera context
if (!cameraDeviceContext) {
return (
<View style={styles.noCameraView}>
<Text
value={t(TRANSLATION_KEYS.CAMERA_NOT_AVAILABLE)}
type={'Regular_400'}
variant={'h3'}
align="center"
color={'white'}
/>
<IconActionButton
backgroundColor={colors.primary}
gutterTop={20}
width={'70%'}
onPress={() => NavigationService.goBack()}
labelVariant={'h3'}
borderColor={colors.primary}
buttonLabel={t(TRANSLATION_KEYS.GO_BACK)}
labelColor={colors.background}
/>
</View>
);
}
return (
<View>
<View
style={[commonStyling.rowView, styles.topFlashAndCloseActionLayout]}
>
<TouchableOpacity
onPress={onCancel}
style={styles.closeCameraIcon}
activeOpacity={0.7}
>
<Icon
tagName={'MaterialCommunityIcons'}
iconName={'close'}
iconSize={Metrix.ModerateScale(25)}
iconColor={colors.constantWhite}
/>
</TouchableOpacity>
<TouchableOpacity
style={{
...styles.flashIcon,
backgroundColor:
flash === 'off' || !isFlashAvailable
? 'black'
: colors.yellowScan,
}}
activeOpacity={0.7}
onPress={toggleFlashMode}
hitSlop={{ top: 10, left: 10, right: 10, bottom: 10 }}
>
<Icon
tagName={'MaterialCommunityIcons'}
iconName={getFlashIcon()}
iconSize={Metrix.ModerateScale(22)}
iconColor={
flash === 'off' || !isFlashAvailable
? colors.emptyListTest
: colors.constantBlack
}
/>
</TouchableOpacity>
</View>
<View>
<Camera
ref={cameraRef}
style={styles.camera}
device={cameraDeviceContext!}
resizeMode="cover"
isActive={isActive}
orientation="portrait"
photo={true}
zoom={zoom}
enableZoomGesture
flash={flash!}
onStarted={() => appLogger('Camera started!')}
onStopped={() => appLogger('Camera stopped!')}
onError={(error) => {
onCameraError(error);
}}
onPreviewStarted={() => appLogger('Preview started!')}
onPreviewStopped={() => appLogger('Preview stopped!')}
/>
</View>
<View style={styles.bottomCameraCaptureLayout}>
<View style={styles.zoomOptionsView}>
{zoomLevels?.map((item) => (
<TouchableOpacity
onPress={() => handleZoomChange(item?.zoomValue)}
style={styles.zoomOption}
>
<Text
variant={'h3'}
type={'Medium_500'}
value={`${item?.label}x`}
color={colors.constantWhite}
/>
</TouchableOpacity>
))}
</View>
<View style={styles.captureIconView}>
<TouchableOpacity
onPress={onCapturePicture}
style={styles.captureIcon}
activeOpacity={0.7}
>
<View style={styles.captureInnerCircle} />
</TouchableOpacity>
</View>
</View>
</View>
);
};
const _renderPictureTakenView = () => { return ( <View style={styles.previewContainer}> <Image source={{ uri: photo?.originalUri }} style={styles.preview} resizeMode={'contain'} />
<View style={[styles.imageTakenLayout, commonStyling.rowView]}>
<IconActionButton
backgroundColor={colors.screenWrapperBackground}
borderRadius={20}
buttonLabel={t(TRANSLATION_KEYS.RETAKE)}
width={'45%'}
labelVariant="h2"
labelColor={colors.primary}
onPress={onRetryPicture}
borderColor={colors.lightGrey}
/>
<IconActionButton
backgroundColor={colors.primary}
width={'45%'}
onPress={onPictureDone}
borderRadius={20}
labelVariant={'h3'}
borderColor={colors.primary}
buttonLabel={t(TRANSLATION_KEYS.CONFIRM)}
labelColor={colors.background}
/>
</View>
</View>
);
};
return ( <SafeAreaView style={styles.safeAreaView}> <StatusBar backgroundColor={colors.screenWrapperBackground} barStyle={isDarkTheme ? 'light-content' : 'dark-content'} />
<View>
{isPictureTaken ? _renderPictureTakenView() : _renderCameraView()}
</View>
</SafeAreaView>
); };
const style = (colors: any) => StyleSheet.create({ safeAreaView: { flex: 1, paddingTop: Metrix.ModerateScale(12), backgroundColor: colors.constantBlack, }, topFlashAndCloseActionLayout: { width: '100%', paddingVertical: Metrix.ModerateScale(20), paddingHorizontal: Metrix.ModerateScale(20), height: Metrix.DimensionalHeight(0.1), }, closeCameraIcon: { backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: Metrix.ModerateScale(20), height: Metrix.ModerateScale(35), width: Metrix.ModerateScale(35), justifyContent: 'center', alignItems: 'center', }, flashIcon: { backgroundColor: colors.screenWrapperBackground, borderRadius: Metrix.ModerateScale(35), height: Metrix.ModerateScale(35), width: Metrix.ModerateScale(35), justifyContent: 'center', alignItems: 'center', }, camera: { height: Metrix.DimensionalHeight(0.58), width: Metrix.DimensionalWidth(1), overflow: 'hidden', }, noCameraView: { height: Metrix.DimensionalHeight(1), justifyContent: 'center', alignItems: 'center', paddingHorizontal: Metrix.ModerateScale(20), }, previewContainer: { justifyContent: 'center', alignItems: 'center', height: '100%', }, imageTakenLayout: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingVertical: Metrix.ModerateScale(12), backgroundColor: colors.constantBlack, paddingHorizontal: Metrix.ModerateScale(30), }, preview: { width: '100%', height: Metrix.DimensionalHeight(0.8), }, bottomCameraCaptureLayout: { paddingTop: Metrix.ModerateScale(20), backgroundColor: colors.constantBlack, height: Metrix.DimensionalHeight(0.22), }, zoomOptionsView: { alignSelf: 'center', display: 'flex', flexDirection: 'row', columnGap: Metrix.ModerateScale(10), marginBottom: Metrix.ModerateScale(20), }, zoomOption: { backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: Metrix.ModerateScale(30), width: Metrix.ModerateScale(35), height: Metrix.ModerateScale(35), justifyContent: 'center', alignItems: 'center', }, captureIconView: { justifyContent: 'center', alignItems: 'center', padding: Metrix.VerticalSize(10), }, captureIcon: { width: Metrix.HorizontalSize(80), height: Metrix.HorizontalSize(80), borderRadius: Metrix.HorizontalSize(40), borderWidth: Metrix.ModerateScale(4), borderColor: '#FFFFFF', justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.1)', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 4.65, elevation: 8, }, captureInnerCircle: { width: Metrix.HorizontalSize(64), height: Metrix.HorizontalSize(64), borderRadius: Metrix.HorizontalSize(32), backgroundColor: '#FFFFFF', // inner button color }, });
export default CameraScreen;
VERSION
"react-native-vision-camera": "4.0.5", "react-native": "0.72.6",
REASONING
i was able to resolve this using the workaround mentioned above My samsung A52 wasn't reading 0.5x value. turns out there is a rating mechanism in the code of the package that checks if your lens have lower rating it will not show that one way to verify this is to open snapchat or insta and see if they support it if they does support it then you will have that other wise it won't come up
On devices, the react-native-vision-camera library uses an internal ranking system to determine the best available camera device. If a camera hardware is marked as "limited", it often gets excluded from getCameraDevice results. This is problematic for ultra-wide-angle cameras (0.5x zoom) on some devices because: Even if the hardware exists, it’s filtered out due to the ranking system. The hardware can be limited due to many reasons one that i found out during development was that ultra wide angle camera didn't support flash there could be many reasons Result: Users are unable to access the ultra-wide lens in camera apps using the default configuration.
Hi @Maazy3, just wanted to say a huge thanks 🙏 for sharing this solution! I ran into the same issue where some Android devices didn’t show the wide-angle camera in react-native-vision-camera, and your approach worked perfectly. I’ve also optimized your code into a lighter version for anyone who wants a more minimal implementation — keeping your original logic but trimming unnecessary overhead. Really appreciate your contribution, it saved me a lot of time! 🚀
import { useIsFocused } from '@react-navigation/native'
import { useRef, useState } from 'react'
import { Platform, SafeAreaView, StyleSheet, Text, TouchableOpacity, View, Dimensions } from 'react-native'
import { Camera, CameraDevice, getCameraDevice, useCameraDevice } from 'react-native-vision-camera'
const isAndroid = Platform.OS === 'android'
const isIOS = Platform.OS === 'ios'
const SCREEN_W = Dimensions.get('window').width
const CameraScreen: React.FC<any> = () => {
const cameraRef = useRef<Camera | null>(null)
const isFocused = useIsFocused()
const isForeground = true
const isActive = isFocused && isForeground
const physicalCameras = Camera.getAvailableCameraDevices()
const backCameras = physicalCameras?.filter((d) => d.position === 'back')
let supportedDevice: CameraDevice | undefined = useCameraDevice('back', {
physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera', 'telephoto-camera'],
})
const hasWideAngleSupport = supportedDevice?.physicalDevices.includes('ultra-wide-angle-camera')
let ultraWideCamera: CameraDevice | undefined
let ultraWideDevice: CameraDevice | undefined
if (isAndroid && !hasWideAngleSupport) {
ultraWideCamera = backCameras?.find((item) => item?.physicalDevices.includes('ultra-wide-angle-camera'))
}
if (ultraWideCamera && isAndroid) {
ultraWideDevice = getCameraDevice([ultraWideCamera], 'back')
}
const { minZoom, maxZoom, neutralZoom } = supportedDevice || {}
const ultraWideZoom = ultraWideCamera && isAndroid ? ultraWideCamera?.minZoom : minZoom
const defaultZoom = neutralZoom
const zoom2xFactor = isIOS ? 0.125 : 0.198
const zoom2x = Math.min(neutralZoom! * 10, maxZoom!) * zoom2xFactor
const [zoom, setZoom] = useState(defaultZoom)
const [isUltraWideSelected, setIsUltraWideSelected] = useState(false)
const zoomLevels = Platform.select({
ios: [
...(hasWideAngleSupport ? [{ zoomValue: ultraWideZoom, label: 0.5 }] : []),
{ zoomValue: defaultZoom, label: 1 },
{ zoomValue: zoom2x, label: 2 },
],
android: [
...(ultraWideCamera || hasWideAngleSupport
? [
{
zoomValue: hasWideAngleSupport ? ultraWideZoom : 0.5,
label: hasWideAngleSupport ? ultraWideZoom?.toFixed(1) : 0.5,
},
]
: []),
{ zoomValue: defaultZoom, label: 1 },
{ zoomValue: zoom2x, label: 2 },
// { zoomValue: zoom2x < 2 ? 3 : 2, label: zoom2x < 2 ? 3 : 2 },
],
})
const handleZoomChange = (zoomValue: number | string) => {
if (isAndroid && ultraWideCamera && Number(zoomValue) < 1) {
setIsUltraWideSelected(true)
} else if (!hasWideAngleSupport) {
setIsUltraWideSelected(false)
}
setZoom(Number(zoomValue) || defaultZoom)
}
const renderCameraView = () => {
const cameraDevice = isAndroid && isUltraWideSelected && ultraWideCamera ? ultraWideDevice : supportedDevice
if (!cameraDevice) {
return (
<View style={styles.noCameraView}>
<Text>Camera not support</Text>
</View>
)
}
return (
<View>
<View>
<Camera
zoom={zoom}
photo={true}
ref={cameraRef}
enableZoomGesture
resizeMode="cover"
isActive={isActive}
style={styles.camera}
device={cameraDevice!}
/>
</View>
<View style={styles.zoomOptionsView}>
{zoomLevels?.map((item: any) => (
<TouchableOpacity onPress={() => handleZoomChange(item?.zoomValue)} style={styles.zoomOption}>
<Text>{item?.label}x</Text>
</TouchableOpacity>
))}
</View>
</View>
)
}
return (
<SafeAreaView style={styles.safeAreaView}>
<View>{renderCameraView()}</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
safeAreaView: {
flex: 1,
width: '100%',
height: '100%',
paddingTop: 20,
backgroundColor: '#000',
},
camera: {
width: SCREEN_W,
height: SCREEN_W * (4 / 3),
overflow: 'hidden',
},
noCameraView: {
height: 20,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20,
},
zoomOptionsView: {
alignSelf: 'center',
display: 'flex',
flexDirection: 'row',
columnGap: 20,
marginBottom: 20,
},
zoomOption: {
backgroundColor: '#fff',
borderRadius: 20,
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 12,
},
})
export default CameraScreen
"react-native-vision-camera": "4.7.2", "react-native": "0.78.3",
@lehoi2195 Thankyou, happy to help the community