rodgomesc / vision-camera-code-scanner

VisionCamera Frame Processor Plugin to read barcodes using MLKit Vision QrCode Scanning
MIT License
338 stars 223 forks source link

add a frame for aiming scanning #125

Open valery-lavrik opened 1 year ago

valery-lavrik commented 1 year ago

Somewhere I came across instructions on how to do this, but I can't find it anymore... Does anyone happen to know?

valery-lavrik commented 1 year ago

It is very difficult to write such a frame yourself. I am sure there must be a ready-made solution...

severinferard commented 1 year ago

I'm currently working on this. I have a working iOS version here.

https://user-images.githubusercontent.com/51379148/217338554-d7ec76ce-0a8f-4cc1-a920-1d83f70bb3b2.mov

const frameProcessor = useFrameProcessor((frame) => {
    'worklet';
    const squareSize = frame.width * MARKER_WIDTH_RATIO - 100;

    const scanFrame: ScanFrame = {
      width: squareSize,
      height: squareSize,
      x: (frame.width - squareSize) / 2,
      y: (frame.height - squareSize) / 2,
    };
    const detectedBarcodes = scanBarcodes(frame, [BarcodeFormat.QR_CODE], {
      checkInverted: true,
      scanFrame: scanFrame,
    });
    runOnJS(setBarcodes)(detectedBarcodes);
  }, []);

@rodgomesc is that a feature you would like to add to the the library ? If so I can start working on a PR.

valery-lavrik commented 1 year ago

I wish it was for android...

severinferard commented 1 year ago

I plan on making the feature available on Android as well, I just don't have an Android device to test it on right now.

sanduluca commented 1 year ago

@severinferard Why you have 2 barcodes when you scan the QR code ?

I am also interested in this feature. Would be cool if you manage to finish it.

mehmettalhairmak commented 1 year ago

I'm currently working on this. I have a working iOS version here.

const frameProcessor = useFrameProcessor((frame) => {
    'worklet';
    const squareSize = frame.width * MARKER_WIDTH_RATIO - 100;

    const scanFrame: ScanFrame = {
      width: squareSize,
      height: squareSize,
      x: (frame.width - squareSize) / 2,
      y: (frame.height - squareSize) / 2,
    };
    const detectedBarcodes = scanBarcodes(frame, [BarcodeFormat.QR_CODE], {
      checkInverted: true,
      scanFrame: scanFrame,
    });
    runOnJS(setBarcodes)(detectedBarcodes);
  }, []);

@rodgomesc is that a feature you would like to add to the the library ? If so I can start working on a PR.

I'm need this for iOS and Android :(

valery-lavrik commented 1 year ago

I managed to make a crooked but working solution on RN. If you're interested, I can post it....

koolll commented 1 year ago

@valery-lavrik can you please post it? I am interested

valery-lavrik commented 1 year ago

Just do not judge strictly, I have little such experience. If you have any ideas what can be improved, please let me know

import React, { useState, useRef, useEffect } from 'react';
import { View, StyleSheet, Alert, Dimensions } from 'react-native';
import { runOnJS } from 'react-native-reanimated';
import { Camera, useCameraDevices, useFrameProcessor } from 'react-native-vision-camera';
import { scanBarcodes, BarcodeFormat } from 'vision-camera-code-scanner';

let rotateListener = null;

export default function QRScanner({
    onScan = () => { },
}) {
    const AimingBorderSize = useRef(null);
    const isFind = useRef(false);
    const [qrBorders, setQrBorders] = useState([]);
    const devices = useCameraDevices();
    const device = devices.back;

    const barCodesHandler = (detectedBarcodes, frame) => {

        if (!!isFind?.current) {
            return null;
        }

        if (!detectedBarcodes?.length) {
            setQrBorders([]);
            return null;
        }

        // 1) отфильтрую одинаковые
        const codeFiltered = detectedBarcodes.filter((value, index, self) => self.findIndex((el, i) => el.content.data === value.content.data) === index);

        // 2) нарисую им рамки
        const bordersStyles = [];
        const win = Dimensions.get('window');
        const isPortal = win.height > win.width;
        const rH = win.height / (isPortal ? Math.max(frame.height, frame.width) : Math.min(frame.height, frame.width));
        const rW = win.width / (isPortal ? Math.min(frame.height, frame.width) : Math.max(frame.height, frame.width));

        for (let i = 0; i < codeFiltered.length; i++) {
            const b = codeFiltered[i];

            bordersStyles.push({
                inside: false,
                boundingBox: {
                    bottom: b.boundingBox.bottom * rH,
                    left: b.boundingBox.left * rW,
                    right: b.boundingBox.right * rW,
                    top: b.boundingBox.top * rH,
                },
                style: {
                    borderWidth: 1,
                    borderColor: 'red',
                    position: 'absolute',
                    top: (Math.min(b.cornerPoints[0].y, b.cornerPoints[1].y)) * rH,  //  b.cornerPoints[1].y
                    left: (Math.min(b.cornerPoints[0].x, b.cornerPoints[3].x)) * rW, // b.cornerPoints[3].x
                    height: (b.cornerPoints[3].y - b.cornerPoints[0].y) * rH,
                    width: (b.cornerPoints[1].x - b.cornerPoints[0].x) * rW,
                },
            });

            if (isInside(bordersStyles[bordersStyles.length - 1])) {
                bordersStyles[bordersStyles.length - 1].inside = true;
                bordersStyles[bordersStyles.length - 1].style.borderColor = '#00FF00';
                bordersStyles[bordersStyles.length - 1].style.borderWidth = 2;
            }
        }
        setQrBorders(bordersStyles);

        // 3) отправлю в коллбек
        const insideIndex = bordersStyles.findIndex(el => el.inside);
        if (insideIndex !== -1) {
            isFind.current = true;
            onScan(codeFiltered[insideIndex].displayValue.toLowerCase().trim());
        }

    };

    const isInside = (oneBord) => {
        const ab = AimingBorderSize.current;
        const bb = oneBord.boundingBox;

        if (bb.top > ab.yt && bb.bottom < ab.yb && bb.left > ab.xl && bb.right < ab.xr) {
            return true;
        }

        return false;
    };

    const frameProcessor = useFrameProcessor((frame) => {
        'worklet';
        const detectedBarcodes = scanBarcodes(frame, [BarcodeFormat.QR_CODE], { checkInverted: true });
        runOnJS(barCodesHandler)(detectedBarcodes, frame);
    }, []);

    return (
        <View style={{ flex: 1 }}>
            {device != null && (
                <>
                    <Camera
                        style={StyleSheet.absoluteFill}
                        device={device}
                        isActive={true}
                        frameProcessor={frameProcessor}
                        frameProcessorFps={5}
                    />

                    {/* Зеленая рамка для прицеливания */}
                    <FrameForAiming getAimingBorderPos={(b) => (AimingBorderSize.current = b)} />

                    {/* Подсвечу все что распозналось */}
                    {!!qrBorders?.length && (
                        qrBorders.map((b, k) => (<View key={k} style={b.style} />))
                    )}
                </>
            )}
        </View>
    );
}

export function FrameForAiming({
    getAimingBorderPos = () => { },
}) {
    const [H, setH] = React.useState(0);
    const maxLayouts = 5;
    const curLayouts = useRef(0);

    useEffect(() => {
        rotateListener = Dimensions.addEventListener('change', () => {
            curLayouts.current = 0;
            setH(0);
        });

        return () => {
            rotateListener?.remove();
        };
    });

    return (
        <View style={ST_.ScreenWrap}>
            <View
                style={[ST_.wrap, { height: H }]}
                onLayout={(e) => {
                    if (curLayouts.current < maxLayouts) {
                        const l = e.nativeEvent.layout;
                        l.height = l.width;

                        setH(parseInt(l.width, 10));

                        getAimingBorderPos({
                            xl: parseInt(l.x, 10),
                            xr: parseInt(l.x + l.width, 10),
                            yt: parseInt(l.y, 10),
                            yb: parseInt(l.y + l.height, 10),
                        });

                        curLayouts.current++;
                    }
                }}
            >
                <View style={ST_.tlb} />
                <View style={ST_.trb} />
                <View style={ST_.blb} />
                <View style={ST_.brb} />
            </View>
        </View>
    );
}

