Tuesday, April 23, 2013

Creating Custom Overlays in Xzing Barcode Scanner using Xamarin Studio and C#



I've recently had need to customize the UI overlay of the excellent and free barcode scanning plugin Zxing.   While gathering material for this post I discovered that an example is provided of customizing the overlay via XML, but when I initially searched the first example I came across was the C# method - so that's the route I took.  The C# version did not have the ability to turn the flash on/off which was a feature I needed to access, so I figured out how to add that while styling.  I'm going to go over how I modified the example class created by Redth to help anyone else who might attempt the same thing for their own Android app.

For starters, you need to modify your call to Zxing to specify the use of a custom overlay:

var scanner = new ZXing.MobileMobileBarcodeScanner();
scanner.UseCustomOverlay = true;
myCustomOverlayInstance = new ZxingOverlayView(this, scanner);
scanner.CustomOverlay = myCustomOverlayInstance;
scanner.Scan().ContinueWith(t => { //Handle Result });

After that, you just need to customize the properties and methods of the ZxingOverlayView example class file.  In my example I added the ability to control the torch (flashlight) and display bitamaps so that I could have my own custom buttons.  I specified a default color and a pressed color, along with boolean flags so I could change the buttons to confirm to the user that they had pressed a button while waiting for a response:

namespace MyProject
{
 public class ZxingOverlayView : View 
 {
  private Paint defaultPaint;
  private Paint pressedPaint;
  private Android.Graphics.Bitmap resultBitmap;
  private Android.Graphics.Bitmap litTorchIcon;
                private Android.Graphics.Bitmap unlitTorchIcon;
  private Rect torchIconDimRect;
  private bool hasTorch = false;
  private bool torchOn = false;
  private bool cancelPressed = false;
  private bool problemPressed = false;
...
public ZxingOverlayView(Context context, MobileBarcodeScanner scanner) : base(context)
{
 this.context = context;
 this.scanner = scanner;
 
 SetDisplayValues ();
}
...
private void SetDisplayValues ()
{
 //Determine if device has flash/torch
 hasTorch = this.Context.PackageManager.HasSystemFeature (PackageManager.FeatureCameraFlash);
 if (hasTorch) {
  //Load button icons
  var metrics = Resources.DisplayMetrics;
  int iconHeight = metrics.HeightPixels * 2 / 9;
  int iconWidth = iconHeight * 7 / 10;
  torchIconDimRect = new Rect (0, 0, iconWidth, iconHeight);
  litTorchIcon = ImageHelper.DecodeSampledBitmapFromResource (Resources, Resource.Drawable.bulb_lit, iconWidth, iconHeight);
  unlitTorchIcon = ImageHelper.DecodeSampledBitmapFromResource (Resources, Resource.Drawable.bulb_unlit, iconWidth, iconHeight);
 }
 // Initialize these once for performance rather than calling them every time in onDraw()
 defaultPaint = new Paint (PaintFlags.AntiAlias);
 pressedPaint = new Paint (PaintFlags.AntiAlias);
 pressedPaint.Color = new Color (96, 97, 104);
 buttonFontColor = Color.White;

For each of my buttons I had to specify a rectangle on the screen.  It's best if you specify not in static pixels, but in percentage of the total screen available.  This will ensure your overlay renders the same on any size of screen.  I created three buttons, plus a rectangle to hold my torch icon bitmap.  Notice that the TorchIconDimRect was created as part of SetDisplayValues() above as we decoded and resized the bitmap icon for it at the same time:

Rect GetProblemButtonRect()
{
 
 var metrics = Resources.DisplayMetrics;
 int width = metrics.WidthPixels * 7 / 16 ;
 int height = metrics.HeightPixels * 2 / 9;
 int leftOffset = metrics.WidthPixels * 33 / 64;
 int topOffset = metrics.HeightPixels * 3 / 4;
 var problemRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
 
 return problemRect;
}

Rect GetCancelButtonRect()
{
 var metrics = Resources.DisplayMetrics;
 int width = metrics.WidthPixels * 7 / 16 ;
 int height = metrics.HeightPixels * 2 / 9;
 int leftOffset = metrics.WidthPixels / 22;
 int topOffset = metrics.HeightPixels * 3 / 4;
 var cancelRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
 
 return cancelRect;
}

Rect GetTorchIconDimRect()
{
 return torchIconDimRect;
}

Rect GetTorchIconRect()
{
 var metrics = Resources.DisplayMetrics;
 int height = metrics.HeightPixels / 8;
 int width = height * 7 / 10;
 int leftOffset = metrics.WidthPixels / 2 - ( width / 2);
 int topOffset = metrics.HeightPixels /18;
 var torchRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
 
 return torchRect;
}

Rect GetTorchButtonRect()
{
 var metrics = Resources.DisplayMetrics;
 int width = metrics.WidthPixels * 3 / 8 ;
 int height = metrics.HeightPixels * 3 / 16;
 int leftOffset = metrics.WidthPixels / 2 - (width / 2);
 int topOffset = metrics.HeightPixels / 64;
 var torchRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
 
 return torchRect;
}

Here is the ImageHelper class file which you will only need if you are using bitmaps on your overlay.  This helper class will resize your bitmap to the desired height to prevent image distortion:

internal static class ImageHelper
{
 public static int CalculateInSampleSize(BitmapFactoryOptions options, int reqWidth, int reqHeight)
 {
  // Raw height and width of image
  var height = (float)options.OutHeight;
  var width = (float)options.OutWidth;
  var inSampleSize = 1D;
  
  if (height > reqHeight || width > reqWidth)
  {
   inSampleSize = width > height
    ? height/reqHeight
     : width/reqWidth;
  }
  
  return (int) inSampleSize;
 }
 
 public static Bitmap DecodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight)
 {
  // First decode with inJustDecodeBounds=true to check dimensions
  var options = new BitmapFactoryOptions { InJustDecodeBounds = true };
  BitmapFactory.DecodeResource(res, resId, options); 
  
  // Calculate inSampleSize
  options.InSampleSize = CalculateInSampleSize(options, reqWidth, reqHeight);
  
  // Decode bitmap with inSampleSize set
  options.InJustDecodeBounds = false;
   Bitmap optimalSize = BitmapFactory.DecodeResource (res, resId, options);
   return Bitmap.CreateScaledBitmap (optimalSize, reqWidth, reqHeight, false);
 }
}

So now that all the colors, shapes and bitmaps have been defined you just need to draw them to the screen.  This is done through the OnDraw() method:

protected override void OnDraw (Canvas canvas)
{
 var scale = Resources.DisplayMetrics.Density;
 
 var frame = GetFramingRect();
 if (frame == null)
  return;
 
 var probBtn = GetProblemButtonRect();
 var cancelBtn = GetCancelButtonRect();
 var torchBtn = GetTorchButtonRect();
 
 var width = canvas.Width;
 var height = canvas.Height;
 var textPaint = new TextPaint();
 textPaint.Color = buttonFontColor;
 textPaint.AntiAlias = true;
 textPaint.BgColor = Color.Gray;
 textPaint.TextSize = 16 * scale;
 
 //Draw mask
 defaultPaint.Color = resultBitmap != null ? resultColor : maskColor;
 defaultPaint.Alpha = 245;
 canvas.DrawRect(0, 0, width, frame.Top, defaultPaint);
 canvas.DrawRect(0, frame.Bottom + 1, width, height, defaultPaint);
 
 //Draw button outlines
 defaultPaint.Color = buttonFontColor;
 defaultPaint.Alpha = 255;
 pressedPaint.Color = Color.Black;
 canvas.DrawRect (probBtn.Left+1, probBtn.Top +1, probBtn.Right + 1, probBtn.Bottom + 1, problemPressed ? pressedPaint : defaultPaint);
 canvas.DrawRect (cancelBtn.Left+1, cancelBtn.Top +1, cancelBtn.Right + 1, cancelBtn.Bottom + 1, cancelPressed ? pressedPaint : defaultPaint);
 if (hasTorch)
  canvas.DrawRect (torchBtn.Left + 1, torchBtn.Top + 1, torchBtn.Right + 1, torchBtn.Bottom + 1, torchOn ? pressedPaint : defaultPaint);
 
 //Draw buttons
 defaultPaint.Color = buttonColor;
 defaultPaint.Alpha = 255;
 pressedPaint.Color = new Color(96, 97, 104);
 canvas.DrawRect (probBtn, problemPressed ? pressedPaint : defaultPaint);
 canvas.DrawRect (cancelBtn, cancelPressed ? pressedPaint : defaultPaint);
 if (hasTorch)
 {
  canvas.DrawRect (torchBtn, torchOn ? pressedPaint : defaultPaint);
  //Draw button icons
  canvas.DrawBitmap ((torchOn ? litTorchIcon : unlitTorchIcon), GetTorchIconDimRect(), GetTorchIconRect(), defaultPaint);
 }
 
 //Draw button text
 var btnText = new StaticLayout("Scan problems?", textPaint, probBtn.Width(), Android.Text.Layout.Alignment.AlignCenter, 1.0f, 0.0f, false);
 canvas.Save();
 canvas.Translate(probBtn.Left, probBtn.Top + (probBtn.Height ()/3)+ (btnText.Height / 2));
 btnText.Draw(canvas);
 canvas.Restore();
 btnText = new StaticLayout("Cancel Scan", textPaint, cancelBtn.Width(), Android.Text.Layout.Alignment.AlignCenter, 1.0f, 0.0f, false);
 canvas.Save();
 canvas.Translate(cancelBtn.Left, cancelBtn.Top + (cancelBtn.Height ()/3) + (btnText.Height / 2));
 btnText.Draw(canvas);
 canvas.Restore();

If your buttons are pressed, you can capture that event by overriding the OnTouchEvent method.  Notice that in each button touch event I call this.Invalidate().  This call causes the screen to refresh and without it the OnDraw() method would not be invoked, and the screen would not reflect our buttons change in display when pressed.

public override bool OnTouchEvent(MotionEvent me)
{
 if (me.Action == MotionEventActions.Down)
 {
  if (GetCancelButtonRect().Contains((int)me.RawX, (int)me.RawY))
  {
   cancelPressed = true;
   this.Invalidate();
   OnUnload();
   scanner.Cancel();
  }
  else if (GetTorchButtonRect().Contains ((int)me.RawX, (int)me.RawY))
  {
   scanner.ToggleTorch();
   torchOn = !torchOn;
   this.Invalidate();
  }
  else if (GetProblemButtonRect().Contains ((int)me.RawX, (int)me.RawY))
  {
   problemPressed = true;
   this.Invalidate();
   
   if (parentActivity == null)
   {
    Intent intent = new Intent();
    intent.SetClass(context, typeof(ManualEntryActivity));
    context.StartActivity(intent);
   }
   else
   {
    parentActivity.GetManualEntry();
   }
   OnUnload();
   scanner.Cancel();
  }
  return true;
 }
 else
  return false;
}

Lastly, I read on a stackoverflow post that graphics and bitmaps are two objects which you must take care to properly dispose of when they are no longer needed  If you don't properly dispose of the bitmaps created for your custom overlay then there is a potential for a memory leak.  I dispose of my bitmaps by calling this OnUnload() method that I use at all points of exit:

private void OnUnload()
{
 if (hasTorch)
 {
  litTorchIcon.Dispose();
  unlitTorchIcon.Dispose();
 }
}

This is how my custom overlay turned out.  (Ignore the black & white checkerboard with green square which is just a test display for the android emulator's camera)


Hopefully my post helped you.  Feel free to post questions if you are having problems, or need an explanation on any of my code.

No comments:

Post a Comment