Adding a QR Code Reader in Flex on Android
One of the features I commonly see requested for Android applications is QR code reader or barcode scanner integration. Some native Android applications actually use an external application for QR code/barcode scanning. That, as far as I know, is not an option at the moment in AIR on Android. However, thanks to the open source ZXing library, some direction from Amer Dababneh and a blog post by Michael B, I was able to create basic sample Flex-based mobile application that accessed the camera and read QR codes. This blog post will show you how.
For any smart asses out there (yes, I mean you Ray) - yes, I can still code. It’s just been a while since I had something “blogworthy.” :)
Create Your Project
To begin with, of course, you need to create your new Flex mobile project - I used a standard view-based application. I should note that while I only tested this on Android (via my Nexus One), it’s entirely possible that this code will work as is on iOS or Blackberry Tablet OS via the recent Flash Builder 4.5.1 update.
The important thing to note here is that you will need to enable Camera permissions in your app.xml file. This can be done during the new project wizard or by manually adding the below items to your app.xml within the Android “manifestAdditions” section:
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.autofocus"/>
In my experience, if you forget to do this or don’t do it properly, the camera will simply fail silently (which can lead to needless debugging frustration - so just don’t forget).
Adding the Camera
Now that we’ve created our new Flex mobile application, we need to lay out our sample application. My sample is very simple with only a home view that contains the camera, a label to display any results and a button to start the process and capture the QR codes. For reference, I borrowed some of the camera placement code from this blog post by Guillaume.
You can see the visual elements of the page laid out below. The SpriteVisualElement is the container that will hold the view to our camera.
<s:VGroup width="100%" horizontalAlign="center" id="vg">
<s:SpriteVisualElement id="sv" width="360" height="400"/>
<s:Label id="lbl" text="" />
<s:Button id="btn" label="Start Camera" width="220" height="93" click="button1_clickHandler(event)"/>
</s:VGroup>
The first time the user clicks the button, it will start the camera. Inside my click handler for the button, I have the following code that first gets an instance of the Camera, attaches it to a VideoDisplay object which is added as a child to our SpriteVisualElement. We also initialize our QRCodeReader class which is from the ZXing library.
if (Camera.isSupported)
{
camera=Camera.getCamera();
camera.setMode(360, 360, 24);
videoDisplay.x = 360;
sv.addChild(videoDisplay);
videoDisplay.attachCamera(camera);
videoDisplay.rotation=90;
qrReader=new QRCodeReader;
btn.label = "Scan Now";
lbl.text = "";
cameraStarted = true;
}
else {
lbl.text = "no camera found";
}
If you ran your app right now (see the full code at the end of this post for all the necessary variables and imports) you would see a view into your device’s camera appear once you click the button to start - that is if our QRCodeReader wasn’t throwing compile errors. Let’s get those fixed.
Modifying the ZXing Library
Obviously, in order to create this project you must download the ZXing library from Google Code. Simply dump the library into your “src” folder where it should be under a com.google.zxing package. As I noted, you will get some complile errors as soon as you try to use the necessary files for QRCodeScanning. Luckily they are very easy to correct as they all involve unnecessary or missing imports.
Here are the changes required to get this working on our project:
- In QRCodeMultiReader add import com.google.zxing.BinaryBitmap; (or just press ctrl+space after the BinaryBitmap on the line with the compile error and have Flash Builder add the import for you);
- In DecoderResult comment out import of mx.controls.List;
- In BufferedImageLuminanceSource comment out import mx.controls.Image;.
Reading the QR Code
Most of the ZXing library usage code to read the QR code was taken verbatim from the blog post by Michael B referenced above. The decodeSnapshot method, which is triggered when the user presses the button to capture a QR code, gets the bitmap data from the Camera and passes it to the decodeBitmapData method. The decodeBitmapData method mostly handles passing this bitmap data to the ZXing library for decoding and then displaying the result in the label. The getAllHints method actually tells the ZXing library what kind of QR/barcode we are trying to decode. Theoretically, if you wanted to decode other standard barcode types, you would just add those to the hashtable. ZXing supports many types of barcode scanning and if you simply add the other types, it should just work - but I emphasize should because I didn’t test it yet.
public function decodeSnapshot():void
{
lbl.text="checking...";
bmd=new BitmapData(300, 300);
bmd.draw(videoDisplay, null, null, null, null, true);
videoDisplay.cacheAsBitmap=true;
videoDisplay.cacheAsBitmapMatrix=new Matrix;
decodeBitmapData(bmd, 300, 300);
bmd.dispose();
bmd=null;
System.gc();
}
public function decodeBitmapData(bmpd:BitmapData, width:int, height:int):void
{
var lsource:BufferedImageLuminanceSource=new BufferedImageLuminanceSource(bmpd);
var bitmap:BinaryBitmap=new BinaryBitmap(new GlobalHistogramBinarizer(lsource));
var ht:HashTable=null;
ht=this.getAllHints();
var res:Result=null;
try {
res=qrReader.decode(bitmap, ht);
}
catch (event:Error) {
}
if (res == null) {
videoDisplay.clear();
lbl.text="nothing decoded";
}
else {
var parsedResult:ParsedResult=ResultParser.parseResult(res);
lbl.text=parsedResult.getDisplayResult();
sv.removeChild(videoDisplay);
cameraStarted = false;
btn.label = "Start Camera";
}
}
public function getAllHints():HashTable
{
var ht:HashTable=new HashTable;
ht.Add(DecodeHintType.POSSIBLE_FORMATS, BarcodeFormat.QR_CODE);
return ht;
}
Testing Your QR Code Reading
For testing, I used this QR Code generator for encode both URL and straight text encoded QR codes. In my tests, not only did the QR code reading work well but it ran pretty fast (and my Nexus One is no powerhouse any longer - if it ever was). One thing I did note though is that it can be pretty picky about where the QR code is placed within the image. It should be centered and should not be so close that it takes up the entire camera. I think this is partly because we are actually passing only a 300x300 bitmap out of our 360x360 camera image to be decoded, so this might be fixable. If that isn’t correctable in that manner than I thought it would be wise to add some guides as an overlay to help the user know where to align the QR code.
The other thing I noticed was that when I created a timer event and tested checked the camera periodically using that (as in the linked sample I sourced), I did get some performance issues where the app periodically became unusable (mostly during debugging mode but it seemed indicative of a problem you’d want to potentially eliminate).
That’s it. Not too complicated eh? For reference, below is the finished Flex Mobile View with all the elements you need to get this sample running. Let me know if this helps you or if you manage to expand upon the example (and feel free to share your code if you do).
P.S. I am not posting an FXP because I am unsure if there are any redistribution restrictions on the license for ZXing.
<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:mx="library://ns.adobe.com/flex/mx"
xmlns:s="library://ns.adobe.com/flex/spark" title="HomeView">
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
<fx:Script>
<![CDATA[
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.BufferedImageLuminanceSource;
import com.google.zxing.DecodeHintType;
import com.google.zxing.Result;
import com.google.zxing.client.result.ParsedResult;
import com.google.zxing.client.result.ResultParser;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.ByteMatrix;
import com.google.zxing.common.GlobalHistogramBinarizer;
import com.google.zxing.common.flexdatatypes.HashTable;
import com.google.zxing.qrcode.QRCodeReader;
import com.google.zxing.qrcode.detector.Detector;
import spark.events.ViewNavigatorEvent;
protected var camera:Camera;
private var videoDisplay:Video=new Video(360, 360);
private var qrReader:QRCodeReader;
private var bmd:BitmapData;
private var cameraStarted:Boolean = false;
protected function button1_clickHandler(event:MouseEvent):void
{
if (!cameraStarted) {
if (Camera.isSupported)
{
camera=Camera.getCamera();
camera.setMode(360, 360, 24);
videoDisplay.x = 360;
sv.addChild(videoDisplay);
videoDisplay.attachCamera(camera);
videoDisplay.rotation=90;
qrReader=new QRCodeReader;
btn.label = "Scan Now";
lbl.text = "";
cameraStarted = true;
}
else {
lbl.text = "no camera found";
}
}
else {
decodeSnapshot();
}
}
public function decodeSnapshot():void
{
lbl.text="checking...";
bmd=new BitmapData(300, 300);
bmd.draw(videoDisplay, null, null, null, null, true);
videoDisplay.cacheAsBitmap=true;
videoDisplay.cacheAsBitmapMatrix=new Matrix;
decodeBitmapData(bmd, 300, 300);
bmd.dispose();
bmd=null;
System.gc();
}
public function decodeBitmapData(bmpd:BitmapData, width:int, height:int):void
{
var lsource:BufferedImageLuminanceSource=new BufferedImageLuminanceSource(bmpd);
var bitmap:BinaryBitmap=new BinaryBitmap(new GlobalHistogramBinarizer(lsource));
var ht:HashTable=null;
ht=this.getAllHints();
var res:Result=null;
try {
res=qrReader.decode(bitmap, ht);
}
catch (event:Error) {
}
if (res == null) {
videoDisplay.clear();
lbl.text="nothing decoded";
}
else {
var parsedResult:ParsedResult=ResultParser.parseResult(res);
lbl.text=parsedResult.getDisplayResult();
sv.removeChild(videoDisplay);
cameraStarted = false;
btn.label = "Start Camera";
}
}
public function getAllHints():HashTable
{
var ht:HashTable=new HashTable;
ht.Add(DecodeHintType.POSSIBLE_FORMATS, BarcodeFormat.QR_CODE);
return ht;
}
]]>
</fx:Script>
<s:VGroup width="100%" horizontalAlign="center" id="vg">
<s:SpriteVisualElement id="sv" width="360" height="400"/>
<s:Label id="lbl" text="" />
<s:Button id="btn" label="Start Camera" width="220" height="93" click="button1_clickHandler(event)"/>
</s:VGroup>
</s:View>
http://remotesynthesis.com/post.cfm/adding-a-qr-code-reader-in-flex-on-android