const BorderWidth = 1;
const BorderStyle = {
    position: 'absolute',
    width: '35%',
    height: '35%',
    borderColor: '#00FF00',
};
const ST_ = {
    ScreenWrap: {
        justifyContent: 'center',
        alignItems: 'center',
        flex: 1,
    },
    wrap: {
        width: '45%',
    },
    tlb: {
        top: 0,
        left: 0,
        borderTopWidth: BorderWidth,
        borderLeftWidth: BorderWidth,
        ...BorderStyle,
    },
    trb: {
        top: 0,
        right: 0,
        borderTopWidth: BorderWidth,
        borderRightWidth: BorderWidth,
        ...BorderStyle,
    },
    blb: {
        bottom: 0,
        left: 0,
        borderBottomWidth: BorderWidth,
        borderLeftWidth: BorderWidth,
        ...BorderStyle,
    },
    brb: {
        bottom: 0,
        right: 0,
        borderBottomWidth: BorderWidth,
        borderRightWidth: BorderWidth,
        ...BorderStyle,
    },
};
Ar-Shak commented 1 year ago

I took inspiration from @valery-lavrik code and came up with the following solution.

index.tsx

import { Text, TouchableRipple, useTheme } from "react-native-paper"
import { AppTheme } from "../../theme"
import createStyles from "./styles"
import { NativeStackScreenProps } from "@react-navigation/native-stack"
import { RootStackParamList } from "../../navigation/RootStackParamList"
import { RouteName } from "../../navigation/RouteName"
import { Camera, useCameraDevices, useFrameProcessor } from "react-native-vision-camera"
import { Dimensions, Platform, StyleSheet, View } from "react-native"
import { useEffect, useState } from "react"
import Icon from "react-native-vector-icons/MaterialIcons";
import { Barcode, BarcodeFormat, scanBarcodes } from "vision-camera-code-scanner"
import Log from "../../utils/log"
import { RNHoleView } from 'react-native-hole-view';
import { heightPercentageToDP, widthPercentageToDP } from 'react-native-responsive-screen'
import { SafeAreaView } from 'react-native-safe-area-context'
import { runOnJS } from 'react-native-reanimated'

type Props = NativeStackScreenProps<RootStackParamList, RouteName.CameraScanner>

