Ionic Vue QR Code Scanner with Capacitor
In the previous article, we’ve given a brief demonstration on how to use the capacitor-plugin-dynamsoft-barcode-reader to create a cross-platform QR code scanner using Ionic React. In this article, we are going to use Ionic Vue to create such a QR code scanner.
Getting started with Dynamsoft Barcode Reader
Building an Ionic Vue QR Code Scanner
Let’s do this in steps.
New project
Create a new Ionic Vue app:
ionic start QRCodeScanner --type=vue --capacitor
We can start a server to have a live test in the browser:
ionic serve
To run it on Android:
ionic capacitor add android
ionic capacitor copy android // sync files
ionic capacitor run android
To run it on iOS:
ionic capacitor add ios
ionic capacitor copy ios // sync files
ionic capacitor open ios // use Xcode to open the project
Add camera permission
For iOS, add the following to Info.plist
:
<key>NSCameraUsageDescription</key>
<string>For barcode scanning</string>
For Android, add the following to AndroidManifest.xml
:
<uses-permission android:name="android.permission.CAMERA" />
Install dependencies
Install the capacitor plugins to use Dynamsoft Barcode Reader and open the camera.
npm install capacitor-plugin-dynamsoft-barcode-reader capacitor-plugin-camera
Modify the home page and create a scanner page
In the home page, add a button to navigate to the scanner page, a checkbox for configuring whether to enable continuous scanning and display the barcode results if found.
Template of the home page:
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>QR Code Scanner</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-button expand="full" v-on:click="gotoScannerPage">Scan Barcodes</ion-button>
<ion-list>
<ion-item>
<ion-label>Continuous Scan</ion-label>
<ion-checkbox slot="end" v-model="sharedStates.continuousScan"></ion-checkbox>
</ion-item>
<ion-list-header v-if="sharedStates.barcodeResults.length>0">
<ion-label>Results:</ion-label>
</ion-list-header>
<ion-item v-bind:key="'result'+index" v-for="(result,index) in sharedStates.barcodeResults">
<ion-label>{{result}}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
Script of the home page:
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonButton, IonList, IonListHeader, IonLabel, IonCheckbox, IonItem, useIonRouter } from '@ionic/vue';
import { defineComponent } from 'vue';
import { states } from '../states'
export default defineComponent({
name: 'HomePage',
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButton,
IonList,
IonListHeader,
IonCheckbox,
IonLabel,
IonItem
},
setup() {
const router = useIonRouter();
const sharedStates = states;
const gotoScannerPage = () => {
router.push('/scanner');
}
return {
sharedStates,
gotoScannerPage
};
},
});
Create a states.ts
file to share the states between the two pages.
export const states = reactive({
continuousScan: false,
barcodeResults: [] as any[],
});
Create a scanner page under this path: src\views\ScannerPage.vue
.
<template>
<ion-page></ion-page>
</template>
<script lang="ts">
import { IonPage, useIonRouter } from '@ionic/vue';
import { defineComponent, onMounted } from 'vue';
import { states } from '../states'
export default defineComponent({
name: 'HomePage',
components: {
IonPage
},
setup() {
const sharedStates = states;
const router = useIonRouter();
const goBack = () => {
sharedStates.barcodeResults = ["1","2","3"]; // dummy results to test the navigation
router.back();
}
onMounted(() => {
setTimeout(goBack,2000);
});
},
});
</script>
<style scoped>
</style>
Configure the router for the scanner page in src/router/index.ts
:
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'Home',
component: HomePage
},
+ {
+ path: '/scanner',
+ name: 'Scanner',
+ component: ScannerPage
+ }
]
Create a QR code scanner component
-
Create a new component in the following path:
src\components\QRCodeScanner.vue
. -
Define props and emits:
const props = defineProps(['torchOn']); const emit = defineEmits(['onScanned','onPlayed']);
-
Add a container in the template.
<template> <div ref="container"> <div class="dce-video-container"></div> </div> </template> <script lang="ts" setup> import { ref } from 'vue'; const container = ref<HTMLDivElement|undefined>(); </script>
-
Initialize the plugin and start scanning in the
onMounted
event:<script lang="ts" setup> import { onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { DBR } from "capacitor-plugin-dynamsoft-barcode-reader"; import { CameraPreview } from "capacitor-plugin-camera"; import { Capacitor,PluginListenerHandle } from "@capacitor/core"; let onPlayedListener:PluginListenerHandle|undefined; onMounted(async () => { if (container.value && Capacitor.isNativePlatform() === false) { await CameraPreview.setElement(container.value); } await CameraPreview.initialize(); await CameraPreview.requestCameraPermission(); await DBR.initialize(); if (onPlayedListener) { onPlayedListener.remove(); } onPlayedListener = await CameraPreview.addListener("onPlayed", async () => { startDecoding(); const orientation = (await CameraPreview.getOrientation()).orientation; const resolution = (await CameraPreview.getResolution()).resolution; emit("onPlayed",{orientation:orientation,resolution:resolution}); }); await CameraPreview.startCamera(); }); </script>
Start an interval to read barcodes from the camera if the camera is opened.
let interval:any; let decoding = false; const startDecoding = () => { stopDecoding(); interval = setInterval(captureAndDecode,100); } const stopDecoding = () => { clearInterval(interval); } const captureAndDecode = async () => { if (decoding === true) { return; } let results = []; let dataURL; decoding = true; try { if (Capacitor.isNativePlatform()) { await CameraPreview.saveFrame(); results = (await DBR.decodeBitmap({})).results; }else{ let frame = await CameraPreview.takeSnapshot({quality:50}); dataURL = "data:image/jpeg;base64,"+frame.base64; results = await readDataURL(dataURL); } console.log(results); emit("onScanned",results); } catch (error) { console.log(error); } decoding = false; } const readDataURL = async (dataURL:string) => { let response = await DBR.decode({source:dataURL}); let results = response.results; return results; }
-
Remove the listeners and stop scanning in the
onBeforeUnmount
event:onBeforeUnmount(async () => { if (onPlayedListener) { onPlayedListener.remove(); } stopDecoding(); await CameraPreview.stopCamera(); console.log("QRCodeScanner unmount"); });
-
Watch the
torchOn
props for toggling the torch.watch(() => props.torchOn, (newVal, oldVal) => { if (newVal === true) { CameraPreview.toggleTorch({on:true}); }else{ CameraPreview.toggleTorch({on:false}); } });
Add the QR code scanner component to the scanner page
-
Add the QR code scanner component in the template:
<template> <ion-page> <QRCodeScanner @onScanned="onScanned" @onPlayed="onPlayed" ></QRCodeScanner> </ion-page> </template>
-
In the script, define
onScanned
andonPlayed
functions.export default defineComponent({ name: 'HomePage', components: { IonPage, QRCodeScanner, }, setup() { const sharedStates = states; const router = useIonRouter(); let scanned = false; const onScanned = (result:ScanResult) => { if (result.results.length>0 && scanned === false && sharedStates.continuousScan === false) { scanned = true; sharedStates.barcodeResults = result.results; router.back(); // go to home page with barcode results } } const onPlayed = (resolution:string) => { console.log(resolution); } return { sharedStates, onScanned, onPlayed }; }, });
-
Update
states.ts
to use theTextResult
type.export const states = reactive({ QRCodeOnly: false, continuousScan: false, - barcodeResults: [] as any[], + barcodeResults: [] as TextResult[], });
-
Update the home page to use the
TextResult
typed result.<ion-item v-bind:key="'result'+index" v-for="(result,index) in sharedStates.barcodeResults"> - <ion-label> {{result}}</ion-label> + <ion-label> {{result.barcodeFormat+": "+result.barcodeText}}</ion-label> </ion-item>
Draw QR code overlays
We can use SVG to draw QR code overlays in continuous scanning mode.
-
Add the following in the template:
<svg :viewBox="viewBox" class="overlay" v-if="sharedStates.continuousScan" > <polygon v-bind:key="'polygon'+index" v-for="(barcodeResult,index) in barcodeResults" :points="getPointsData(barcodeResult)" class="barcode-polygon" /> <text v-bind:key="'text'+index" v-for="(barcodeResult,index) in barcodeResults" :x="barcodeResult.x1" :y="barcodeResult.y1" fill="red" font-size="25" > {{barcodeResult.barcodeText}}</text> </svg>
-
Add the following styles:
.barcode-polygon { fill:rgba(85,240,40,0.5); stroke:green; stroke-width:1; } .overlay { top: 0; left: 0; position: absolute; width: 100%; height: 100%; z-index: 998; }
-
In the script, define a
viewBox
property and abarcodeResults
property usingref
and update them in theonPlayed
andonScanned
events.const viewBox = ref("0 0 1280 720"); const barcodeResults = ref([] as TextResult[]); const onScanned = (results:TextResult[]) => { if (results.length>0 && scanned === false && sharedStates.continuousScan === false) { scanned = true; document.documentElement.style.setProperty('--ion-background-color', ionBackground); sharedStates.barcodeResults = results; router.back(); }else{ barcodeResults.value = results; } } const onPlayed = (result:{orientation:"LANDSCAPE"|"PORTRAIT",resolution:string}) => { const width = result.resolution.split("x")[0]; const height = result.resolution.split("x")[1]; let frameWidth = parseInt(width); let frameHeight = parseInt(height); if (result.orientation === "PORTRAIT") { viewBox.value = "0 0 " + frameHeight + " " + frameWidth; }else{ viewBox.value = "0 0 " + frameWidth + " " + frameHeight; } } const getPointsData = (tr:TextResult) => { let pointsData = tr.x1 + "," + tr.y1 + " "; pointsData = pointsData + tr.x2+ "," + tr.y2 + " "; pointsData = pointsData + tr.x3+ "," + tr.y3 + " "; pointsData = pointsData + tr.x4+ "," + tr.y4; return pointsData; }
Add floating action buttons to the scanner page
We can add floating action buttons to control the scanning.
Here, we add a button to stop scanning and a button to toggle the torch.
-
Add the following to
ScannerPage.vue
’s template:<ion-fab vertical="bottom" horizontal="start" slot="fixed"> <ion-fab-button> <ion-icon name="ellipsis-horizontal-outline"></ion-icon> </ion-fab-button> <ion-fab-list side="top"> <ion-fab-button @click="toggleFlash"> <ion-icon name="flashlight-outline"></ion-icon> </ion-fab-button> <ion-fab-button @click="close"> <ion-icon name="close-outline"></ion-icon> </ion-fab-button> </ion-fab-list> </ion-fab>
-
In the script, add icons, define relevant functions and properties:
setup() { addIcons({ 'ellipsis-horizontal-outline': ellipsisHorizontalOutline, 'flashlight-outline': flashlightOutline, 'close-outline': closeOutline, }); const torchOn = ref(false); const toggleFlash = () => { torchOn.value = !torchOn.value; } const close = () => { router.back(); } return { torchOn, toggleFlash, close, sharedStates, viewBox, barcodeResults, getPointsData, onScanned, onPlayed }; }
All right, we’ve now finished the Ionic Vue QR Code Scanner.
Source Code
Check out the source code to have a try on your own: