Animated QR Code Scanner Bounding Box in React Native

Animated QR Code Scanner Bounding Box in React Native

Enhance your React Native QR code scanning experience with a smooth, animated bounding box that highlights detected codes in real-time. This blog post explores how to achieve a visually polished and responsive scanning interface using expo-camera and react-native-reanimated. We'll dive into the code to see how it's done.

Introduction

A QR code scanner is a common feature in many mobile applications. While expo-camera provides the core functionality for scanning, adding an animated bounding box around detected QR codes significantly improves user feedback and the overall aesthetic. This component focuses on creating a dynamic and animated bounding box that appears, moves, and disappears gracefully as QR codes are detected or go out of view.

Technologies Used

This animated bounding box solution leverages two key React Native libraries:

  • expo-camera: This library is essential for accessing the device's camera and scanning QR codes in real time. It provides the underlying capability to detect the codes.
  • react-native-reanimated: This powerful animation library is used to animate the bounding box. Specifically, it utilizes Reanimated's useSharedValue, useAnimatedStyle, withSpring, withTiming, FadeInDown, and FadeOutDown to ensure the bounding box appears and moves smoothly without flickering or jank.

How it Works

The component operates by dynamically rendering and animating the bounding box when a QR code is detected. Here's a breakdown of the process:

  1. Camera Permission: The component first requests camera permissions from the user. If not granted, it prompts the user to grant permission.
  2. QR Code Detection: expo-camera continuously scans for QR codes using the onBarcodeScanned prop. When a code is detected, it provides the BarcodeScanningResult, including the bounds (origin and size) of the detected QR code.
  3. Dynamic Bounding Box Animation:
    • Shared values (x, y, boxWidth, boxHeight, opacity) are used to control the position, size, and visibility of the bounding box.
    • When a barcode is scanned, these shared values are updated using withSpring for smooth, spring-like animations to match the detected QR code's bounds. An extraSize is added to the bounding box to make it slightly larger than the detected code, improving visibility.
    • The opacity is animated to 1 when a code is detected and then animated back to 0 after a short delay, making the box fade out.
  4. Displaying Scanned Data: If barcodeScanningData is available, an Animated.View containing the scanned data (or children passed to the component) is rendered. This view uses FadeInDown and FadeOutDown layout animations from Reanimated to appear and disappear gracefully, providing additional feedback to the user.
  5. Resetting Box Size: After the FadeOutDown animation completes for the scanned data display, the bounding box's shared values (x, y, boxWidth, boxHeight) are reset to their initial states, preparing for the next scan.

This approach guarantees a responsive and visually polished scanning experience, minimizing any visual disruptions during transitions between detection states.

Code Snippets

Here's the core code for the QRScanner component, demonstrating the use of expo-camera and react-native-reanimated to create the animated bounding box.

First, the App.tsx file to render the QRScanner component:

1import { QRScanner } from "./";
2
3export default function App() {
4 return <QRScanner />;
5}
6

And here's the index.tsx containing the QRScanner component logic:

1import type { BarcodeScanningResult } from "expo-camera";
2import { CameraView, useCameraPermissions } from "expo-camera";
3import React, { useState } from "react";
4import {
5 Button,
6 StyleSheet,
7 Text,
8 useWindowDimensions,
9 View,
10 ViewStyle,
11} from "react-native";
12import Animated, {
13 FadeInDown,
14 FadeOutDown,
15 runOnJS,
16 useAnimatedStyle,
17 useSharedValue,
18 withDelay,
19 withSpring,
20 withTiming,
21} from "react-native-reanimated";
22
23export function QRScanner({
24 onBarcodeScanned,
25 onCameraReady,
26 children,
27 style,
28}: {
29 onBarcodeScanned?: CameraView["props"]["onBarcodeScanned"];
30 onCameraReady?: () => void;
31 children?: React.ReactNode;
32 style?: ViewStyle;
33}) {
34 const [barcodeScanningData, setBarcodeScanningData] = useState<
35 BarcodeScanningResult["data"] | null
36 >(null);
37 const { width, height } = useWindowDimensions();
38 // Shared values for the bounding box
39 const x = useSharedValue(width / 2);
40 const y = useSharedValue(height / 2);
41 const boxWidth = useSharedValue(0);
42 const boxHeight = useSharedValue(0);
43 const opacity = useSharedValue(0);
44
45 const handleBarCodeScanned = (scanningResults: BarcodeScanningResult) => {
46 const { bounds } = scanningResults;
47 if (bounds) {
48 if (scanningResults.data !== barcodeScanningData) {
49 setBarcodeScanningData(scanningResults.data);
50 onBarcodeScanned?.(scanningResults);
51 }
52 x.value = withSpring(bounds.origin.x);
53 y.value = withSpring(bounds.origin.y);
54 boxWidth.value = withSpring(bounds.size.width);
55 boxHeight.value = withSpring(bounds.size.height);
56 opacity.value = withSpring(1, { duration: 300 }, (finished) => {
57 if (finished) {
58 opacity.value = withDelay(
59 200,
60 withTiming(0, { duration: 300 }, (finished) => {
61 if (finished) {
62 runOnJS(setBarcodeScanningData)(null);
63 }
64 })
65 );
66 }
67 });
68 } else {
69 // Hide the rectangle after 2 seconds
70 // opacity.value = withTiming(0, { duration: 300 });
71 }
72 };
73
74 const animatedStyle = useAnimatedStyle(() => {
75 const extraSize = 0.1;
76 return {
77 position: "absolute",
78 left: x.value - (boxWidth.value * extraSize) / 2,
79 top: y.value - (boxHeight.value * extraSize) / 2,
80 width: boxWidth.value * (1 + extraSize),
81 height: boxHeight.value * (1 + extraSize),
82 borderWidth: 2,
83 borderStyle: "dashed",
84 // a better color for a qr code scanner
85 borderColor: "rgba(0,0,0,0.9 )",
86 borderRadius: boxWidth.value / 10,
87 opacity: opacity.value,
88 };
89 });
90
91 const [permission, requestPermission] = useCameraPermissions();
92
93 if (!permission || !permission.granted) {
94 return (
95 <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
96 <Button title='Request Camera Permission' onPress={requestPermission} />
97 </View>
98 );
99 }
100
101 return (
102 <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
103 <CameraView
104 style={StyleSheet.absoluteFillObject}
105 onBarcodeScanned={handleBarCodeScanned}
106 onCameraReady={onCameraReady}
107 mute
108 />
109 <Animated.View style={[animatedStyle, { alignItems: "center" }]}>
110 {barcodeScanningData && (
111 <Animated.View
112 key={barcodeScanningData}
113 entering={FadeInDown.springify().delay(200)}
114 exiting={FadeOutDown.springify().withCallback((finished) => {
115 //reset the box size to 0 after the animation is done
116 if (finished) {
117 x.value = width / 2;
118 y.value = height / 2;
119 boxWidth.value = 0;
120 boxHeight.value = 0;
121 }
122 })}
123 style={[
124 {
125 position: "absolute",
126 top: "100%",
127 marginTop: 10,
128 backgroundColor: "rgba(0,0,0,0.5)",
129 paddingVertical: 4,
130 paddingHorizontal: 8,
131 borderRadius: 20,
132 minWidth: 200,
133 },
134 style,
135 ]}>
136 {children ?? (
137 <Text
138 style={{ color: "#fff", fontSize: 13 }}
139 numberOfLines={1}
140 adjustsFontSizeToFit>
141 {barcodeScanningData}
142 </Text>
143 )}
144 </Animated.View>
145 )}
146 </Animated.View>
147 </View>
148 );
149}
150
151

Styling

The animatedStyle defines the visual properties of the bounding box. Key aspects include:

  • position: "absolute": Allows the bounding box to float over the camera view.
  • left****, top****, width****, height: Dynamically set by useAnimatedStyle based on the scanned QR code's bounds and animated using shared values.
  • borderWidth****, borderStyle****, borderColor****, borderRadius: Styles the box visually, with a dashed border and dynamic borderRadius for a smoother look.
  • opacity: Controls the visibility of the box, fading in and out with withSpring and withTiming.

The Animated.View for barcodeScanningData also includes styling for the data display, such as backgroundColor, padding, and borderRadius to create a clear, readable label for the scanned code.

Conclusion

Implementing an animated bounding box for your React Native QR code scanner elevates the user experience by providing clear, smooth visual feedback. By combining the real-time scanning capabilities of expo-camera with the fluid animations of react-native-reanimated, developers can create a professional and engaging scanning interface that feels polished and modern. This example demonstrates a robust solution for adding this engaging feature to your mobile applications.