const CameraScanner = ({ navigation, route }: Props) => {

    const TAG = CameraScanner.name

    const theme = useTheme<AppTheme>()
    const styles = createStyles(theme)

    const [hasPermission, setHasPermission] = useState(false);
    const [barcodes, setBarcodes] = useState<Barcode[]>([])
    const [isScanned, setIsScanned] = useState<boolean>(false);

    const devices = useCameraDevices()
    const device = devices.back

    const vf_x = widthPercentageToDP('8.5%')
    const vf_y = heightPercentageToDP('20%')
    const vf_width = widthPercentageToDP('83%')
    const vf_height = heightPercentageToDP('25%')

    const win = Dimensions.get('window');

    useEffect(() => {

        requestPermissions()

    }, [])

    useEffect(() => {

        toggleActiveState()

        return () => {
            barcodes;
        }

    }, [barcodes])

    const toggleActiveState = async () => {

        if (barcodes && barcodes.length > 0 && isScanned === false) {

            setIsScanned(true)

            if (barcodes[0].rawValue !== '') {

                setBarcodes([barcodes[0]]);
                Log(TAG, 'scanned barcode = ' + barcodes[0].rawValue)

            }

        }

    }

    const frameProcessor = useFrameProcessor(frame => {
        "worklet"
        const detectedBarcodes = scanBarcodes(frame, [BarcodeFormat.ALL_FORMATS], { checkInverted: true })

        if (detectedBarcodes?.length) {

            // console.log('frameProcessor Barcodes :' + JSON.stringify(detectedBarcodes))

            var handleBarcode = function () {

                // Looked into sample code from https://github.com/rodgomesc/vision-camera-code-scanner/issues/125

                for (let barcode of detectedBarcodes) {

                    const isPortal = win.height > win.width;
                    const rH = win.height / (isPortal ? Math.max(frame.height, frame.width) : Math.min(frame.height, frame.width));
                    const rW = win.width / (isPortal ? Math.min(frame.height, frame.width) : Math.max(frame.height, frame.width));

                    var boundingBottom = (barcode.boundingBox?.bottom ?? 0) * rH
                    var boundingLeft = (barcode.boundingBox?.left ?? 0) * rW
                    var boundingRight = (barcode.boundingBox?.right ?? 0) * rW
                    var boundingTop = (barcode.boundingBox?.top ?? 0) * rH

                    // iOS - wasn't sending bounding box so using cornerPoints instead

                    // cornerPoints
                    // The four corner points of the barcode, in clockwise order starting with the top left relative to the detected image in the view coordinate system. 
                    // These are CGPoints wrapped in NSValues. Due to the possible perspective distortions, this is not necessarily a rectangle.
                    // https://developers.google.com/ml-kit/reference/swift/mlkitbarcodescanning/api/reference/Classes/Barcode#cornerpoints

                    if (Platform.OS !== 'android') {

                        boundingLeft = (barcode.cornerPoints?.[0].x ?? 0) * rW
                        boundingRight = (barcode.cornerPoints?.[1].x ?? 0) * rW
                        boundingTop = (barcode.cornerPoints?.[0].y ?? 0) * rH
                        boundingBottom = (barcode.cornerPoints?.[2].y ?? 0) * rH

                    }

                    // console.log(
                    //     'Bounding Box Bottom = ' + boundingBottom +
                    //     ' Left = ' + boundingLeft +
                    //     ' Right = ' + boundingRight +
                    //     ' Top = ' + boundingTop
                    // )

                    const vf_bottom = vf_y + vf_height;
                    const vf_left = vf_x;
                    const vf_right = vf_x + vf_width;
                    const vf_top = vf_y;

                    // console.log(
                    //     'view finder Bottom = ' + vf_bottom +
                    //     ' Left = ' + vf_left +
                    //     ' Right = ' + vf_right +
                    //     ' Top = ' + vf_top
                    // )

                    if (
                        boundingTop > vf_top &&
                        boundingBottom < vf_bottom &&
                        boundingLeft > vf_left &&
                        boundingRight < vf_right
                    ) {
                        // console.log('Barcode is inside View Finder')
                        runOnJS(setBarcodes)([barcode])
                        break
                    }

                }

            }

            handleBarcode()

        }

    }, [])

    const requestPermissions = async () => {
        const status = await Camera.requestCameraPermission()
        setHasPermission(status === "authorized")
    }

    return (

        <View
            style={styles.container}
        >

            {
                device !== undefined &&
                hasPermission && frameProcessor !== undefined && (
                    <>
                        <Camera
                            style={StyleSheet.absoluteFill}
                            device={device}
                            audio={false}
                            isActive={!isScanned}
                            frameProcessor={frameProcessor}
                            frameProcessorFps={5}
                        />
                        <RNHoleView
                            holes={[
                                {
                                    x: vf_x,
                                    y: vf_y,
                                    width: vf_width,
                                    height: vf_height,
                                    borderRadius: 10,
                                },
                            ]}
                            style={styles.rnholeView}
                        />
                        <View
                            style={{
                                height: 2,
                                width: widthPercentageToDP('83%'),
                                marginStart: widthPercentageToDP('8.5%'),
                                marginTop: heightPercentageToDP('32.5%'),
                                backgroundColor: 'red'
                            }}
                        />

                    </>
                )
            }

            <SafeAreaView
                style={styles.safeAreaView}
            >

                <TouchableRipple
                    style={styles.backButton}
                    onPress={
                        () => {
                            navigation.goBack()
                        }
                    }
                    rippleColor={theme.colors.elevation.level0}
                    borderless={true}
                >
                    <Icon
                        name="arrow-back-ios"
                        size={30}
                        color={theme.colors.onPrimary}
                        style={{ start: 10 }}
                    />
                </TouchableRipple>

                {
                    barcodes.length ?

                        <View
                            style={styles.containerView}
                        >

                            <View
                                style={styles.barcodeDetectedView}
                            >
                                <Text
                                    variant="titleMedium"
                                    style={styles.barcodeTitle}
                                >
                                    Barcode Detected
                                </Text>
                                <Text
                                    style={styles.barcodeTextURL}
                                    variant="titleLarge"
                                >
                                    {barcodes[0].displayValue}
                                </Text>
                            </View>

                        </View>

                        :

                        <></>

                }

            </SafeAreaView>

        </View>

    )

}

