From 69baacad738e06382bac5c085b541ab794eccef9 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 10 Nov 2023 12:08:28 +0100 Subject: [PATCH] chore: Simplifies Code Scanner Page (#2145) * chore: Clean up Code Scanner Page * Update CodeScannerPage.tsx --- package/example/src/CameraPage.tsx | 9 +- package/example/src/CodeScannerPage.tsx | 200 ++++-------------------- package/example/src/Constants.ts | 3 + package/example/src/DevicesPage.tsx | 10 +- 4 files changed, 44 insertions(+), 178 deletions(-) diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx index 58945f1..a6c7db3 100644 --- a/package/example/src/CameraPage.tsx +++ b/package/example/src/CameraPage.tsx @@ -4,7 +4,7 @@ import { StyleSheet, Text, View } from 'react-native' import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler' import { CameraRuntimeError, PhotoFile, useCameraDevice, useCameraFormat, useFrameProcessor, VideoFile } from 'react-native-vision-camera' import { Camera } from 'react-native-vision-camera' -import { CONTENT_SPACING, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING, SCREEN_HEIGHT, SCREEN_WIDTH } from './Constants' +import { CONTENT_SPACING, CONTROL_BUTTON_SIZE, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING, SCREEN_HEIGHT, SCREEN_WIDTH } from './Constants' import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from 'react-native-reanimated' import { useEffect } from 'react' import { useIsForeground } from './hooks/useIsForeground' @@ -26,7 +26,6 @@ Reanimated.addWhitelistedNativeProps({ }) const SCALE_FULL_ZOOM = 3 -const BUTTON_SIZE = 40 type Props = NativeStackScreenProps export function CameraPage({ navigation }: Props): React.ReactElement { @@ -262,9 +261,9 @@ const styles = StyleSheet.create({ }, button: { marginBottom: CONTENT_SPACING, - width: BUTTON_SIZE, - height: BUTTON_SIZE, - borderRadius: BUTTON_SIZE / 2, + width: CONTROL_BUTTON_SIZE, + height: CONTROL_BUTTON_SIZE, + borderRadius: CONTROL_BUTTON_SIZE / 2, backgroundColor: 'rgba(140, 140, 140, 0.3)', justifyContent: 'center', alignItems: 'center', diff --git a/package/example/src/CodeScannerPage.tsx b/package/example/src/CodeScannerPage.tsx index 13929dc..40b7541 100644 --- a/package/example/src/CodeScannerPage.tsx +++ b/package/example/src/CodeScannerPage.tsx @@ -1,12 +1,9 @@ import * as React from 'react' -import { useRef, useState, useCallback } from 'react' +import { useState } from 'react' import { StyleSheet, View } from 'react-native' -import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler' -import { CameraRuntimeError, useCameraDevice, useCameraFormat, useCodeScanner } from 'react-native-vision-camera' +import { useCameraDevice, useCodeScanner } from 'react-native-vision-camera' import { Camera } from 'react-native-vision-camera' -import { CONTENT_SPACING, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING, SCREEN_HEIGHT, SCREEN_WIDTH } from './Constants' -import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from 'react-native-reanimated' -import { useEffect } from 'react' +import { CONTENT_SPACING, CONTROL_BUTTON_SIZE, SAFE_AREA_PADDING } from './Constants' import { useIsForeground } from './hooks/useIsForeground' import { StatusBarBlurBackground } from './views/StatusBarBlurBackground' import { PressableOpacity } from 'react-native-pressable-opacity' @@ -14,130 +11,23 @@ import IonIcon from 'react-native-vector-icons/Ionicons' import type { Routes } from './Routes' import type { NativeStackScreenProps } from '@react-navigation/native-stack' import { useIsFocused } from '@react-navigation/core' -import { usePreferredCameraDevice } from './hooks/usePreferredCameraDevice' - -const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera) -Reanimated.addWhitelistedNativeProps({ - zoom: true, -}) - -const SCALE_FULL_ZOOM = 3 -const BUTTON_SIZE = 40 type Props = NativeStackScreenProps export function CodeScannerPage({ navigation }: Props): React.ReactElement { - const camera = useRef(null) - const [isCameraInitialized, setIsCameraInitialized] = useState(false) - const zoom = useSharedValue(0) - const isPressingButton = useSharedValue(false) + // 1. Use a simple default back camera + const device = useCameraDevice('back') - // check if camera page is active - const isFocussed = useIsFocused() + // 2. Only activate Camera when the app is focused and this screen is currently opened + const isFocused = useIsFocused() const isForeground = useIsForeground() - const isActive = isFocussed && isForeground + const isActive = isFocused && isForeground - const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>('back') - const [enableHdr, setEnableHdr] = useState(false) - const [torch, setTorch] = useState<'off' | 'on'>('off') - const [enableNightMode, setEnableNightMode] = useState(false) - - // camera device settings - const [preferredDevice] = usePreferredCameraDevice() - let device = useCameraDevice(cameraPosition) - - if (preferredDevice != null && preferredDevice.position === cameraPosition) { - // override default device with the one selected by the user in settings - device = preferredDevice - } - - const [targetFps, setTargetFps] = useState(60) - - const screenAspectRatio = SCREEN_HEIGHT / SCREEN_WIDTH - const format = useCameraFormat(device, [ - { fps: targetFps }, - { videoAspectRatio: screenAspectRatio }, - { videoResolution: 'max' }, - { photoAspectRatio: screenAspectRatio }, - { photoResolution: 'max' }, - ]) - - const fps = Math.min(format?.maxFps ?? 1, targetFps) - - const supportsTorch = device?.hasTorch ?? false - - //#region Animated Zoom - // This just maps the zoom factor to a percentage value. - // so e.g. for [min, neutr., max] values [1, 2, 128] this would result in [0, 0.0081, 1] - const minZoom = device?.minZoom ?? 1 - const maxZoom = Math.min(device?.maxZoom ?? 1, MAX_ZOOM_FACTOR) - - const cameraAnimatedProps = useAnimatedProps(() => { - const z = Math.max(Math.min(zoom.value, maxZoom), minZoom) - return { - zoom: z, - } - }, [maxZoom, minZoom, zoom]) - //#endregion - - //#region Callbacks - - // Camera callbacks - const onError = useCallback((error: CameraRuntimeError) => { - console.error(error) - }, []) - const onInitialized = useCallback(() => { - console.log('Camera initialized!') - setIsCameraInitialized(true) - }, []) - const onFlipCameraPressed = useCallback(() => { - setCameraPosition((p) => (p === 'back' ? 'front' : 'back')) - }, []) - const onTorchPressed = useCallback(() => { - setTorch((f) => (f === 'off' ? 'on' : 'off')) - }, []) - //#endregion - - //#region Tap Gesture - const onDoubleTap = useCallback(() => { - onFlipCameraPressed() - }, [onFlipCameraPressed]) - //#endregion - - //#region Effects - const neutralZoom = device?.neutralZoom ?? 1 - useEffect(() => { - // Run everytime the neutralZoomScaled value changes. (reset zoom when device changes) - zoom.value = neutralZoom - }, [neutralZoom, zoom]) - - //#endregion - - //#region Pinch to Zoom Gesture - // The gesture handler maps the linear pinch gesture (0 - 1) to an exponential curve since a camera's zoom - // function does not appear linear to the user. (aka zoom 0.1 -> 0.2 does not look equal in difference as 0.8 -> 0.9) - const onPinchGesture = useAnimatedGestureHandler({ - onStart: (_, context) => { - context.startZoom = zoom.value - }, - onActive: (event, context) => { - // we're trying to map the scale gesture to a linear zoom here - const startZoom = context.startZoom ?? 0 - const scale = interpolate(event.scale, [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM], [-1, 0, 1], Extrapolate.CLAMP) - zoom.value = interpolate(scale, [-1, 0, 1], [minZoom, startZoom, maxZoom], Extrapolate.CLAMP) - }, - }) - //#endregion - - useEffect(() => { - const f = - format != null - ? `(${format.photoWidth}x${format.photoHeight} photo / ${format.videoWidth}x${format.videoHeight}@${format.maxFps} video @ ${fps}fps)` - : undefined - console.log(`Camera: ${device?.name} | Format: ${f}`) - }, [device?.name, format, fps]) + // 3. (Optional) enable a torch setting + const [torch, setTorch] = useState(false) + // 4. Initialize the Code Scanner to scan QR codes and Barcodes const codeScanner = useCodeScanner({ - codeTypes: ['qr'], + codeTypes: ['qr', 'ean-13'], onCodeScanned: (codes, frame) => { console.log(codes, frame) }, @@ -146,50 +36,27 @@ export function CodeScannerPage({ navigation }: Props): React.ReactElement { return ( {device != null && ( - - - - - - - + )} - - - - {torch && ( - - - - )} - navigation.navigate('CameraPage')}> - - - navigation.navigate('Devices')}> - + setTorch(!torch)} disabledOpacity={0.4}> + + + {/* Back Button */} + + + ) } @@ -201,9 +68,9 @@ const styles = StyleSheet.create({ }, button: { marginBottom: CONTENT_SPACING, - width: BUTTON_SIZE, - height: BUTTON_SIZE, - borderRadius: BUTTON_SIZE / 2, + width: CONTROL_BUTTON_SIZE, + height: CONTROL_BUTTON_SIZE, + borderRadius: CONTROL_BUTTON_SIZE / 2, backgroundColor: 'rgba(140, 140, 140, 0.3)', justifyContent: 'center', alignItems: 'center', @@ -213,10 +80,9 @@ const styles = StyleSheet.create({ right: SAFE_AREA_PADDING.paddingRight, top: SAFE_AREA_PADDING.paddingTop, }, - text: { - color: 'white', - fontSize: 11, - fontWeight: 'bold', - textAlign: 'center', + backButton: { + position: 'absolute', + left: SAFE_AREA_PADDING.paddingLeft + CONTENT_SPACING, + top: SAFE_AREA_PADDING.paddingTop + CONTENT_SPACING, }, }) diff --git a/package/example/src/Constants.ts b/package/example/src/Constants.ts index a17bc8b..9e4e0f7 100644 --- a/package/example/src/Constants.ts +++ b/package/example/src/Constants.ts @@ -26,3 +26,6 @@ export const SCREEN_HEIGHT = Platform.select({ // Capture Button export const CAPTURE_BUTTON_SIZE = 78 + +// Control Button like Flash +export const CONTROL_BUTTON_SIZE = 40 diff --git a/package/example/src/DevicesPage.tsx b/package/example/src/DevicesPage.tsx index 7742c79..1ba77d9 100644 --- a/package/example/src/DevicesPage.tsx +++ b/package/example/src/DevicesPage.tsx @@ -42,20 +42,20 @@ function Device({ device, onPress }: DeviceProps): React.ReactElement { return ( - + {device.name} ({device.position}) {deviceTypes} - + {maxPhotoRes.photoWidth}x{maxPhotoRes.photoHeight} - + {maxVideoRes.videoWidth}x{maxVideoRes.videoHeight} @ {maxVideoRes.maxFps} FPS @@ -122,7 +122,7 @@ export function DevicesPage({ navigation }: Props): React.ReactElement { - + Camera Devices @@ -208,8 +208,6 @@ const styles = StyleSheet.create({ height: 40, marginTop: 7, }, - icon: {}, - inlineIcon: {}, resolutionText: { marginLeft: 5, fontSize: 12,