π Camera Rendering Issue in Portal/View on Android
What's happening?
Hello @all,
I am experiencing an issue when attempting to render the camera inside a Portal/ View component. The expected behavior and the actual behavior are inconsistent on Android devices. On iOS, it works stable and exactly as expected.
Expected Behavior: The camera should render consistently inside the container, below the Appbar.Header.
Expected View:
Actual Behavior: In most cases, the camera does not render as expected, resulting in inconsistent and incorrect positioning.
Actual View:
Additional Information: This issue occurs on real devices as well as in the simulator. If I replace the Camera component with a View using the same styling, it works consistently as expected. This leads me to believe the issue might be with the React-Native-Vision-Camera library. Please let me know if you need any additional details or steps to reproduce the issue.
Best regards, Andreas
Reproduceable Code
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Linking, StyleSheet, View } from 'react-native';
import { Props } from './ScannerContainer';
import { ActivityIndicator, Appbar, Button, Portal, Text } from 'react-native-paper';
import BarcodeMask from 'react-native-barcode-mask';
import { CameraPermissionStatus, Code } from 'react-native-vision-camera';
import { useCameraDevice, useCodeScanner } from 'react-native-vision-camera';
import { Camera } from 'react-native-vision-camera';
export const Scanner: React.FC<Props> = ({
isScannerOpen,
navigateToUrl,
setNavigationStack,
closeBarCodeScanner,
baseUrl,
specialLinks,
}) => {
const [isLoading, setIsLoading] = useState(false);
const [cameraPermission, setCameraPermission] = useState<CameraPermissionStatus>('not-determined');
const device = useCameraDevice('back', {
physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'],
});
useEffect(() => {
const requestPermissions = async () => {
try {
const permission = await Camera.requestCameraPermission();
if (permission !== 'granted') {
console.error('Not allowed to access camera');
}
setCameraPermission(permission);
} catch (err) {
console.error('Error requesting camera permission:', err);
}
};
const promise = requestPermissions();
promise.catch((err) => console.error('Unhandled error:', err));
}, []);
const onCodeScanned = useCallback((codes: Code[]) => {
if (!codes?.length) {
console.error('No codes scanned.');
return;
}
const productNumber = codes[0]?.value;
if (!productNumber) {
console.error('Scanned code does not contain a product number.');
return;
}
const fallbackUrl = specialLinks.searchProductsFallback(productNumber!);
setIsLoading(true);
fetch(specialLinks.searchProducts, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
Input: {
q: productNumber,
},
SearchType: 'Products',
}),
})
.then((response) => response.json())
.then((response) => {
const urlActionProductDetail = response?.products?.[0]?.urlActionProductDetail;
if (urlActionProductDetail) {
setNavigationStack([fallbackUrl, specialLinks.product(urlActionProductDetail)]);
} else {
navigateToUrl({ url: fallbackUrl, baseUrl });
}
})
.catch((error) => {
console.error('Failed to search product with scanned number', error);
navigateToUrl({ url: fallbackUrl, baseUrl });
})
.finally(() => setIsLoading(false));
}, []);
const codeScanner = useCodeScanner({
codeTypes: ['qr', 'ean-13', 'ean-8', "code-128", "code-93", "code-39"],
onCodeScanned: onCodeScanned,
});
console.log(JSON.stringify(device, (k, v) => k === "formats" ? [] : v, 2))
return isScannerOpen ? (
<Portal>
<View style={styles.container}>
<Appbar.Header>
<Appbar.Action icon="close" onPress={closeBarCodeScanner} />
<Appbar.Content title={"Barcode scannen"} />
</Appbar.Header>
{cameraPermission === null || device == undefined ? (
<View style={[styles.fill, styles.black]} />
) : !cameraPermission ? (
<View style={[styles.fill, styles.noAccess]}>
<Text>{"No access"}</Text>
<Button onPress={() => Linking.openSettings().catch(() => console.warn('cannot open settings'))}>
<Text>{"Open Settings"}</Text>
</Button>
</View>
) : isLoading ? (
<View style={[styles.fill, styles.loadingContainer]}>
<ActivityIndicator size="large" />
</View>
) : (
<View style={styles.fill}>
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={true}
codeScanner={codeScanner}
focusable={true}
>
</Camera>
<BarcodeMask height={130} showAnimatedLine={false} />
</View>
)}
</View>
</Portal>
) : null;
};
const styles = StyleSheet.create({
fill: {
flex: 1,
},
container: {
flex: 1,
flexDirection: 'column',
},
black: {
backgroundColor: 'black',
},
loadingContainer: {
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
},
noAccess: {
backgroundColor: 'white',
padding: 20,
},
});
Relevant log output
01-27 07:37:47.991 381 467 D CompositionEngine: Layer: ch.documed.xxxx/com.xxxx.MainActivity#993 had an out of bounds transparent region
01-27 07:37:47.991 381 467 D Region : Region transparentRegionHint (this=0xb400007b57878d90, count=2)
01-27 07:37:47.991 381 467 D Region : [-258, 210, -10, 240]
01-27 07:37:47.991 381 467 D Region : [-258, 240, 0, 1170]
01-27 07:37:48.092 381 467 D CompositionEngine: Layer: ch.documed.xxxx/com.xxxx.MainActivity#993 had an out of bounds transparent region
01-27 07:37:48.092 381 467 D Region : Region transparentRegionHint (this=0xb400007b57878d90, count=2)
01-27 07:37:48.092 381 467 D Region : [-258, 210, -10, 240]
01-27 07:37:48.092 381 467 D Region : [-258, 240, 0, 1170]
01-27 07:37:48.193 381 467 D CompositionEngine: Layer: ch.documed.xxxx/com.xxxx.MainActivity#993 had an out of bounds transparent region
01-27 07:37:48.193 381 467 D Region : Region transparentRegionHint (this=0xb400007b57878d90, count=2)
01-27 07:37:48.193 381 467 D Region : [-258, 210, -10, 240]
01-27 07:37:48.193 381 467 D Region : [-258, 240, 0, 1170]
Camera Device
{
"id": "10",
"position": "back",
"hasTorch": false,
"hasFlash": false,
"name": "10 (BACK) androidx.camera.camera2",
"minExposure": -6,
"minFocusDistance": 999.999985098839,
"isMultiCam": false,
"supportsRawCapture": false,
"supportsFocus": true,
"physicalDevices": [
"ultra-wide-angle-camera"
],
"neutralZoom": 1,
"supportsLowLightBoost": false,
"maxExposure": 6,
"minZoom": 1,
"maxZoom": 1,
"hardwareLevel": "limited",
"sensorOrientation": "landscape-left",
"formats": []
}
Device
e.g. Samsung Galaxy A20e (real device), Pixel 7 (real device), any simulated device
VisionCamera Version
4.6.3
Can you reproduce this issue in the VisionCamera Example app?
Yes, I can reproduce the same issue in the Example app here
Additional information
- [x] 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! π»
Thank you for your detailed report, Andreas. It appears youβre experiencing a rendering issue with ze camera in a Portal/View component on Android. You provided good information and logs which will be helpful for mrousavy to investigate this.
Could you please check if the issue persists in a standalone example app that uses the Vision Camera without Expo? It would help to narrow down ze problem. Additionally, sometimes issues like this can be related to specific device configurations. If you could share ze specific device model and OS version where you are facing this, it would also aid in troubleshooting.
Letβs keep zis issue open for now!
Note: If you think I made a mistake, please ping
@mrousavyto take a look.
I have them same problem
I have them same problem
Thanks for your response! It does seem like a real bug in the lib. Have you worked with older versions of it before, or did you just start using it and encounter the same problem?
@mrousavy Would be great to get some feedback as we're somehow blocked with our next release. Thanks alot.
Is there any update on this issue? I'm experiencing the same problem.
@mrousavy Would be great to get some feedback as we're somehow blocked with our next release. Thanks alot.
If you're blocked on releases feel free to contact my agency - we offer consultancy services to unblock you asap.
Other than that; no idea. I haven't worked with Portals yet. Try setting the preview surface mode - see Preview: Overlays and Masks
Thanks for your response. Setting the androidPreviewViewType to "surface-view" didn't solve the problem.
It's not only limited to Portals, it also happens exactly the same if I use e.g. a Modal from react-native. I figured out, that it works much better if I use the onLayout() like this (but i'm still able to mess the layout up):
import React, { useCallback, useState } from 'react';
import { Linking, Modal, StyleSheet, View, ViewStyle } from 'react-native';
import { useTranslation } from 'react-i18next';
import { Props } from './ScannerContainer';
import { ActivityIndicator, Appbar, Button, Text } from 'react-native-paper';
import BarcodeMask from 'react-native-barcode-mask';
import { Camera, getCameraDevice, useCodeScanner, Code } from 'react-native-vision-camera';
import { useCameraPermissions } from '../../hooks/useCameraPermissions.ts';
import { theme } from '../theme.ts';
export const Scanner: React.FC<Props> = ({
isScannerOpen,
navigateToUrl,
setNavigationStack,
closeBarCodeScanner,
baseUrl,
specialLinks,
}) => {
const { t } = useTranslation();
const cameraPermission = useCameraPermissions(isScannerOpen);
const allDevices = Camera.getAvailableCameraDevices();
const devices = getCameraDevice(allDevices, 'back', {
physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'],
});
const [isLoading, setIsLoading] = useState(false);
const [camStyles, setCamStyles] = useState<ViewStyle>({ width: 0, height: 0 }); // SET DEFAULT camStyle
const handleCodeScanned = useCallback(
async (codes: Code[]) => {
// handle the scanned Code callback
},
[baseUrl, navigateToUrl, setNavigationStack, specialLinks],
);
const codeScanner = useCodeScanner({
codeTypes: ['qr', 'ean-13', 'ean-8', 'code-128', 'code-93', 'code-39'],
onCodeScanned: handleCodeScanned,
});
const cameraContent = () => {
if (cameraPermission === 'not-determined' || !devices) {
return <View style={[styles.fill, styles.black]} />;
}
if (cameraPermission !== 'granted') {
return (
<View style={[styles.fill, styles.noAccess]}>
<Text>{t('scanner.noAccess')}</Text>
<Button
onPress={() => Linking.openSettings().catch(() => console.warn('Unable to open settings'))}
>
{t('scanner.openSettings')}
</Button>
</View>
);
}
if (isLoading) {
return (
<View style={[styles.fill, styles.loadingContainer]}>
<ActivityIndicator size="large" color={theme.colors.actionColor} />
</View>
);
}
return (
<View style={styles.fill}>
<Camera
style={camStyles}
device={devices}
isActive={true}
codeScanner={codeScanner}
focusable={true}
onLayout={() => {
// This is the workaround to correctly render the camera inside of its container.
// Otherwise, it sometimes appears outside of the expected container
// Still not perfect, but ok
setCamStyles({
flex: 1,
});
}}
/>
<BarcodeMask height={130} showAnimatedLine={false} />
</View>
);
};
return (
<Modal // Using a Modal instead of a Portal
statusBarTranslucent={true}
transparent={true}
visible={isScannerOpen}
animationType={'slide'}
onRequestClose={() => {
closeBarCodeScanner();
}}
>
<View style={styles.container}>
<Appbar.Header>
<Appbar.Action icon="close" onPress={closeBarCodeScanner} />
<Appbar.Content title={t('scanner.title')} />
</Appbar.Header>
{cameraContent()}
</View>
</Modal>
);
};
const styles = StyleSheet.create({
fill: {
flex: 1,
},
container: {
flex: 1,
flexDirection: 'column',
},
black: {
backgroundColor: 'black',
},
loadingContainer: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white',
},
noAccess: {
justifyContent: 'center',
alignItems: 'center',
padding: 20,
backgroundColor: 'white',
},
});
With this solution, I can currently live with - but it's still a bug to me that it doesn't work consistently.
Andreas
Setting the androidPreviewViewType to "surface-view" didn't solve the problem.
You should set the surface mode to texture-view, not surface-view. surface-view is the default.
Sorry - that's what I ment.
androidPreviewViewType={"texture-view"}
The combination of onLayout{() => ...} and setting this flag to "texture-view" seems to be the best working solution currently, though it's still not entirely consistent. I can still occasionally disrupt the layout, but it happens rarely. I can reproduce the issue by launching the application and quickly opening the camera modal view right after the app starts.
If I completely remove the onLayout{() => ...} and only set the "texture-view", it happens quite often. But so far, i'm happy with that solution for now.
<View style={styles.fill}>
<Camera
androidPreviewViewType={"texture-view"}
style={camStyles}
device={devices}
isActive={true}
codeScanner={codeScanner}
focusable={true}
onLayout={() => {
// This is a workaround to correctly render the camera inside of its container.
// Otherwise, it sometimes appears outside of the expected container
setCamStyles({
flex: 1,
});
}}
/>
<BarcodeMask height={130} showAnimatedLine={false} />
</View>
I can confirm this bug. When I use the camera in a portal on Android, the layout of the view gets messed up weirdly. It doesn't always happen, but quite often. Thanks @abucher90 for your workaround.