export default CameraScanner

styles.ts

import { StyleSheet } from "react-native";
import { MD3Theme } from "react-native-paper";

const createStyles = (theme: MD3Theme) => StyleSheet.create({
    container: {
        backgroundColor: 'black',
        color: 'black',
        flex: 1,
    },
    backButton: {
        borderRadius: 50,
        height: 40,
        width: 40,
        marginEnd: 10,
        left: 20,
        top: 15,
    },
    rnholeView: {
        position: 'absolute',
        width: '100%',
        height: '100%',
        alignSelf: 'center',
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'rgba(0,0,0,0.5)',
    },
    safeAreaView: {
        position: 'absolute',
        left: 0,
        top: 0,
        right: 0,
        bottom: 0,
    },
    containerView: {
        flex: 1,
    },
    barcodeDetectedView: {
        flex: 1,
        justifyContent: 'flex-end',
        paddingBottom: 40,
    },
    barcodeTitle: {
        alignSelf: 'center',
        color: 'white',
    },
    barcodeTextURL: {
        fontSize: 20,
        color: 'white',
        alignSelf: 'center',
    },
})

export default createStyles
chaos2171053 commented 1 year ago

I took inspiration from @valery-lavrik code and came up with the following solution.

index.tsx

import { Text, TouchableRipple, useTheme } from "react-native-paper"
import { AppTheme } from "../../theme"
import createStyles from "./styles"
import { NativeStackScreenProps } from "@react-navigation/native-stack"
import { RootStackParamList } from "../../navigation/RootStackParamList"
import { RouteName } from "../../navigation/RouteName"
import { Camera, useCameraDevices, useFrameProcessor } from "react-native-vision-camera"
import { Dimensions, Platform, StyleSheet, View } from "react-native"
import { useEffect, useState } from "react"
import Icon from "react-native-vector-icons/MaterialIcons";
import { Barcode, BarcodeFormat, scanBarcodes } from "vision-camera-code-scanner"
import Log from "../../utils/log"
import { RNHoleView } from 'react-native-hole-view';
import { heightPercentageToDP, widthPercentageToDP } from 'react-native-responsive-screen'
import { SafeAreaView } from 'react-native-safe-area-context'
import { runOnJS } from 'react-native-reanimated'

type Props = NativeStackScreenProps<RootStackParamList, RouteName.CameraScanner>

const CameraScanner = ({ navigation, route }: Props) => {

    const TAG = CameraScanner.name

    const theme = useTheme<AppTheme>()
    const styles = createStyles(theme)

    const [hasPermission, setHasPermission] = useState(false);
    const [barcodes, setBarcodes] = useState<Barcode[]>([])
    const [isScanned, setIsScanned] = useState<boolean>(false);

    const devices = useCameraDevices()
    const device = devices.back

    const vf_x = widthPercentageToDP('8.5%')
    const vf_y = heightPercentageToDP('20%')
    const vf_width = widthPercentageToDP('83%')
    const vf_height = heightPercentageToDP('25%')

    const win = Dimensions.get('window');

    useEffect(() => {

        requestPermissions()

    }, [])

    useEffect(() => {

        toggleActiveState()

        return () => {
            barcodes;
        }

    }, [barcodes])

    const toggleActiveState = async () => {

        if (barcodes && barcodes.length > 0 && isScanned === false) {

            setIsScanned(true)

            if (barcodes[0].rawValue !== '') {

                setBarcodes([barcodes[0]]);
                Log(TAG, 'scanned barcode = ' + barcodes[0].rawValue)

            }

        }

    }

    const frameProcessor = useFrameProcessor(frame => {
        "worklet"
        const detectedBarcodes = scanBarcodes(frame, [BarcodeFormat.ALL_FORMATS], { checkInverted: true })

        if (detectedBarcodes?.length) {

            // console.log('frameProcessor Barcodes :' + JSON.stringify(detectedBarcodes))

            var handleBarcode = function () {

                // Looked into sample code from https://github.com/rodgomesc/vision-camera-code-scanner/issues/125

                for (let barcode of detectedBarcodes) {

                    const isPortal = win.height > win.width;
                    const rH = win.height / (isPortal ? Math.max(frame.height, frame.width) : Math.min(frame.height, frame.width));
                    const rW = win.width / (isPortal ? Math.min(frame.height, frame.width) : Math.max(frame.height, frame.width));

                    var boundingBottom = (barcode.boundingBox?.bottom ?? 0) * rH
                    var boundingLeft = (barcode.boundingBox?.left ?? 0) * rW
                    var boundingRight = (barcode.boundingBox?.right ?? 0) * rW
                    var boundingTop = (barcode.boundingBox?.top ?? 0) * rH

                    // iOS - wasn't sending bounding box so using cornerPoints instead

                    // cornerPoints
                    // The four corner points of the barcode, in clockwise order starting with the top left relative to the detected image in the view coordinate system. 
                    // These are CGPoints wrapped in NSValues. Due to the possible perspective distortions, this is not necessarily a rectangle.
                    // https://developers.google.com/ml-kit/reference/swift/mlkitbarcodescanning/api/reference/Classes/Barcode#cornerpoints

                    if (Platform.OS !== 'android') {

                        boundingLeft = (barcode.cornerPoints?.[0].x ?? 0) * rW
                        boundingRight = (barcode.cornerPoints?.[1].x ?? 0) * rW
                        boundingTop = (barcode.cornerPoints?.[0].y ?? 0) * rH
                        boundingBottom = (barcode.cornerPoints?.[2].y ?? 0) * rH

                    }

                    // console.log(
                    //     'Bounding Box Bottom = ' + boundingBottom +
                    //     ' Left = ' + boundingLeft +
                    //     ' Right = ' + boundingRight +
                    //     ' Top = ' + boundingTop
                    // )

                    const vf_bottom = vf_y + vf_height;
                    const vf_left = vf_x;
                    const vf_right = vf_x + vf_width;
                    const vf_top = vf_y;

                    // console.log(
                    //     'view finder Bottom = ' + vf_bottom +
                    //     ' Left = ' + vf_left +
                    //     ' Right = ' + vf_right +
                    //     ' Top = ' + vf_top
                    // )

                    if (
                        boundingTop > vf_top &&
                        boundingBottom < vf_bottom &&
                        boundingLeft > vf_left &&
                        boundingRight < vf_right
                    ) {
                        // console.log('Barcode is inside View Finder')
                        runOnJS(setBarcodes)([barcode])
                        break
                    }

                }

            }

            handleBarcode()

        }

    }, [])

    const requestPermissions = async () => {
        const status = await Camera.requestCameraPermission()
        setHasPermission(status === "authorized")
    }

    return (

        <View
            style={styles.container}
        >

            {
                device !== undefined &&
                hasPermission && frameProcessor !== undefined && (
                    <>
                        <Camera
                            style={StyleSheet.absoluteFill}
                            device={device}
                            audio={false}
                            isActive={!isScanned}
                            frameProcessor={frameProcessor}
                            frameProcessorFps={5}
                        />
                        <RNHoleView
                            holes={[
                                {
                                    x: vf_x,
                                    y: vf_y,
                                    width: vf_width,
                                    height: vf_height,
                                    borderRadius: 10,
                                },
                            ]}
                            style={styles.rnholeView}
                        />
                        <View
                            style={{
                                height: 2,
                                width: widthPercentageToDP('83%'),
                                marginStart: widthPercentageToDP('8.5%'),
                                marginTop: heightPercentageToDP('32.5%'),
                                backgroundColor: 'red'
                            }}
                        />

                    </>
                )
            }

            <SafeAreaView
                style={styles.safeAreaView}
            >

                <TouchableRipple
                    style={styles.backButton}
                    onPress={
                        () => {
                            navigation.goBack()
                        }
                    }
                    rippleColor={theme.colors.elevation.level0}
                    borderless={true}
                >
                    <Icon
                        name="arrow-back-ios"
                        size={30}
                        color={theme.colors.onPrimary}
                        style={{ start: 10 }}
                    />
                </TouchableRipple>

                {
                    barcodes.length ?

                        <View
                            style={styles.containerView}
                        >

                            <View
                                style={styles.barcodeDetectedView}
                            >
                                <Text
                                    variant="titleMedium"
                                    style={styles.barcodeTitle}
                                >
                                    Barcode Detected
                                </Text>
                                <Text
                                    style={styles.barcodeTextURL}
                                    variant="titleLarge"
                                >
                                    {barcodes[0].displayValue}
                                </Text>
                            </View>

                        </View>

                        :

                        <></>

                }

            </SafeAreaView>

        </View>

    )

}

export default CameraScanner

styles.ts

import { StyleSheet } from "react-native";
import { MD3Theme } from "react-native-paper";

const createStyles = (theme: MD3Theme) => StyleSheet.create({
    container: {
        backgroundColor: 'black',
        color: 'black',
        flex: 1,
    },
    backButton: {
        borderRadius: 50,
        height: 40,
        width: 40,
        marginEnd: 10,
        left: 20,
        top: 15,
    },
    rnholeView: {
        position: 'absolute',
        width: '100%',
        height: '100%',
        alignSelf: 'center',
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'rgba(0,0,0,0.5)',
    },
    safeAreaView: {
        position: 'absolute',
        left: 0,
        top: 0,
        right: 0,
        bottom: 0,
    },
    containerView: {
        flex: 1,
    },
    barcodeDetectedView: {
        flex: 1,
        justifyContent: 'flex-end',
        paddingBottom: 40,
    },
    barcodeTitle: {
        alignSelf: 'center',
        color: 'white',
    },
    barcodeTextURL: {
        fontSize: 20,
        color: 'white',
        alignSelf: 'center',
    },
})

export default createStyles

In fact, with this implementation, the scanning range is still the entire camera area. When scanning multiple codes, it is still difficult to recognize the desired code.

liangchaoqin commented 1 year ago

I'm currently working on this. I have a working iOS version here.

scanFrame-3.mov

const frameProcessor = useFrameProcessor((frame) => {
    'worklet';
    const squareSize = frame.width * MARKER_WIDTH_RATIO - 100;

    const scanFrame: ScanFrame = {
      width: squareSize,
      height: squareSize,
      x: (frame.width - squareSize) / 2,
      y: (frame.height - squareSize) / 2,
    };
    const detectedBarcodes = scanBarcodes(frame, [BarcodeFormat.QR_CODE], {
      checkInverted: true,
      scanFrame: scanFrame,
    });
    runOnJS(setBarcodes)(detectedBarcodes);
  }, []);

@rodgomesc is that a feature you would like to add to the the library ? If so I can start working on a PR.

Have you implemented this functionality on the Android version?

fukemy commented 1 year ago

> here

this is best solution I have found, did you successful to implement on Android side?

fukemy commented 1 year ago

I found other solution work : https://www.dynamsoft.com/codepool/react-native-qr-code-scanner-vision-camera.html

azuddin commented 1 year ago

I wish it was for android...

here's snippet for android. https://github.com/rodgomesc/vision-camera-code-scanner/commit/f4430376396f1bbaead5dc7a1b352c45abf02864

eisodev commented 1 year ago

Here is a working overlay that I made from scratch using 4 customizable square corners wrapped by a bigger square container. Makes it easier to make a full rectangle or just use rounded corner like most QR readers use to "aim". To make it without gaps, just change size of squares to be half of the overlay and customize borderRadius to taste:

const OVERLAY_HEIGHT = 250;
const OVERLAY_WIDTH = 250;
const SQUARE_HEIGHT = 50;
const SQUARE_WIDTH = 50;
const SQUARE_BORDER_WIDTH = 3;

const styles = StyleSheet.create({
    container: {
        alignItems: 'center',
        flex: 1,
        justifyContent: 'center',
    },
    iconButton: {
        position: 'absolute',
        right: 10,
        top: 10,
    },
    overlayContainer: {
        flexDirection: 'column',
        height: OVERLAY_HEIGHT,
        width: OVERLAY_WIDTH,
    },
    rowContainer: {
        flexDirection: 'row',
        flex: 1,
        justifyContent: 'space-between',
    },
});

const SquareComponent = ({ corner }) => {
    const { colors } = useTheme();
    const borderRadius = 20;
    let borderStyle = {};

    switch (corner) {
        case 'UpperLeft':
            borderStyle = {
                borderTopWidth: SQUARE_BORDER_WIDTH,
                borderLeftWidth: SQUARE_BORDER_WIDTH,
                borderColor: colors.white,
                borderTopLeftRadius: borderRadius,
                alignSelf: 'flex-start',
            };
            break;
        case 'UpperRight':
            borderStyle = {
                borderTopWidth: SQUARE_BORDER_WIDTH,
                borderRightWidth: SQUARE_BORDER_WIDTH,
                borderColor: colors.white,
                borderTopRightRadius: borderRadius,
                alignSelf: 'flex-start',
            };
            break;
        case 'LowerRight':
            borderStyle = {
                borderBottomWidth: SQUARE_BORDER_WIDTH,
                borderRightWidth: SQUARE_BORDER_WIDTH,
                borderColor: colors.white,
                borderBottomRightRadius: borderRadius,
                alignSelf: 'flex-end',
            };
            break;
        case 'LowerLeft':
            borderStyle = {
                borderBottomWidth: SQUARE_BORDER_WIDTH,
                borderLeftWidth: SQUARE_BORDER_WIDTH,
                borderColor: colors.white,
                borderBottomLeftRadius: borderRadius,
                alignSelf: 'flex-end',
            };
            break;
        default:
            break;
    }

    return (
        <View
            style={{
                width: SQUARE_WIDTH,
                height: SQUARE_HEIGHT,
                backgroundColor: colors.transparent,
                ...borderStyle,
            }}
        />
    );
};

const AimpointOverlay = () => {
    return (
        <View style={styles.overlayContainer}>
            <View style={styles.rowContainer}>
                <SquareComponent corner="UpperLeft" />
                <SquareComponent corner="UpperRight" />
            </View>
            <View style={styles.rowContainer}>
                <SquareComponent corner="LowerLeft" />
                <SquareComponent corner="LowerRight" />
            </View>
        </View>
    );
};

<View style={styles.container}>
    <Camera
        style={StyleSheet.absoluteFill}
        device={device}
         isActive
         frameProcessor={frameProcessor}
         frameProcessorFps={1}
     />
     <AimpointOverlay />
</View>

Hope this helps some React Native users that needs this for both platforms, using simple styling.